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

A proper parser for at-tags inside doc-comments.

parent 462e1e0a
Loading
Loading
Loading
Loading
+82 −23
Original line number Diff line number Diff line
@@ -94,16 +94,22 @@ module JsDuck


  class DocComment
    attr_accessor :function, :doc
    
    def initialize(input)
      @function = ""
      @doc = ""
      @params = []
      @return = "void"
      @current_tag = {:doc => ""}
      @tags = {:default => @current_tag}
      parse(purify(input))
    end

    # sets the name and properties of the default at-tag
    def set_default(tagname, attrs={})
      @tags[tagname] = attrs
      @tags[tagname][:doc] = @tags[:default][:doc]
    end

    def [](tagname)
      @tags[tagname]
    end

    # Extracts content inside /** ... */
    def purify(input)
      result = []
@@ -121,25 +127,78 @@ module JsDuck
    end

    def parse(input)
      doc = []
      input.each_line do |line|
        line.chomp!
        if line =~ /\A@param\b/ then
          @params << line
        elsif line =~ /\A@return\b/ then
          @return = line
      @input = StringScanner.new(input)
      while !@input.eos? do
        if look(/@return\b/) then
          at_return
        elsif look(/@param\b/) then
          at_param
        elsif look(/@/) then
          @current_tag[:doc] += @input.scan(/@/)
        elsif look(/[^@]/) then
          @current_tag[:doc] += @input.scan(/[^@]+/)
        end
      end
    end

    # matches @return {type} ...
    def at_return
      match(/@return/)
      @current_tag = @tags[:return] = {:doc => ""}
      skip_white
      if look(/\{/) then
        @current_tag[:type] = typedef
      end
      skip_white
    end

    # matches @param {type} variable ...
    def at_param
      match(/@param/)
      @current_tag = {:doc => ""}
      if @tags[:param] then
        @tags[:param] << @current_tag
      else
          doc << line
        @tags[:param] = [@current_tag]
      end
      skip_white
      if look(/\{/) then
        @current_tag[:type] = typedef
      end
      @doc = doc.join("\n")
      skip_white
      if look(/\w/) then
        @current_tag[:name] = ident
      end
      skip_white
    end

    # matches {...} and returns text inside brackets
    def typedef
      match(/\{/)
      name = @input.scan(/[^}]+/)
      match(/\}/)
      return name
    end

    # matches identifier and returns its name
    def ident
      @input.scan(/\w+/)
    end

    def look(re)
      @input.check(re)
    end
    
    def match(re)
      @input.scan(re)
    end

    def skip_white
      @input.scan(/\s+/)
    end

    def print
      puts "function: " + @function
      puts "doc: " + @doc
      puts "params: " + @params.join("\n")
      puts "return: " + @return
      pp @tags
    end
  end

@@ -153,16 +212,16 @@ module JsDuck
        if lex.look("function", :keyword) then
          lex.next
          # function name(){
          doc.function = lex.next
          doc.set_default(:function, {:name => lex.next})
        elsif lex.look("var", :keyword, "=", "function") then
          lex.next
          # var name = function(){
          doc.function = lex.next
          doc.set_default(:function, {:name => lex.next})
        elsif lex.look(:keyword, "=", "function") ||
            lex.look(:keyword, ":", "function") ||
            lex.look(:string, ":", "function") then
          # name: function(){
          doc.function = lex.next
          doc.set_default(:function, {:name => lex.next})
        end
        docs << doc
      else
+39 −7
Original line number Diff line number Diff line
@@ -15,6 +15,38 @@ class TestJsDuck < Test::Unit::TestCase
    assert_equal([], JsDuck.parse("/* ") )
  end

  def test_return
    docs = JsDuck.parse("
/**
 * Some function
 * @return {String} some value
 * on several
 * lines
 */
")
    assert_equal("String", docs[0][:return][:type])
    assert_equal("some value\non several\nlines", docs[0][:return][:doc])
  end

  def test_param
    docs = JsDuck.parse("
/**
 * Some function
 * @param {Number} x value 1
 * @param {Float} y value 2
 */
")
    param1 = docs[0][:param][0]
    assert_equal("Number", param1[:type])
    assert_equal("x", param1[:name])
    assert_equal("value 1\n", param1[:doc])

    param2 = docs[0][:param][1]
    assert_equal("Float", param2[:type])
    assert_equal("y", param2[:name])
    assert_equal("value 2", param2[:doc])
  end

  def test_function
    docs = JsDuck.parse("
/**
@@ -23,8 +55,8 @@ class TestJsDuck < Test::Unit::TestCase
function foo() {
}
")
    assert_equal("Some function", docs[0].doc)
    assert_equal("foo", docs[0].function)
    assert_equal("Some function", docs[0][:function][:doc])
    assert_equal("foo", docs[0][:function][:name])
  end

  def test_function_with_var
@@ -34,7 +66,7 @@ function foo() {
var foo = function() {
}
")
    assert_equal("foo", docs[0].function)
    assert_equal("foo", docs[0][:function][:name])
  end

  def test_function_without_var
@@ -44,7 +76,7 @@ var foo = function() {
foo = function() {
}
")
    assert_equal("foo", docs[0].function)
    assert_equal("foo", docs[0][:function][:name])
  end

  def test_function_in_object_literal
@@ -54,7 +86,7 @@ foo = function() {
foo: function() {
}
")
    assert_equal("foo", docs[0].function)
    assert_equal("foo", docs[0][:function][:name])
  end

  def test_function_in_object_literal_string
@@ -64,7 +96,7 @@ foo: function() {
'foo': function() {
}
")
    assert_equal("foo", docs[0].function)
    assert_equal("foo", docs[0][:function][:name])
  end

  def test_function_private
@@ -73,7 +105,7 @@ foo: function() {
function foo() {
}
")
    assert_equal(0, docs.length)
    assert_equal([], docs)
  end
end