Commit e70352b3 authored by Rene Saarsoo's avatar Rene Saarsoo
Browse files

Big refactoring of Merger.

Dealing with DocParser results is now removed from merger.  Instead the
DocAst class deals with it and produces doc hash just like the Ast class
does for code part.

So the merger is now really just tasked with merging the two doc hashes
together.
parent db4a9f5f
Loading
Loading
Loading
Loading

lib/jsduck/doc_ast.rb

0 → 100644
+291 −0
Original line number Diff line number Diff line
require 'jsduck/logger'
require 'jsduck/meta_tag_registry'

module JsDuck

  # Detects docs info directly from comment.
  class DocAst
    # Allow passing in filename and line for error reporting
    attr_accessor :filename
    attr_accessor :linenr

    def initialize
      @filename = ""
      @linenr = 0
      @meta_tags = MetaTagRegistry.instance
    end

    # Given tagname and array of tags from DocParser, produces docs
    # of the type determined by tagname.
    def detect(tagname, docs)
      case tagname
      when :class
        create_class(docs)
      when :event
        create_event(docs)
      when :method
        create_method(docs)
      when :cfg
        create_cfg(docs)
      when :property
        create_property(docs)
      when :css_var
        create_css_var(docs)
      when :css_mixin
        create_css_mixin(docs)
      end
    end

    private

    def create_class(docs)
      doc_map = build_doc_map(docs)
      return add_shared({
        :tagname => :class,
        :name => detect_name(:class, doc_map),
        :doc => detect_doc(docs),
        :extends => detect_extends(doc_map),
        :mixins => detect_list(:mixins, doc_map),
        :alternateClassNames => detect_list(:alternateClassNames, doc_map),
        :aliases => detect_aliases(doc_map),
        :singleton => !!doc_map[:singleton],
        :requires => detect_list(:requires, doc_map),
        :uses => detect_list(:uses, doc_map),
      }, doc_map)
    end

    def create_method(docs)
      doc_map = build_doc_map(docs)
      name = detect_name(:method, doc_map)
      return add_shared({
        :tagname => :method,
        :name => name,
        :owner => detect_owner(doc_map),
        :doc => detect_doc(docs),
        :params => detect_params(doc_map),
        :return => detect_return(doc_map, name == "constructor" ? "Object" : "undefined"),
      }, doc_map)
    end

    def create_event(docs)
      doc_map = build_doc_map(docs)
      return add_shared({
        :tagname => :event,
        :name => detect_name(:event, doc_map),
        :owner => detect_owner(doc_map),
        :doc => detect_doc(docs),
        :params => detect_params(doc_map),
      }, doc_map)
    end

    def create_cfg(docs)
      doc_map = build_doc_map(docs)
      return add_shared({
        :tagname => :cfg,
        :name => detect_name(:cfg, doc_map),
        :owner => detect_owner(doc_map),
        :type => detect_type(:cfg, doc_map),
        :doc => detect_doc(docs),
        :default => detect_default(:cfg, doc_map),
        :properties => detect_subproperties(:cfg, docs),
        :accessor => !!doc_map[:accessor],
        :evented => !!doc_map[:evented],
      }, doc_map)
    end

    def create_property(docs)
      doc_map = build_doc_map(docs)
      return add_shared({
        :tagname => :property,
        :name => detect_name(:property, doc_map),
        :owner => detect_owner(doc_map),
        :type => detect_type(:property, doc_map),
        :doc => detect_doc(docs),
        :default => detect_default(:property, doc_map),
        :properties => detect_subproperties(:property, docs),
      }, doc_map)
    end

    def create_css_var(docs)
      doc_map = build_doc_map(docs)
      return add_shared({
        :tagname => :css_var,
        :name => detect_name(:css_var, doc_map),
        :owner => detect_owner(doc_map),
        :type => detect_type(:css_var, doc_map),
        :doc => detect_doc(docs),
      }, doc_map)
    end

    def create_css_mixin(docs)
      doc_map = build_doc_map(docs)
      return add_shared({
        :tagname => :css_mixin,
        :name => detect_name(:css_mixin, doc_map),
        :owner => detect_owner(doc_map),
        :doc => detect_doc(docs),
        :params => detect_params(doc_map),
      }, doc_map)
    end

    # Detects properties common for each doc-object and adds them
    def add_shared(hash, doc_map)
      hash.merge!({
        :inheritable => !!doc_map[:inheritable],
        :inheritdoc => extract(doc_map, :inheritdoc),
        :meta => detect_meta(doc_map),
      })
      # copy :private also to main hash
      hash[:private] = true if hash[:meta][:private]

      return hash
    end

    def detect_name(tagname, doc_map)
      name = extract(doc_map, tagname, :name)
      if name
        name
      else
        doc_map[:constructor] ? "constructor" : nil
      end
    end

    def extract(doc_map, tagname, propname = nil)
      tag = doc_map[tagname] ? doc_map[tagname].first : nil
      if tag && propname
        tag[propname]
      else
        tag
      end
    end

    def detect_owner(doc_map)
      extract(doc_map, :member, :member)
    end

    def detect_type(tagname, doc_map)
      extract(doc_map, tagname, :type) || extract(doc_map, :type, :type)
    end

    def detect_extends(doc_map)
      extract(doc_map, :extends, :extends)
    end

    def detect_default(tagname, doc_map)
      extract(doc_map, tagname, :default)
    end

    # for detecting mixins and alternateClassNames
    def detect_list(type, doc_map)
      if doc_map[type]
        doc_map[type].map {|d| d[type] }.flatten
      else
        nil
      end
    end

    def detect_aliases(doc_map)
      if doc_map[:alias]
        doc_map[:alias].map {|tag| tag[:name] }
      else
        nil
      end
    end

    def detect_meta(doc_map)
      meta = {}
      (doc_map[:meta] || []).map do |tag|
        meta[tag[:name]] = [] unless meta[tag[:name]]
        meta[tag[:name]] << tag[:doc]
      end

      meta.each_pair do |key, value|
        tag = @meta_tags[key]
        meta[key] = tag.to_value(tag.boolean ? true : value)
      end

      meta[:required] = true if detect_required(doc_map)
      meta
    end

    def detect_required(doc_map)
      doc_map[:cfg] && doc_map[:cfg].first[:optional] == false
    end

    def detect_params(doc_map)
      combine_properties(doc_map[:param] || [])
    end

    def detect_subproperties(tagname, docs)
      prop_docs = docs.find_all {|tag| tag[:tagname] == tagname}
      prop_docs.length > 0 ? combine_properties(prop_docs)[0][:properties] : []
    end

    def combine_properties(raw_items)
      # First item can't be namespaced, if it is ignore the rest.
      if raw_items[0] && raw_items[0][:name] =~ /\./
        return [raw_items[0]]
      end

      # build name-index of all items
      index = {}
      raw_items.each {|it| index[it[:name]] = it }

      # If item name has no dots, add it directly to items array.
      # Otherwise look up the parent of item and add it as the
      # property of that parent.
      items = []
      raw_items.each do |it|
        if it[:name] =~ /^(.+)\.([^.]+)$/
          it[:name] = $2
          parent = index[$1]
          if parent
            parent[:properties] = [] unless parent[:properties]
            parent[:properties] << it
          else
            Logger.instance.warn(:subproperty, "Ignoring subproperty #{$1}.#{$2}, no parent found with name '#{$1}'.", @filename, @linenr)
          end
        else
          items << it
        end
      end
      items
    end

    def detect_return(doc_map, default_type="undefined")
      ret = extract(doc_map, :return) || {}
      return {
        :type => ret[:type] || default_type,
        :name => ret[:name] || "return",
        :doc => ret[:doc] || "",
        :properties => doc_map[:return] ? detect_subproperties(:return, doc_map[:return]) : []
      }
    end

    # Combines :doc-s of most tags
    # Ignores tags that have doc comment themselves and subproperty tags
    def detect_doc(docs)
      ignore_tags = [:param, :return, :meta]
      doc_tags = docs.find_all { |tag| !ignore_tags.include?(tag[:tagname]) && !subproperty?(tag) }
      doc_tags.map { |tag| tag[:doc] }.compact.join(" ")
    end

    def subproperty?(tag)
      (tag[:tagname] == :cfg || tag[:tagname] == :property) && tag[:name] =~ /\./
    end

    # Build map of at-tags for quick lookup
    def build_doc_map(docs)
      map = {}
      docs.each do |tag|
        if map[tag[:tagname]]
          map[tag[:tagname]] << tag
        else
          map[tag[:tagname]] = [tag]
        end
      end
      map
    end
  end

