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

Complete rewrite of Parser.

Now doing a proper recursive-descent parsing of the code following the
doc-comment.  After that, having the parsed structure it's a lot easier
to inspect it and add info to the doc-comment data as needed.

Also implemented auto-detection of superclass when Ext.extend() is
used.
parent 6fc9b777
Loading
Loading
Loading
Loading
+135 −52
Original line number Diff line number Diff line
@@ -117,6 +117,12 @@ module JsDuck
      end
    end

    # Sets default name for superclass
    def set_default_extends(*name_chain)
      @tags[:class] = {:doc => ""} unless @tags[:class]
      @tags[:class][:extends] = name_chain.join(".") unless @tags[:class][:extends]
    end

    # sets default names and possibly other properties of params
    def set_default_params(params)
      if @tags[:param] then
@@ -286,36 +292,20 @@ module JsDuck

    def parse
      while !@lex.empty? do
        if @lex.look(:doc_comment) then
          doc = @lex.next
          if @lex.look("function", :ident) then
            @lex.next
            # function name(){
            doc.set_default_name(@lex.next)
            doc.set_default_params(parse_params)
          elsif @lex.look("var", :ident, "=", "function") then
            @lex.next
            # var name = function(){
            doc.set_default_name(@lex.next)
            @lex.next # =
            doc.set_default_params(parse_anonymous_function_params)
          elsif @lex.look(:ident, "=", "function") ||
              @lex.look(:ident, ":", "function") ||
              @lex.look(:string, ":", "function") then
            # name: function(){
            doc.set_default_name(@lex.next)
            @lex.next # : or =
            doc.set_default_params(parse_anonymous_function_params)
          elsif @lex.look(:ident, ".") then
            # some.long.prototype.chain = function() {
            name_chain = [@lex.next]
            while @lex.look(".", :ident) do
              @lex.next
              name_chain << @lex.next
              if @lex.look("=", "function") then
                doc.set_default_name(*name_chain)
                @lex.next # =
                doc.set_default_params(parse_anonymous_function_params)
        if look(:doc_comment) then
          doc = match(:doc_comment)
          block = code_block
          if block[:type] == :function then
            doc.set_default_name(*block[:name]) if block[:name]
            doc.set_default_params(block[:params])
          elsif block[:type] == :assignment then
            doc.set_default_name(*block[:left])
            if block[:right] then
              right = block[:right]
              if right[:type] == :function then
                doc.set_default_params(right[:params])
              elsif right[:type] == :ext_extend then
                doc.set_default_extends(right[:extend])
              end
            end
          end
@@ -327,35 +317,128 @@ module JsDuck
      @docs
    end

    # Parses function parameters out of:  function blah(x, y, z) {...
    def parse_anonymous_function_params
      if @lex.look("function") then
        @lex.next # function
        @lex.next if @lex.look(:ident) # optional anonymous function name
        parse_params
    # The following is a recursive-descent parser for JavaScript that
    # can possibly follow a doc-comment

    # <code-block> := <function> | <var-declaration> | <assignment> | <property-literal>
    def code_block
      if look("function") then
        function
      elsif look("var") then
        var_declaration
      elsif look(:ident, ":") || look(:string, ":") then
        property_literal
      elsif look(:ident) then
        maybe_assignment
      else
        []
        {:type => :nop}
      end
    end

    # Parses parameters out of:  (x, y, z, ...)
    def parse_params
      params = []
      if @lex.look("(") then
        @lex.next
        while @lex.look(:ident) do
          params << {:name => @lex.next}
          if @lex.look(",") then
            @lex.next
          else
            break
    # <function> := "function" [ <ident> ] <function-parameters> <function-body>
    def function
      match("function")
      return {
        :type => :function,
        :name => look(:ident) ? match(:ident) : nil,
        :params => function_parameters,
        :body => function_body,
      }
    end

    # <function-parameters> := "(" [ <ident> [ "," <ident> ]* ] ")"
    def function_parameters
      match("(")
      params = look(:ident) ? [{:name => match(:ident)}] : []
      while look(",", :ident) do
        params << {:name => match(",", :ident)}
      end
      match(")")
      return params
    end
      params

    # <function-body> := "{" ...
    def function_body
      match("{")
    end

    # <var-declaration> := "var" <assignment>
    def var_declaration
      match("var")
      maybe_assignment
    end

    # <maybe-assignment> := <ident-chain> [ "=" <expression> ]
    def maybe_assignment
      left = ident_chain
      if look("=") then
        match("=")
        right = expression
      end
      return {
        :type => :assignment,
        :left => left,
        :right => right,
      }
    end

    # <ident-chain> := <ident> [ "." <ident> ]*
    def ident_chain
      chain = [match(:ident)]
      while look(".", :ident) do
        chain << match(".", :ident)
      end
      return chain
    end

    # <expression> := <function> | <ext-extend> | <literal>
    # <literal> := <string> | <boolean> | <number> | <regex>
    def expression
      if look("function") then
        function
      elsif look("Ext", ".", "extend") then
        ext_extend
      end
    end

    # <ext-extend> := "Ext" "." "extend" "(" <ident-chain> "," ...
    def ext_extend
      match("Ext", ".", "extend", "(")
      return {
        :type => :ext_extend,
        :extend => ident_chain,
      }
    end

    # <property-literal> := ( <ident> | <string> ) ":" <expression>
    def property_literal
      left = look(:ident) ? match(:ident) : match(:string)
      match(":")
      right = expression
      return {
        :type => :assignment,
        :left => left,
        :right => right,
      }
    end

    # Matches all arguments, returns the value of last match
    # When the whole sequence doesn't match, throws exception
    def match(*args)
      if look(*args) then
        last = nil
        args.length.times { last = @lex.next }
        last
      else
        throw "Expected: " + args.join(", ")
      end
    end

    def look(*args)
      @lex.look(*args)
    end

  end

  def JsDuck.parse(input)
    Parser.new(input).parse
+26 −0
Original line number Diff line number Diff line
@@ -249,5 +249,31 @@ some.namespace.ClassName = function(){}
    assert_equal("My class", docs[0][:class][:doc])
  end

  def test_implicit_extends
    docs = JsDuck.parse("
/**
 * My class
 */
MyClass = Ext.extend(Ext.util.Observable, {
});
")
    assert_equal("MyClass", docs[0][:class][:name])
    assert_equal("Ext.util.Observable", docs[0][:class][:extends])
    assert_equal("My class", docs[0][:class][:doc])
  end

  def test_implicit_extends_with_var
    docs = JsDuck.parse("
/**
 * My class
 */
var MyClass = Ext.extend(Ext.util.Observable, {
});
")
    assert_equal("MyClass", docs[0][:class][:name])
    assert_equal("Ext.util.Observable", docs[0][:class][:extends])
    assert_equal("My class", docs[0][:class][:doc])
  end

end