Commit 791bdbbc authored by Rene Saarsoo's avatar Rene Saarsoo
Browse files

Move JavaScript parsing classes to js/ subdir.

To clean up the lib/jsduck/ dir even more.
parent f6d3c1a4
Loading
Loading
Loading
Loading

lib/jsduck/ast.rb

deleted100644 → 0
+0 −359
Original line number Diff line number Diff line
require "jsduck/function_ast"
require "jsduck/ast_node"
require "jsduck/tag_registry"

module JsDuck

  # Analyzes the AST produced by EsprimaParser.
  class Ast
    # Should be initialized with EsprimaParser#parse result.
    def initialize(docs = [])
      @docs = docs
    end

    # Performs the detection of code in all docsets.
    #
    # @returns the processed array of docsets. (But it does it
    # destructively by modifying the passed-in docsets.)
    #
    def detect_all!
      # First deal only with doc-comments
      doc_comments = @docs.find_all {|d| d[:type] == :doc_comment }

      # Detect code in each docset.  Sometimes a docset has already
      # been detected as part of detecting some previous docset (like
      # Class detecting all of its configs) - in such case, skip.
      doc_comments.each do |docset|
        code = docset[:code]
        docset[:code] = detect(code) unless code && code[:tagname]
      end

      # Return all doc-comments + other comments for which related
      # code was detected.
      @docs.find_all {|d| d[:type] == :doc_comment || d[:code] && d[:code][:tagname] }
    end

    # Given Esprima-produced syntax tree, detects documentation data.
    #
    # This method is exposed for testing purposes only, JSDuck itself
    # only calls the above #detect_all method.
    #
    # @param ast :code from Result of EsprimaParser
    # @returns Hash consisting of the detected :tagname, :name, and
    # other properties relative to the tag.  Like so:
    #
    #     { :tagname => :method, :name => "foo", ... }
    #
    def detect(node)
      ast = AstNode.create(node)

      exp = ast.expression_statement? ? ast["expression"] : nil
      var = ast.variable_declaration? ? ast["declarations"][0] : nil

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

      # Ext.override(Class, {})
      elsif exp && exp.ext_override?
        make_class("", exp)

      # foo = Ext.extend(Parent, {})
      elsif exp && exp.assignment_expression? && exp["right"].ext_extend?
        make_class(exp["left"].to_s, exp["right"])

      # Foo = ...
      elsif exp && exp.assignment_expression? && class_name?(exp["left"].to_s)
        make_class(exp["left"].to_s, exp["right"])

      # var foo = Ext.extend(Parent, {})
      elsif var && var["init"].ext_extend?
        make_class(var["id"].to_s, var["init"])

      # var Foo = ...
      elsif var && class_name?(var["id"].to_s)
        make_class(var["id"].to_s, var["right"])

      # function Foo() {}
      elsif ast.function? && class_name?(ast["id"].to_s)
        make_class(ast["id"].to_s)

      # { ... }
      elsif ast.object_expression?
        make_class("", ast)

      # function foo() {}
      elsif ast.function?
        make_method(ast["id"].to_s, ast)

      # foo = function() {}
      elsif exp && exp.assignment_expression? && exp["right"].function?
        make_method(exp["left"].to_s, exp["right"])

      # var foo = function() {}
      elsif var && var["init"] && var["init"].function?
        make_method(var["id"].to_s, var["init"])

      # (function() {})
      elsif exp && exp.function?
        make_method(exp["id"].to_s || "", exp)

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

      # this.fireEvent("foo", ...)
      elsif exp && exp.fire_event?
        make_event(exp["arguments"][0].to_value)

      # foo = ...
      elsif exp && exp.assignment_expression?
        make_property(exp["left"].to_s, exp["right"])

      # var foo = ...
      elsif var
        make_property(var["id"].to_s, var["init"])

      # foo: ...
      elsif ast.property?
        make_property(ast["key"].key_value, ast["value"])

      # foo;
      elsif exp && exp.identifier?
        make_property(exp.to_s)

      # "foo"  (inside some expression)
      elsif ast.string?
        make_property(ast.to_value)

      # "foo";  (as a statement of it's own)
      elsif exp && exp.string?
        make_property(exp.to_value)

      else
        make_property()
      end
    end

    private

    # Class name begins with upcase char
    def class_name?(name)
      return name.split(/\./).last =~ /\A[A-Z]/
    end

    def make_class(name, ast=nil)
      cls = {
        :tagname => :class,
        :name => name,
      }

      # apply information from Ext.extend, Ext.define, or {}
      if ast
        if ast.ext_define?
          detect_ext_define(cls, ast)
        elsif ast.ext_extend?
          detect_ext_something(:extends, cls, ast)
        elsif ast.ext_override?
          detect_ext_something(:override, cls, ast)
        elsif ast.object_expression?
          detect_class_members_from_object(cls, ast)
        elsif ast.array_expression?
          detect_class_members_from_array(cls, ast)
        end
      end

      return cls
    end

    # Detection of Ext.extend() or Ext.override().
    # The type parameter must be correspondingly either :extend or :override.
    def detect_ext_something(type, cls, ast)
      args = ast["arguments"]
      cls[type] = args[0].to_s
      if args.length == 2 && args[1].object_expression?
        detect_class_members_from_object(cls, args[1])
      end
    end

    # Inspects Ext.define() and copies detected properties over to the
    # given cls Hash
    def detect_ext_define(cls, ast)
      # defaults
      cls.merge!(TagRegistry.ext_define_defaults)
      cls[:members] = []
      cls[:code_type] = :ext_define

      ast["arguments"][1].each_property do |key, value, pair|
        if tag = TagRegistry.get_by_ext_define_pattern(key)
          tag.parse_ext_define(cls, value)
        else
          case key
          when "config"
            cls[:members] += make_configs(value, {:accessor => true})
          when "cachedConfig"
            cls[:members] += make_configs(value, {:accessor => true})
          when "eventedConfig"
            cls[:members] += make_configs(value, {:accessor => true, :evented => true})
          when "statics"
            cls[:members] += make_statics(value)
          when "inheritableStatics"
            cls[:members] += make_statics(value, {:inheritable => true})
          else
            detect_method_or_property(cls, key, value, pair)
          end
        end
      end
    end

    # Detects class members from object literal
    def detect_class_members_from_object(cls, ast)
      cls[:members] = []
      ast.each_property do |key, value, pair|
        detect_method_or_property(cls, key, value, pair)
      end
    end

    # Detects class members from array literal
    def detect_class_members_from_array(cls, ast)
      cls[:members] = []

      # This will most likely be an @enum class, in which case the
      # enum will be for documentation purposes only.
      cls[:enum] = {:doc_only => true}

      ast["elements"].each do |el|
        detect_method_or_property(cls, el.key_value, el, el)
      end
    end

    # Detects item in object literal either as method or property
    def detect_method_or_property(cls, key, value, pair)
      if value.function?
        m = make_method(key, value)
        cls[:members] << m if apply_autodetected(m, pair)
      else
        p = make_property(key, value)
        cls[:members] << p if apply_autodetected(p, pair)
      end
    end

    def make_configs(ast, defaults={})
      configs = []

      ast.each_property do |name, value, pair|
        cfg = make_property(name, value, :cfg)
        cfg.merge!(defaults)
        configs << cfg if apply_autodetected(cfg, pair)
      end

      configs
    end

    def make_statics(ast, defaults={})
      statics = []

      ast.each_property do |name, value, pair|
        if value.function?
          s = make_method(name, value)
        else
          s = make_property(name, value)
        end

        s[:static] = true
        s.merge!(defaults)

        statics << s if apply_autodetected(s, pair, defaults[:inheritable])
      end

      statics
    end

    # Sets auto-detection related properties :autodetected and
    # :inheritdoc on the given member Hash.
    #
    # When member has a comment, adds code to the related docset and
    # returns false.
    #
    # Otherwise detects the line number of member and returns true.
    def apply_autodetected(m, ast, inheritable=true)
      docset = find_docset(ast.raw)

      if !docset || docset[:type] != :doc_comment
        if inheritable
          m[:inheritdoc] = {}
        else
          m[:private] = true
        end
        m[:autodetected] = true
      end

      if docset
        docset[:code] = m
        return false
      else
        m[:linenr] = ast.linenr
        return true
      end
    end

    # Looks up docset associated with given AST node.
    # A dead-stupid and -slow implementation, but works.
    #
    # The comparison needs to be done between raw AST nodes - multiple
    # AstNode instances can be created to wrap a single raw AST node,
    # and they will then not be equal.
    def find_docset(raw_ast)
      @docs.find do |docset|
        docset[:code] == raw_ast
      end
    end

    def make_method(name, ast)
      return {
        :tagname => :method,
        :name => name,
        :params => make_params(ast),
        :chainable => chainable?(ast) && name != "constructor",
      }
    end

    def make_params(ast)
      if ast.function? && !ast.ext_empty_fn?
        ast["params"].map {|p| {:name => p.to_s} }
      else
        []
      end
    end

    def chainable?(ast)
      if ast.function? && !ast.ext_empty_fn?
        FunctionAst.return_types(ast.raw) == [:this]
      else
        false
      end
    end

    def make_event(name)
      return {
        :tagname => :event,
        :name => name,
      }
    end

    def make_property(name=nil, ast=nil, tagname=:property)
      return {
        :tagname => tagname,
        :name => name,
        :type => ast && ast.value_type,
        :default => ast && make_default(ast),
      }
    end

    def make_default(ast)
      ast.to_value != nil ? ast.to_s : nil
    end

  end