end
+74 −312
Original line number Diff line number Diff line
require 'jsduck/logger'
require 'jsduck/meta_tag_registry'
require 'jsduck/class'

module JsDuck

  # Takes data from doc-comment and code that follows it and combines
  # Takes data from comment and code that follows it and combines
  # these to pieces of information into one.  The code comes from
  # JsDuck::Parser and doc-comment from JsDuck::DocParser.
  # JsDuck::Ast and comment from JsDuck::DocAst.
  #
  # The main method merge() produces a hash as a result.
  class Merger
    # Allow passing in filename and line for error reporting
    attr_accessor :filename
    attr_accessor :linenr

    def initialize
      @filename = ""
      @linenr = 0
      @meta_tags = MetaTagRegistry.instance
    end

    # Takes a docset and merges the :comment and :code inside it,
    # producing hash as a result.
    def merge(docset)
      docs = docset[:comment]
      code = docset[:code]

      case docset[:tagname]
      when :class
        create_class(docs, code)
      when :event
        create_event(docs, code)
      when :method
        create_method(docs, code)
      when :cfg
        create_cfg(docs, code)
      when :property
        create_property(docs, code)
      when :css_var
        create_css_var(docs, code)
      when :css_mixin
        create_css_mixin(docs, code)
      end
        merge_class(docs, code)
      when :method, :event, :css_mixin
        merge_like_method(docs, code)
      when :cfg, :property, :css_var
        merge_like_property(docs, code)
      end

    def create_class(docs, code)
      doc_map = build_doc_map(docs)
      return add_shared({
        :tagname => :class,
        :name => detect_name(:class, doc_map, code, :full_name),
        :doc => detect_doc(docs),
        :extends => detect_extends(doc_map, code),
        :mixins => detect_list(:mixins, doc_map, code),
        :alternateClassNames => detect_list(:alternateClassNames, doc_map, code),
        :aliases => detect_aliases(doc_map, code),
        :singleton => detect_singleton(doc_map, code),
        :requires => detect_list(:requires, doc_map, code),
        :uses => detect_list(:uses, doc_map, code),
        # Used by Aggregator to determine if we're dealing with Ext4 code
        :code_type => code[:tagname],
        :members => Class.default_members_hash,
        :statics => Class.default_members_hash,
      }, doc_map)
    end

    def create_method(docs, code)
      doc_map = build_doc_map(docs)
      name = detect_name(:method, doc_map, code)
      return add_shared({
        :tagname => :method,
        :name => name,
        :owner => detect_owner(doc_map),
        :doc => detect_doc(docs),
        :params => detect_params(:method, doc_map, code),
        :return => detect_return(doc_map, name == "constructor" ? "Object" : "undefined"),
      }, doc_map)
    end
    private

    def create_event(docs, code)
      doc_map = build_doc_map(docs)
      return add_shared({
        :tagname => :event,
        :name => detect_name(:event, doc_map, code),
        :owner => detect_owner(doc_map),
        :doc => detect_doc(docs),
        :params => detect_params(:event, doc_map, code),
      }, doc_map)
    end

    def create_cfg(docs, code, owner = nil)
      doc_map = build_doc_map(docs)
      return add_shared({
        :tagname => :cfg,
        :name => detect_name(:cfg, doc_map, code),
        :owner => detect_owner(doc_map) || owner,
        :type => detect_type(:cfg, doc_map, code),
        :doc => detect_doc(docs),
        :default => detect_default(:cfg, doc_map, code),
        :properties => detect_subproperties(docs, :cfg),
        :accessor => !!doc_map[:accessor],
        :evented => !!doc_map[:evented],
      }, doc_map, code)
    end

    def create_property(docs, code)
      doc_map = build_doc_map(docs)
      return add_shared({
        :tagname => :property,
        :name => detect_name(:property, doc_map, code),
        :owner => detect_owner(doc_map),
        :type => detect_type(:property, doc_map, code),
        :doc => detect_doc(docs),
        :default => detect_default(:property, doc_map, code),
        :properties => detect_subproperties(docs, :property),
      }, doc_map)
    end

    def create_css_var(docs, code)
      doc_map = build_doc_map(docs)
      return add_shared({
        :tagname => :css_var,
        :name => detect_name(:css_var, doc_map, code),
        :owner => detect_owner(doc_map),
        :type => detect_type(:css_var, doc_map, code),
        :doc => detect_doc(docs),
      }, doc_map)
    end

    def create_css_mixin(docs, code)
      doc_map = build_doc_map(docs)
      return add_shared({
        :tagname => :css_mixin,
        :name => detect_name(:css_mixin, doc_map, code),
        :owner => detect_owner(doc_map),
        :doc => detect_doc(docs),
        :params => detect_params(:css_mixin, doc_map, code),
      }, doc_map)
    end

    # Detects properties common for each doc-object and adds them
    def add_shared(hash, doc_map, code={})
      hash.merge!({
        :inheritable => !!doc_map[:inheritable],
        :inheritdoc => doc_map[:inheritdoc] ? doc_map[:inheritdoc].first : code[:inheritdoc],
        :meta => detect_meta(doc_map),
    def merge_class(docs, code)
      h = do_merge(docs, code, {
        :mixins => [],
        :alternateClassNames => [],
        :requires => [],
        :uses => [],
        :singleton => false,
      })
      # copy :private also to main hash
      hash[:private] = true if hash[:meta][:private]

      hash[:id] = create_member_id(hash)
      # Ignore extending of the Object class
      h[:extends] = nil if h[:extends] == "Object"

      return hash
    end
      h[:aliases] = build_aliases_hash(h[:aliases] || [])
      # Used by Aggregator to determine if we're dealing with Ext4 code
      h[:code_type] = code[:tagname]
      h[:members] = Class.default_members_hash
      h[:statics] = Class.default_members_hash

    def create_member_id(m)
      # Sanitize $ in member names with something safer
      name = m[:name].gsub(/\$/, 'S-')
      "#{m[:meta][:static] ? 'static-' : ''}#{m[:tagname]}-#{name}"
      h
    end

    def detect_name(tagname, doc_map, code, name_type = :last_name)
      main_tag = doc_map[tagname] ? doc_map[tagname].first : {}
      if main_tag[:name]
        main_tag[:name]
      elsif doc_map[:constructor]
        "constructor"
      elsif code[:name]
        extract_name(code[:name], name_type)
      else
        ""
      end
    def merge_like_method(docs, code)
      h = do_merge(docs, code)
      h[:params] = merge_params(docs, code)
      h
    end

    def extract_name(name, type)
      type == :last_name ? name.split(/\./).last : name
    end
    def merge_like_property(docs, code)
      h = do_merge(docs, code)

    def detect_owner(doc_map)
      if doc_map[:member]
        doc_map[:member].first[:member]
      else
        nil
      end
      h[:type] = merge_if_code_matches(:type, docs, code)
      if h[:type] == nil
        h[:type] = code[:tagname] == :method ? "Function" : "Object"
      end

    def detect_type(tagname, doc_map, code)
      main_tag = doc_map[tagname] ? doc_map[tagname].first : {}
      if main_tag[:type]
        return main_tag[:type]
      elsif doc_map[:type]
        return doc_map[:type].first[:type]
      elsif code_matches_doc?(tagname, doc_map, code)
        if code[:tagname] == :method
          return "Function"
        elsif code[:tagname] == :property && code[:type]
          return code[:type]
        end
      end
      return "Object"
      h[:default] = merge_if_code_matches(:default, docs, code)
      h
    end

    def detect_extends(doc_map, code)
      if doc_map[:extends]
        cls = doc_map[:extends].first[:extends]
      elsif code[:tagname] == :class && code[:extends]
        cls = code[:extends]
      else
        cls = nil
      end
      # Ignore extending of the Object class
      cls == "Object" ? nil : cls
    end
    # --- helpers ---

    def detect_default(tagname, doc_map, code)
      main_tag = doc_map[tagname] ? doc_map[tagname].first : {}
      if main_tag[:default]
        main_tag[:default]
      elsif code_matches_doc?(tagname, doc_map, code) && code[:tagname] == :property && code[:default]
        code[:default]
      end
    def do_merge(docs, code, defaults={})
      h = {}
      docs.each_pair do |key, value|
        h[key] = docs[key] || code[key] || defaults[key]
      end

    # True if the name detected from code matches with explicitly documented name.
    # Also true when no explicit name documented.
    def code_matches_doc?(tagname, doc_map, code)
      explicit_name = detect_name(tagname, doc_map, {})
      implicit_name = detect_name(tagname, {}, code)
      return explicit_name == "" || explicit_name == implicit_name
    end
      h[:name] = merge_name(docs, code)
      h[:id] = create_member_id(h)

    # for detecting mixins and alternateClassNames
    def detect_list(type, doc_map, code)
      if doc_map[type]
        doc_map[type].map {|d| d[type] }.flatten
      elsif code[:tagname] == :class && code[type]
        code[type]
      else
        []
      end
      h
    end

    def detect_aliases(doc_map, code)
      if doc_map[:alias]
        build_aliases_hash(doc_map[:alias].map {|tag| tag[:name] })
      elsif code[:aliases]
        build_aliases_hash(code[:aliases])
      else
        {}
      end
    def create_member_id(m)
      # Sanitize $ in member names with something safer
      name = m[:name].gsub(/\$/, 'S-')
      "#{m[:meta][:static] ? 'static-' : ''}#{m[:tagname]}-#{name}"
    end

    # Given array of full alias names like "foo.bar", "foo.baz"
@@ -263,44 +102,19 @@ module JsDuck
      hash
    end

    def detect_meta(doc_map)
      meta = {}
      (doc_map[:meta] || []).map do |tag|
        meta[tag[:name]] = [] unless meta[tag[:name]]
        meta[tag[:name]] << tag[:doc]
      end

      meta.each_pair do |key, value|
        tag = @meta_tags[key]
        meta[key] = tag.to_value(tag.boolean ? true : value)
      end

      meta[:required] = true if detect_required(doc_map)
      meta
    end

    def detect_singleton(doc_map, code)
      !!(doc_map[:singleton] || code[:tagname] == :class && code[:singleton])
    end

    def detect_required(doc_map)
      doc_map[:cfg] && doc_map[:cfg].first[:optional] == false
    end

    def detect_params(tagname, doc_map, code)
      implicit = code_matches_doc?(tagname, doc_map, code) ? detect_implicit_params(code) : []
      explicit = detect_explicit_params(doc_map)
    def merge_params(docs, code)
      explicit = docs[:params] || []
      implicit = code_matches_doc?(docs, code) ? (code[:params] || []) : []
      # Override implicit parameters with explicit ones
      # But if explicit ones exist, don't append the implicit ones.
      params = []
      (explicit.length > 0 ? explicit.length : implicit.length).times do |i|
        im = implicit[i] || {}
        ex = explicit[i] || {}
        doc = ex[:doc] || im[:doc] || ""
        params << {
          :type => ex[:type] || im[:type] || "Object",
          :name => ex[:name] || im[:name] || "",
          :doc => doc,
          :doc => ex[:doc] || im[:doc] || "",
          :optional => ex[:optional] || false,
          :default => ex[:default],
          :properties => ex[:properties] || [],
@@ -309,88 +123,36 @@ module JsDuck
      params
    end

    def detect_implicit_params(code)
      if code[:tagname] == :method
        code[:params]
      else
        []
      end
    end

    def detect_explicit_params(doc_map)
      combine_properties(doc_map[:param] || [])
    end

    def detect_subproperties(docs, tagname)
      prop_docs = docs.find_all {|tag| tag[:tagname] == tagname}
      prop_docs.length > 0 ? combine_properties(prop_docs)[0][:properties] : []
    end

    def combine_properties(raw_items)
      # First item can't be namespaced, if it is ignore the rest.
      if raw_items[0] && raw_items[0][:name] =~ /\./
        return [raw_items[0]]
      end

      # build name-index of all items
      index = {}
      raw_items.each {|it| index[it[:name]] = it }

      # If item name has no dots, add it directly to items array.
      # Otherwise look up the parent of item and add it as the
      # property of that parent.
      items = []
      raw_items.each do |it|
        if it[:name] =~ /^(.+)\.([^.]+)$/
          it[:name] = $2
          parent = index[$1]
          if parent
            parent[:properties] = [] unless parent[:properties]
            parent[:properties] << it
    def merge_name(docs, code)
      if docs[:name]
        docs[:name]
      elsif code[:name]
        if docs[:tagname] == :class
          code[:name]
        else
            Logger.instance.warn(:subproperty, "Ignoring subproperty #{$1}.#{$2}, no parent found with name '#{$1}'.", @filename, @linenr)
          code[:name].split(/\./).last
        end
      else
          items << it
        end
        ""
      end
      items
    end

    def detect_return(doc_map, default_type="undefined")
      ret = doc_map[:return] ? doc_map[:return].first : {}
      return {
        :type => ret[:type] || default_type,
        :name => ret[:name] || "return",
        :doc => ret[:doc] || "",
        :properties => doc_map[:return] ? detect_subproperties(doc_map[:return], :return) : []
      }
    def merge_if_code_matches(key, docs, code, default=nil)
      if docs[key]
        docs[key]
      elsif code[key] && code_matches_doc?(docs, code)
        code[key]
      else
        default
      end

    # Combines :doc-s of most tags
    # Ignores tags that have doc comment themselves and subproperty tags
    def detect_doc(docs)
      ignore_tags = [:param, :return, :meta]
      doc_tags = docs.find_all { |tag| !ignore_tags.include?(tag[:tagname]) && !subproperty?(tag) }
      doc_tags.map { |tag| tag[:doc] }.compact.join(" ")
    end

    def subproperty?(tag)
      (tag[:tagname] == :cfg || tag[:tagname] == :property) && tag[:name] =~ /\./
    # True if the name detected from code matches with explicitly documented name.
    # Also true when no explicit name documented.
    def code_matches_doc?(docs, code)
      return docs[:name] == nil || docs[:name] == code[:name]
    end

    # Build map of at-tags for quick lookup
    def build_doc_map(docs)
      map = {}
      docs.each do |tag|
        if map[tag[:tagname]]
          map[tag[:tagname]] << tag
        else
          map[tag[:tagname]] = [tag]
        end
      end
      map
    end
  end

end
+7 −3
Original line number Diff line number Diff line
@@ -5,6 +5,7 @@ require 'jsduck/doc_parser'
require 'jsduck/merger'
require 'jsduck/ast'
require 'jsduck/doc_type'
require 'jsduck/doc_ast'
require 'jsduck/class_doc_expander'

module JsDuck
@@ -20,12 +21,13 @@ module JsDuck
      @doc_type = DocType.new
      @doc_parser = DocParser.new
      @class_doc_expander = ClassDocExpander.new
      @doc_ast = DocAst.new
      @merger = Merger.new
    end

    # Parses file into final docset that can be fed into Aggregator
    def parse(contents, filename="", options={})
      @merger.filename = filename
      @doc_ast.filename = filename

      parse_js_or_css(contents, filename, options)
        .map {|docset| expand(docset) }
@@ -57,9 +59,11 @@ module JsDuck
      end
    end

    # Merges comment and code parst of docset
    # Merges comment and code parts of docset
    def merge(docset)
      @merger.linenr = docset[:linenr]
      @doc_ast.linenr = docset[:linenr]
      docset[:comment] = @doc_ast.detect(docset[:tagname], docset[:comment])

      @merger.merge(docset)
    end
  end
+14 −2
Original line number Diff line number Diff line
@@ -10,7 +10,13 @@ describe JsDuck::Merger do
    before do
      @doc = merge({
        :tagname => :cfg,
        :comment => [{:tagname => :cfg, :type => "String", :doc => "My Config"}],
        :comment => {
          :tagname => :cfg,
          :name => nil,
          :meta => {},
          :type => "String",
          :doc => "My Config"
        },
        :code => {
          :tagname => :property,
          :name => "option",
@@ -36,7 +42,13 @@ describe JsDuck::Merger do
    before do
      @doc = merge({
        :tagname => :property,
        :comment => [{:tagname => :default, :doc => "Hello world"}],
        :comment => {
          :tagname => :property,
          :name => nil,
          :meta => {},
          :type => nil,
          :doc => "Hello world"
        },
        :code => {
          :tagname => :property,
          :name => "some.prop",