Commit 8a317cad authored by Rene Saarsoo's avatar Rene Saarsoo
Browse files

Implement detection of all method calls within a function.

First off it's just a Js::MethodCalls class that's not yet
connected to JSDuck.
parent 6d55d375
Loading
Loading
Loading
Loading
+82 −0
Original line number Diff line number Diff line
require "jsduck/util/singleton"

module JsDuck
  module Js

    # Looks the AST of a FunctionDeclaration or FunctionExpression for
    # calls to methods of the owner class.
    class MethodCalls
      include Util::Singleton

      # Returns array of method names called by the given function.
      # When no methods called, empty array is returned.
      def detect(node)
        @this_map = {
          "this" => true
        }

        detect_body(node["body"]["body"]).sort.uniq
      end

      private

      def detect_body(body_nodes)
        methods = []

        body_nodes.each do |node|
          if method_call?(node)
            methods << node["callee"]["property"].to_s
          end

          if this_var?(node)
            var_name = node["id"].to_s
            @this_map[var_name] = true
          end

          methods.concat(detect_body(extract_body(node)))
        end

        methods
      end

      # True when node is this.someMethod() call.
      # Also true for me.someMethod() when me == this.
      def method_call?(node)
        node.call_expression? &&
          node["callee"].member_expression? &&
          node["callee"].raw["computed"] == false &&
          @this_map[node["callee"]["object"].to_s]
      end

      # True when initialization of variable with `this`
      def this_var?(node)
        node.type == "VariableDeclarator" && node["init"].type == "ThisExpression"
      end

      # Extracts all sub-statements and sub-expressions from AST node.
      # Without looking at the type of node, we just take all the
      # sub-hashes and -arrays.
      #
      # A downside of this simple algorithm is that the statements can
      # end up in different order than they are in source code.  For
      # example the IfStatement has three parts in the following
      # order: "test", "consequent", "alternate": But because we're
      # looping over a hash, they might end up in a totally different
      # order.
      def extract_body(node)
        body = []
        node.raw.each_pair do |key, value|
          if key == "type" || key == "range"
            # ignore
          elsif value.is_a?(Array)
            node[key].each {|n| body << n }
          elsif value.is_a?(Hash)
            body << node[key]
          end
        end
        body
      end

    end
  end
end
+63 −0
Original line number Diff line number Diff line
require "jsduck/js/parser"
require "jsduck/js/method_calls"
require "jsduck/js/node"

describe "JsDuck::Js::MethodCalls" do
  def calls(string)
    docset = JsDuck::Js::Parser.new(string).parse[0]
    node = JsDuck::Js::Node.create(docset[:code])
    return JsDuck::Js::MethodCalls.detect(node)
  end

  describe "detects called methods when function body" do
    it "has method calls inside control structures" do
      calls(<<-EOJS).should == ["alfa", "beta", "chico", "delta", "eeta"]
        /** */
        function f() {
            if (this.alfa()) {
                while (this.beta()) {
                    this.chico('Hello');
                }
            }
            else {
                return function() {
                    this.delta(1, 2, this.eeta());
                };
            }
        }
      EOJS
    end

    it "has var me=this and me.someMethod()" do
      calls(<<-EOJS).should == ["someMethod"]
        /** */
        function f() {
            var me = this;
            me.someMethod('Blah');
        }
      EOJS
    end
  end

  describe "detects only unique methods when function body" do
    it "has the same method called multiple times" do
      calls(<<-EOJS).should == ["blah", "click"]
        /** */
        function f() {
            this.click("a");
            this.click("b");
            this.blah();
            this.click("c");
            this.blah();
        }
      EOJS
    end
  end

  describe "detects no methods when function body" do
    it "is empty" do
      calls("/** */ function foo() { }").should == []
    end
  end

end