Commit 391c9bf6 authored by Rene Saarsoo's avatar Rene Saarsoo
Browse files

Better :requires detection and AST Evaluator.

Moved AST node evaluation to separate class which functions just like
the Serializer class, but instead of returning string attempts to turn
the AST into a ruby object.
parent 182c038d
Loading
Loading
Loading
Loading
+29 −17
Original line number Diff line number Diff line
require "jsduck/serializer"
require "jsduck/evaluator"

module JsDuck

@@ -6,6 +7,7 @@ module JsDuck
  class Ast
    def initialize
      @serializer = JsDuck::Serializer.new
      @evaluator = JsDuck::Evaluator.new
    end

    # Given parsed code, returns the tagname for documentation item.
@@ -21,7 +23,7 @@ module JsDuck

      # Ext.define("Class", {})
      if exp && ext_define?(exp)
        make_class(to_s_value(exp["arguments"][0]), exp)
        make_class(to_value(exp["arguments"][0]), exp)

      # foo = Ext.extend("Parent", {})
      elsif exp && assignment?(exp) && ext_extend?(exp["right"])
@@ -57,7 +59,7 @@ module JsDuck

      # foo: function() {}
      elsif property?(ast) && function?(ast["value"])
        make_method(to_s_value(ast["key"]), ast["value"])
        make_method(to_value(ast["key"]), ast["value"])

      else
        {:type => :property}
@@ -121,19 +123,35 @@ module JsDuck
        elsif ext_define?(ast)
          cfg = object_expression_to_hash(ast["arguments"][1])

          cls[:extends] = cfg["extend"] ? to_s_value(cfg["extend"]) : nil
          cls[:requires] = cfg["requires"] ? to_s_value(cfg["requires"]) : nil
          cls[:extends] = make_extends(cfg["extend"])
          cls[:requires] = make_requires(cfg["requires"])
        end
      end

      return cls
    end

    def make_extends(cfg_value)
      return nil unless cfg_value

      parent = to_value(cfg_value)

      return parent.is_a?(String) ? parent : nil
    end

    def make_requires(cfg_value)
      return [] unless cfg_value

      classes = Array(to_value(cfg_value))

      return classes.all? {|c| c.is_a? String } ? classes : []
    end

    def object_expression_to_hash(ast)
      h = {}
      if ast && ast["type"] == "ObjectExpression"
        ast["properties"].each do |p|
          h[to_s_value(p["key"])] = p["value"]
          h[to_value(p["key"])] = p["value"]
        end
      end
      return h
@@ -152,18 +170,12 @@ module JsDuck
      @serializer.to_s(ast)
    end

    # Does simple serialization where strings serialize into their
    # values (without quotes). Should be called only with Identifier
    # and Literal nodes, but if something else is passed in, we fall
    # back to the real serializer (just in case).
    def to_s_value(ast)
      case ast["type"]
      when "Identifier"
        ast["name"]
      when "Literal"
        ast["value"].to_s
      else
        @serializer.to_s(ast)
    # Converts AST node into a value.
    def to_value(ast)
      begin
        @evaluator.to_value(ast)
      rescue
        nil
      end
    end
  end
+32 −0
Original line number Diff line number Diff line
module JsDuck

  # Evaluates Esprima AST node into Ruby object
  class Evaluator

    # Converts AST node into a value.
    #
    # - String literals become Ruby strings
    # - Number literals become Ruby number
    # - Array expressions become Ruby arrays
    # - etc
    #
    # For anything it doesn't know how to evaluate (like a function
    # expression) it throws exception.
    #
    def to_value(ast)
      case ast["type"]
      when "ArrayExpression"
        ast["elements"].map {|e| to_value(e) }
      when "Identifier"
        ast["name"]
      when "Literal"
        ast["value"]
      else
        throw "Unknown node type: " + ast["type"]
      end
    end

  end

end
+47 −1
Original line number Diff line number Diff line
@@ -88,17 +88,63 @@ describe "JsDuck::Ast detects class with" do
        });
      EOS
    end

    it "Ext.define() with extend as number" do
      detect(<<-EOS)[:extends].should == nil
        /** */
        Ext.define('MyClass', {
            extend: 5
        });
      EOS
    end
  end

  describe "requries in" do
    it "Ext.define() with requires as string" do
      detect(<<-EOS)[:requires].should == "Other.Class"
      detect(<<-EOS)[:requires].should == ["Other.Class"]
        /** */
        Ext.define('MyClass', {
            requires: "Other.Class"
        });
      EOS
    end

    it "Ext.define() with requires as array of strings" do
      detect(<<-EOS)[:requires].should == ["Some.Class", "Other.Class"]
        /** */
        Ext.define('MyClass', {
            requires: ["Some.Class", "Other.Class"]
        });
      EOS
    end
  end

  describe "no requries in" do
    it "Ext.define() without requires" do
      detect(<<-EOS)[:requires].should == []
        /** */
        Ext.define('MyClass', {
        });
      EOS
    end

    it "Ext.define() with requires as array of functions and strings" do
      detect(<<-EOS)[:requires].should == []
        /** */
        Ext.define('MyClass', {
            requires: [function(){}, "Foo"]
        });
      EOS
    end

    it "Ext.define() with requires as nested array" do
      detect(<<-EOS)[:requires].should == []
        /** */
        Ext.define('MyClass', {
            requires: ["Foo", ["Bar"]]
        });
      EOS
    end
  end

end