end

lib/jsduck/ast_node.rb

deleted100644 → 0
+0 −192
Original line number Diff line number Diff line
require "jsduck/serializer"
require "jsduck/evaluator"
require "jsduck/ext_patterns"
require "jsduck/ast_node_array"

module JsDuck

  # Wraps around AST node returned from Esprima, providing methods for
  # investigating it.
  class AstNode
    # Factor method that creates either AstNode or AstNodeArray.
    def self.create(node)
      if node.is_a? Array
        AstNodeArray.new(node)
      else
        AstNode.new(node)
      end
    end

    # Initialized with a AST Hash from Esprima.
    def initialize(node)
      @node = node || {}
    end

    # Returns a child AST node as AstNode class.
    def child(name)
      AstNode.create(@node[name])
    end
    # Shorthand for #child method
    def [](name)
      child(name)
    end

    # Returns the raw Exprima AST node this class wraps.
    def raw
      @node
    end

    # Serializes the node into string
    def to_s
      begin
        Serializer.new.to_s(@node)
      rescue
        nil
      end
    end

    # Evaluates the node into basic JavaScript value.
    def to_value
      begin
        Evaluator.new.to_value(@node)
      rescue
        nil
      end
    end

    # Converts object expression property key to string value
    def key_value
      Evaluator.new.key_value(@node)
    end

    # Returns the type of node value.
    def value_type
      v = to_value
      if v.is_a?(String)
        "String"
      elsif v.is_a?(Numeric)
        "Number"
      elsif v.is_a?(TrueClass) || v.is_a?(FalseClass)
        "Boolean"
      elsif v.is_a?(Array)
        "Array"
      elsif v.is_a?(Hash)
        "Object"
      elsif v == :regexp
        "RegExp"
      else
        nil
      end
    end

    # Iterates over keys and values in ObjectExpression.  The keys
    # are turned into strings, but values are left as is for further
    # processing.
    def each_property
      return unless object_expression?

      child("properties").each do |ast|
        yield(ast["key"].key_value, ast["value"], ast)
      end
    end

    # Returns line number in parsed source where the AstNode resides.
    def linenr
      # Get line number from third place at range array.
      # This third item exists in forked EsprimaJS at
      # https://github.com/nene/esprima/tree/linenr-in-range
      @node["range"][2]
    end

    # Tests for higher level types which don't correspond directly to
    # Esprima AST types.

    def function?
      function_declaration? || function_expression? || ext_empty_fn?
    end

    def fire_event?
      call_expression? && child("callee").to_s == "this.fireEvent"
    end

    def string?
      literal? && @node["value"].is_a?(String)
    end

    # Checks dependent on Ext namespace,
    # which may not always be "Ext" but also something user-defined.

    def ext_empty_fn?
      member_expression? && ext_pattern?("Ext.emptyFn")
    end

    def ext_define?
      call_expression? && child("callee").ext_pattern?("Ext.define")
    end

    def ext_extend?
      call_expression? && child("callee").ext_pattern?("Ext.extend")
    end

    def ext_override?
      call_expression? && child("callee").ext_pattern?("Ext.override")
    end

    def ext_pattern?(pattern)
      ExtPatterns.matches?(pattern, to_s)
    end

    # Simple shorthands for testing the type of node
    # These have one-to-one mapping to Esprima node types.

    def call_expression?
      @node["type"] == "CallExpression"
    end

    def assignment_expression?
      @node["type"] == "AssignmentExpression"
    end

    def object_expression?
      @node["type"] == "ObjectExpression"
    end

    def array_expression?
      @node["type"] == "ArrayExpression"
    end

    def function_expression?
      @node["type"] == "FunctionExpression"
    end

    def member_expression?
      @node["type"] == "MemberExpression"
    end

    def expression_statement?
      @node["type"] == "ExpressionStatement"
    end

    def variable_declaration?
      @node["type"] == "VariableDeclaration"
    end

    def function_declaration?
      @node["type"] == "FunctionDeclaration"
    end

    def property?
      @node["type"] == "Property"
    end

    def identifier?
      @node["type"] == "Identifier"
    end

    def literal?
      @node["type"] == "Literal"
    end

  end

