Loading lib/jsduck/function_ast.rb +60 −26 Original line number Diff line number Diff line Loading @@ -14,7 +14,8 @@ module JsDuck # what the function returns. def chainable?(ast) if ast && function?(ast) body_returns(ast["body"]["body"]) rvalues = return_values(ast["body"]["body"]) rvalues.keys == [:this] else false end Loading @@ -26,10 +27,43 @@ module JsDuck ast["type"] == "FunctionDeclaration" || ast["type"] == "FunctionExpression" end def body_returns(body) body = skip_returnless_statements(body) # Given an array of statements determines the possible return values. # Returns a hash with the return values. # # For now there are three possible detected return values: # # * :void - the code can finish without explicitly returning anything # * :this - the code contins 'return this;' # * :other - some other value is returned. # def return_values(body) rvalues = {} body.each do |ast| if return_this?(ast) rvalues[:this] = true return rvalues elsif return?(ast) rvalues[:other] = true return rvalues elsif possibly_blocking?(ast) extract_bodies(ast).each do |b| rvalues.merge!(return_values(b)) end if !rvalues[:void] return rvalues else rvalues.delete(:void) end elsif control_flow?(ast) extract_bodies(ast).each do |b| rvalues.merge!(return_values(b)) end rvalues.delete(:void) end end return body.length > 0 && return_this?(body[0]) rvalues[:void] = true return rvalues end def return_this?(ast) Loading @@ -44,42 +78,42 @@ module JsDuck ast["type"] == "ThisExpression" end def skip_returnless_statements(statements) i = statements.find_index {|s| contains_return?(s) } if i statements.slice(i, statements.length) else [] end end def contains_return?(ast) if return?(ast) true elsif control_flow?(ast) extract_body(ast).any? {|s| contains_return?(s) } else false end end def control_flow?(ast) CONTROL_FLOW[ast["type"]] end def extract_body(ast) def extract_bodies(ast) body = [] CONTROL_FLOW[ast["type"]].each do |name| statements = ast[name] if statements.is_a?(Hash) body << statements body << [statements] else body += Array(statements) body << Array(statements) end end body end # True if the node is a control structure which will block further # program flow when all its branches finish with a return # statement. def possibly_blocking?(ast) if POSSIBLY_BLOCKING[ast["type"]] CONTROL_FLOW[ast["type"]].all? {|key| ast[key] } else false end end POSSIBLY_BLOCKING = { "IfStatement" => true, "DoWhileStatement" => true, "WithStatement" => true, "LabeledStatement" => true, "BlockStatement" => true, } CONTROL_FLOW = { "IfStatement" => ["consequent", "alternate"], "SwitchStatement" => ["cases"], Loading spec/function_ast_spec.rb +68 −0 Original line number Diff line number Diff line Loading @@ -144,4 +144,72 @@ describe "JsDuck::FunctionAst#chainable?" do EOJS end it "true when RETURN THIS after statements also containing a RETURN THIS" do chainable?(<<-EOJS).should == true /** */ function foo() { while (x) { if (foo) { } else if (ooh) { return this; } } return this; } EOJS end it "false when only one branch finishes with RETURN THIS" do chainable?(<<-EOJS).should == false /** */ function foo() { if (foo) { doSomething(); } else { return this; } } EOJS end it "true when both branches of IF finish with RETURN THIS" do chainable?(<<-EOJS).should == true /** */ function foo() { if (foo) { blah(); if (true) { return this; } else { chah(); return this; } } else { return this; } } EOJS end it "true when DO WHILE contains RETURN THIS" do chainable?(<<-EOJS).should == true /** */ function foo() { do { return this; } while(true); } EOJS end it "false when WHILE contains RETURN THIS" do chainable?(<<-EOJS).should == false /** */ function foo() { while (condition) { return this; }; } EOJS end end Loading
lib/jsduck/function_ast.rb +60 −26 Original line number Diff line number Diff line Loading @@ -14,7 +14,8 @@ module JsDuck # what the function returns. def chainable?(ast) if ast && function?(ast) body_returns(ast["body"]["body"]) rvalues = return_values(ast["body"]["body"]) rvalues.keys == [:this] else false end Loading @@ -26,10 +27,43 @@ module JsDuck ast["type"] == "FunctionDeclaration" || ast["type"] == "FunctionExpression" end def body_returns(body) body = skip_returnless_statements(body) # Given an array of statements determines the possible return values. # Returns a hash with the return values. # # For now there are three possible detected return values: # # * :void - the code can finish without explicitly returning anything # * :this - the code contins 'return this;' # * :other - some other value is returned. # def return_values(body) rvalues = {} body.each do |ast| if return_this?(ast) rvalues[:this] = true return rvalues elsif return?(ast) rvalues[:other] = true return rvalues elsif possibly_blocking?(ast) extract_bodies(ast).each do |b| rvalues.merge!(return_values(b)) end if !rvalues[:void] return rvalues else rvalues.delete(:void) end elsif control_flow?(ast) extract_bodies(ast).each do |b| rvalues.merge!(return_values(b)) end rvalues.delete(:void) end end return body.length > 0 && return_this?(body[0]) rvalues[:void] = true return rvalues end def return_this?(ast) Loading @@ -44,42 +78,42 @@ module JsDuck ast["type"] == "ThisExpression" end def skip_returnless_statements(statements) i = statements.find_index {|s| contains_return?(s) } if i statements.slice(i, statements.length) else [] end end def contains_return?(ast) if return?(ast) true elsif control_flow?(ast) extract_body(ast).any? {|s| contains_return?(s) } else false end end def control_flow?(ast) CONTROL_FLOW[ast["type"]] end def extract_body(ast) def extract_bodies(ast) body = [] CONTROL_FLOW[ast["type"]].each do |name| statements = ast[name] if statements.is_a?(Hash) body << statements body << [statements] else body += Array(statements) body << Array(statements) end end body end # True if the node is a control structure which will block further # program flow when all its branches finish with a return # statement. def possibly_blocking?(ast) if POSSIBLY_BLOCKING[ast["type"]] CONTROL_FLOW[ast["type"]].all? {|key| ast[key] } else false end end POSSIBLY_BLOCKING = { "IfStatement" => true, "DoWhileStatement" => true, "WithStatement" => true, "LabeledStatement" => true, "BlockStatement" => true, } CONTROL_FLOW = { "IfStatement" => ["consequent", "alternate"], "SwitchStatement" => ["cases"], Loading
spec/function_ast_spec.rb +68 −0 Original line number Diff line number Diff line Loading @@ -144,4 +144,72 @@ describe "JsDuck::FunctionAst#chainable?" do EOJS end it "true when RETURN THIS after statements also containing a RETURN THIS" do chainable?(<<-EOJS).should == true /** */ function foo() { while (x) { if (foo) { } else if (ooh) { return this; } } return this; } EOJS end it "false when only one branch finishes with RETURN THIS" do chainable?(<<-EOJS).should == false /** */ function foo() { if (foo) { doSomething(); } else { return this; } } EOJS end it "true when both branches of IF finish with RETURN THIS" do chainable?(<<-EOJS).should == true /** */ function foo() { if (foo) { blah(); if (true) { return this; } else { chah(); return this; } } else { return this; } } EOJS end it "true when DO WHILE contains RETURN THIS" do chainable?(<<-EOJS).should == true /** */ function foo() { do { return this; } while(true); } EOJS end it "false when WHILE contains RETURN THIS" do chainable?(<<-EOJS).should == false /** */ function foo() { while (condition) { return this; }; } EOJS end end