Commit 21bea606 authored by Rene Saarsoo's avatar Rene Saarsoo
Browse files

Create Js::Fires for auto-detecting @fires.

Looks for this.fireEvent("blah") inside function body.
Also understands me.fireEvent("blah") when me has been
assigned with `this`.
parent 478b7db3
Loading
Loading
Loading
Loading

lib/jsduck/js/fires.rb

0 → 100644
+102 −0
Original line number Diff line number Diff line
require "jsduck/util/singleton"
require "jsduck/js/node_array"

module JsDuck
  module Js

    # Looks the AST of a FunctionDeclaration or FunctionExpression for
    # uses of this.fireEvent().
    class Fires
      include Util::Singleton

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

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

      private

      def detect_body(body_nodes)
        events = []

        body_nodes.each do |node|
          if fire_event?(node)
            events << extract_event_name(node)
          elsif control_flow?(node)
            events.concat(detect_body(extract_body(node)))
          elsif node.variable_declaration?
            extract_this_vars(node).each do |var_name|
              @this_map[var_name] = true
            end
          end
        end

        events
      end

      # True when node is this.fireEvent("name") call.
      # Also true for me.fireEvent() when me == this.
      def fire_event?(node)
        node.expression_statement? &&
          node["expression"].call_expression? &&
          node["expression"]["callee"].member_expression? &&
          @this_map[node["expression"]["callee"]["object"].to_s] &&
          node["expression"]["callee"]["property"].to_s == "fireEvent" &&
          node["expression"]["arguments"].length > 0 &&
          node["expression"]["arguments"][0].value_type == "String"
      end

      def extract_event_name(node)
        node["expression"]["arguments"][0].to_value
      end

      # Extracts variable names assigned with `this`.
      def extract_this_vars(var)
        mappings = []
        var["declarations"].each do |v|
          if v["init"].type == "ThisExpression"
            mappings << v["id"].to_s
          end
        end
        mappings
      end

      def control_flow?(ast)
        CONTROL_FLOW[ast.type]
      end

      def extract_body(ast)
        body = []
        CONTROL_FLOW[ast.type].each do |name|
          statements = ast[name]
          if statements.is_a?(NodeArray)
            statements.each {|s| body << s }
          else
            body << statements
          end
        end
        body
      end

      CONTROL_FLOW = {
        "IfStatement" => ["consequent", "alternate"],
        "SwitchStatement" => ["cases"],
        "SwitchCase" => ["consequent"],
        "ForStatement" => ["body"],
        "ForInStatement" => ["body"],
        "WhileStatement" => ["body"],
        "DoWhileStatement" => ["body"],
        "TryStatement" => ["block", "handlers", "finalizer"],
        "CatchClause" => ["body"],
        "WithStatement" => ["body"],
        "LabeledStatement" => ["body"],
        "BlockStatement" => ["body"],
      }
    end
  end
end
+5 −0
Original line number Diff line number Diff line
@@ -80,6 +80,11 @@ module JsDuck
        end
      end

      # Returns the type of node.
      def type
        @node["type"]
      end

      # Iterates over keys and values in ObjectExpression.  The keys
      # are turned into strings, but values are left as is for further
      # processing.

spec/js_fires_spec.rb

0 → 100644
+81 −0
Original line number Diff line number Diff line
require "jsduck/js/parser"
require "jsduck/js/fires"
require "jsduck/js/node"

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

  describe "detects fired events when function body" do
    it "has single this.fireEvent() statement" do
      fires(<<-EOJS).should == ["click"]
        /** */
        function f() {
            this.fireEvent('click');
        }
      EOJS
    end

    it "has multiple this.fireEvent() statements" do
      fires(<<-EOJS).should == ["click", "dblclick"]
        /** */
        function f() {
            this.fireEvent('click');
            var x = 10;
            this.fireEvent('dblclick');
        }
      EOJS
    end

    it "has this.fireEvent() inside control structures" do
      fires(<<-EOJS).should == ["click", "dblclick"]
        /** */
        function f() {
            if (true) {
                while (x) {
                    this.fireEvent('click');
                }
            }
            else {
                this.fireEvent('dblclick');
            }
        }
      EOJS
    end

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

  describe "detects only unique events when function body" do
    it "has the same event fired multiple times" do
      fires(<<-EOJS).should == ["click", "blah"]
        /** */
        function f() {
            this.fireEvent('click');
            this.fireEvent('click');
            this.fireEvent('blah');
            this.fireEvent('click');
            this.fireEvent('blah');
        }
      EOJS
    end
  end

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

end