end

lib/jsduck/ast_node_array.rb

deleted100644 → 0
+0 −34
Original line number Diff line number Diff line
require "jsduck/ast_node"

module JsDuck

  # Wraps around array of AST nodes.
  class AstNodeArray
    # Initialized with array of AST Hashes from Esprima.
    def initialize(nodes)
      @nodes = nodes || []
    end

    # Returns a child AST node as AstNode class.
    def [](i)
      AstNode.create(@nodes[i])
    end

    # The length of array
    def length
      @nodes.length
    end

    # Iterates over all the AstNodes in array.
    def each
      @nodes.each {|p| yield(AstNode.create(p)) }
    end

    # Maps over all the AstNodes in array.
    def map
      @nodes.map {|p| yield(AstNode.create(p)) }
    end

  end

end

lib/jsduck/ast_utils.rb

deleted100644 → 0
+0 −19
Original line number Diff line number Diff line
module JsDuck

  # Helpers for handling the parsing of Ext.define definitions
  class AstUtils
    # When the value is string, returns the string, otherwise nil
    def self.make_string(ast)
      str = ast.to_value
      str.is_a?(String) ? str : nil
    end

    # When the value is string or array of strings, returns array of
    # strings. In any other case, returns empty array.
    def self.make_string_list(ast)
      strings = Array(ast.to_value)
      strings.all? {|s| s.is_a?(String) } ? strings : []
    end
  end

end
+1 −1
Original line number Diff line number Diff line
@@ -35,7 +35,7 @@ module JsDuck
        elsif look(:var, ":")
          var_declaration
        else
          # Default to property like in JsParser.
          # Default to property like in Js::Parser.
          {:tagname => :property}
        end
      end
Loading