mirror of
https://gitea.invidious.io/iv-org/shard-ameba.git
synced 2024-08-15 00:53:29 +00:00
Extend UnreachableCode rule: handle control flow (#83)
This commit is contained in:
parent
eca0f3f350
commit
0fd5890738
11 changed files with 880 additions and 101 deletions
|
@ -5,42 +5,37 @@ module Ameba::AST
|
||||||
describe "#initialize" do
|
describe "#initialize" do
|
||||||
it "creates a new flow expression" do
|
it "creates a new flow expression" do
|
||||||
node = as_node("return 22")
|
node = as_node("return 22")
|
||||||
parent_node = as_node("def foo; return 22; end")
|
flow_expression = FlowExpression.new node, false
|
||||||
flow_expression = FlowExpression.new node, parent_node
|
|
||||||
flow_expression.node.should_not be_nil
|
flow_expression.node.should_not be_nil
|
||||||
flow_expression.parent_node.should_not be_nil
|
flow_expression.in_loop?.should eq false
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "#delegation" do
|
describe "#delegation" do
|
||||||
it "delegates to_s to @node" do
|
it "delegates to_s to @node" do
|
||||||
node = as_node("return 22")
|
node = as_node("return 22")
|
||||||
parent_node = as_node("def foo; return 22; end")
|
flow_expression = FlowExpression.new node, false
|
||||||
flow_expression = FlowExpression.new node, parent_node
|
|
||||||
flow_expression.to_s.should eq node.to_s
|
flow_expression.to_s.should eq node.to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
it "delegates location to @node" do
|
it "delegates location to @node" do
|
||||||
node = as_node %(break if true)
|
node = as_node %(break if true)
|
||||||
parent_node = as_node("def foo; return 22 if true; end")
|
flow_expression = FlowExpression.new node, false
|
||||||
flow_expression = FlowExpression.new node, parent_node
|
|
||||||
flow_expression.location.should eq node.location
|
flow_expression.location.should eq node.location
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "#find_unreachable_node" do
|
describe "#unreachable_nodes" do
|
||||||
it "returns first unreachable node" do
|
it "returns unreachable nodes" do
|
||||||
nodes = as_nodes %(
|
nodes = as_nodes %(
|
||||||
def foobar
|
def foobar
|
||||||
return
|
return
|
||||||
a = 1
|
a = 1
|
||||||
a + 1
|
a = 2
|
||||||
end
|
end
|
||||||
)
|
)
|
||||||
node = nodes.control_expression_nodes.first
|
node = nodes.expressions_nodes.first
|
||||||
assign_node = nodes.assign_nodes.first
|
flow_expression = FlowExpression.new node, false
|
||||||
def_node = nodes.def_nodes.first
|
flow_expression.unreachable_nodes.should eq nodes.assign_nodes
|
||||||
flow_expression = FlowExpression.new node, def_node
|
|
||||||
flow_expression.find_unreachable_node.should eq assign_node
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns nil if there is no unreachable node" do
|
it "returns nil if there is no unreachable node" do
|
||||||
|
@ -50,10 +45,9 @@ module Ameba::AST
|
||||||
return a
|
return a
|
||||||
end
|
end
|
||||||
)
|
)
|
||||||
node = nodes.control_expression_nodes.first
|
node = nodes.expressions_nodes.first
|
||||||
def_node = nodes.def_nodes.first
|
flow_expression = FlowExpression.new node, false
|
||||||
flow_expression = FlowExpression.new node, def_node
|
flow_expression.unreachable_nodes.empty?.should eq true
|
||||||
flow_expression.find_unreachable_node.should eq nil
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -71,5 +71,242 @@ module Ameba::AST
|
||||||
])
|
])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "#flow_command?" do
|
||||||
|
it "returns true if this is return" do
|
||||||
|
node = as_node("return 22")
|
||||||
|
subject.flow_command?(node, false).should eq true
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns true if this is a break in a loop" do
|
||||||
|
node = as_node("break")
|
||||||
|
subject.flow_command?(node, true).should eq true
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns false if this is a break out of loop" do
|
||||||
|
node = as_node("break")
|
||||||
|
subject.flow_command?(node, false).should eq false
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns true if this is a next in a loop" do
|
||||||
|
node = as_node("next")
|
||||||
|
subject.flow_command?(node, true).should eq true
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns false if this is a next out of loop" do
|
||||||
|
node = as_node("next")
|
||||||
|
subject.flow_command?(node, false).should eq false
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns true if this is raise" do
|
||||||
|
node = as_node("raise e")
|
||||||
|
subject.flow_command?(node, false).should eq true
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns true if this is exit" do
|
||||||
|
node = as_node("exit")
|
||||||
|
subject.flow_command?(node, false).should eq true
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns true if this is abort" do
|
||||||
|
node = as_node("abort")
|
||||||
|
subject.flow_command?(node, false).should eq true
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns false otherwise" do
|
||||||
|
node = as_node("foobar")
|
||||||
|
subject.flow_command?(node, false).should eq false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#flow_expression?" do
|
||||||
|
it "returns true if this is a flow command" do
|
||||||
|
node = as_node("return")
|
||||||
|
subject.flow_expression?(node, true).should eq true
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns true if this is if-else consumed by flow expressions" do
|
||||||
|
node = as_node %(
|
||||||
|
if foo
|
||||||
|
return :foo
|
||||||
|
else
|
||||||
|
return :bar
|
||||||
|
end
|
||||||
|
)
|
||||||
|
subject.flow_expression?(node, false).should eq true
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns true if this is unless-else consumed by flow expressions" do
|
||||||
|
node = as_node %(
|
||||||
|
unless foo
|
||||||
|
return :foo
|
||||||
|
else
|
||||||
|
return :bar
|
||||||
|
end
|
||||||
|
)
|
||||||
|
subject.flow_expression?(node).should eq true
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns true if this is case consumed by flow expressions" do
|
||||||
|
node = as_node %(
|
||||||
|
case
|
||||||
|
when 1
|
||||||
|
return 1
|
||||||
|
when 2
|
||||||
|
return 2
|
||||||
|
else
|
||||||
|
return 3
|
||||||
|
end
|
||||||
|
)
|
||||||
|
subject.flow_expression?(node).should eq true
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns true if this is exception handler consumed by flow expressions" do
|
||||||
|
node = as_node %(
|
||||||
|
begin
|
||||||
|
raise "exp"
|
||||||
|
rescue e
|
||||||
|
return e
|
||||||
|
end
|
||||||
|
)
|
||||||
|
subject.flow_expression?(node).should eq true
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns true if this while consumed by flow expressions" do
|
||||||
|
node = as_node %(
|
||||||
|
while true
|
||||||
|
return
|
||||||
|
end
|
||||||
|
)
|
||||||
|
subject.flow_expression?(node).should eq true
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns false if this while with break" do
|
||||||
|
node = as_node %(
|
||||||
|
while true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
)
|
||||||
|
subject.flow_expression?(node).should eq false
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns true if this until consumed by flow expressions" do
|
||||||
|
node = as_node %(
|
||||||
|
until false
|
||||||
|
return
|
||||||
|
end
|
||||||
|
)
|
||||||
|
subject.flow_expression?(node).should eq true
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns false if this until with break" do
|
||||||
|
node = as_node %(
|
||||||
|
until false
|
||||||
|
break
|
||||||
|
end
|
||||||
|
)
|
||||||
|
subject.flow_expression?(node).should eq false
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns true if this expressions consumed by flow expressions" do
|
||||||
|
node = as_node %(
|
||||||
|
exp1
|
||||||
|
exp2
|
||||||
|
return
|
||||||
|
)
|
||||||
|
subject.flow_expression?(node).should eq true
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns false otherwise" do
|
||||||
|
node = as_node %(
|
||||||
|
exp1
|
||||||
|
exp2
|
||||||
|
)
|
||||||
|
subject.flow_expression?(node).should eq false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#raise?" do
|
||||||
|
it "returns true if this is a raise method call" do
|
||||||
|
node = as_node "raise e"
|
||||||
|
subject.raise?(node).should eq true
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns false if it has a receiver" do
|
||||||
|
node = as_node "obj.raise e"
|
||||||
|
subject.raise?(node).should eq false
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns false if size of the arguments doesn't match" do
|
||||||
|
node = as_node "raise"
|
||||||
|
subject.raise?(node).should eq false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#exit?" do
|
||||||
|
it "returns true if this is a exit method call" do
|
||||||
|
node = as_node "exit"
|
||||||
|
subject.exit?(node).should eq true
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns true if this is a exit method call with one argument" do
|
||||||
|
node = as_node "exit 1"
|
||||||
|
subject.exit?(node).should eq true
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns false if it has a receiver" do
|
||||||
|
node = as_node "obj.exit"
|
||||||
|
subject.exit?(node).should eq false
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns false if size of the arguments doesn't match" do
|
||||||
|
node = as_node "exit 1, 1"
|
||||||
|
subject.exit?(node).should eq false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#abort?" do
|
||||||
|
it "returns true if this is an abort method call" do
|
||||||
|
node = as_node "abort"
|
||||||
|
subject.abort?(node).should eq true
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns true if this is an abort method call with one argument" do
|
||||||
|
node = as_node "abort \"message\""
|
||||||
|
subject.abort?(node).should eq true
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns true if this is an abort method call with two arguments" do
|
||||||
|
node = as_node "abort \"message\", 1"
|
||||||
|
subject.abort?(node).should eq true
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns false if it has a receiver" do
|
||||||
|
node = as_node "obj.abort"
|
||||||
|
subject.abort?(node).should eq false
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns false if size of the arguments doesn't match" do
|
||||||
|
node = as_node "abort 1, 1, 1"
|
||||||
|
subject.abort?(node).should eq false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#loop?" do
|
||||||
|
it "returns true if this is a loop method call" do
|
||||||
|
node = as_node "loop"
|
||||||
|
subject.loop?(node).should eq true
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns false if it has a receiver" do
|
||||||
|
node = as_node "obj.loop"
|
||||||
|
subject.loop?(node).should eq false
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns false if size of the arguments doesn't match" do
|
||||||
|
node = as_node "loop 1"
|
||||||
|
subject.loop?(node).should eq false
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -25,7 +25,7 @@ module Ameba::AST
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
)
|
)
|
||||||
rule.expressions.size.should eq 2
|
rule.expressions.size.should eq 3
|
||||||
end
|
end
|
||||||
|
|
||||||
it "properly creates nested flow expressions" do
|
it "properly creates nested flow expressions" do
|
||||||
|
@ -40,7 +40,7 @@ module Ameba::AST
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
)
|
)
|
||||||
rule.expressions.size.should eq 3
|
rule.expressions.size.should eq 4
|
||||||
end
|
end
|
||||||
|
|
||||||
it "creates an expression for break" do
|
it "creates an expression for break" do
|
||||||
|
|
|
@ -5,14 +5,12 @@ module Ameba::AST
|
||||||
source = Source.new ""
|
source = Source.new ""
|
||||||
|
|
||||||
describe NodeVisitor do
|
describe NodeVisitor do
|
||||||
{% for name in NODES %}
|
describe "visit" do
|
||||||
describe "{{name}}" do
|
it "allow to visit ASTNode" do
|
||||||
it "allow to visit {{name}} node" do
|
visitor = NodeVisitor.new rule, source
|
||||||
visitor = NodeVisitor.new rule, source
|
nodes = Crystal::Parser.new("").parse
|
||||||
nodes = Crystal::Parser.new("").parse
|
nodes.accept visitor
|
||||||
nodes.accept visitor
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
{% end %}
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -41,6 +41,23 @@ module Ameba::Rule::Lint
|
||||||
subject.catch(s).should be_valid
|
subject.catch(s).should be_valid
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "doesn't report if there is no else in if" do
|
||||||
|
s = Source.new %(
|
||||||
|
if a > 0
|
||||||
|
return :positive
|
||||||
|
end
|
||||||
|
:reachable
|
||||||
|
)
|
||||||
|
subject.catch(s).should be_valid
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't report return in on-line if" do
|
||||||
|
s = Source.new %(
|
||||||
|
return :positive if a > 0
|
||||||
|
)
|
||||||
|
subject.catch(s).should be_valid
|
||||||
|
end
|
||||||
|
|
||||||
it "doesn't report if return is used in a block" do
|
it "doesn't report if return is used in a block" do
|
||||||
s = Source.new %(
|
s = Source.new %(
|
||||||
def foo
|
def foo
|
||||||
|
@ -57,7 +74,7 @@ module Ameba::Rule::Lint
|
||||||
subject.catch(s).should be_valid
|
subject.catch(s).should be_valid
|
||||||
end
|
end
|
||||||
|
|
||||||
pending "reports if there is unreachable code after if-then-else" do
|
it "reports if there is unreachable code after if-then-else" do
|
||||||
s = Source.new %(
|
s = Source.new %(
|
||||||
def foo
|
def foo
|
||||||
if a > 0
|
if a > 0
|
||||||
|
@ -71,7 +88,448 @@ module Ameba::Rule::Lint
|
||||||
)
|
)
|
||||||
subject.catch(s).should_not be_valid
|
subject.catch(s).should_not be_valid
|
||||||
issue = s.issues.first
|
issue = s.issues.first
|
||||||
issue.location.to_s.should eq ":8:4"
|
issue.location.to_s.should eq ":8:3"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "reports if there is unreachable code after if-then-else-if" do
|
||||||
|
s = Source.new %(
|
||||||
|
def foo
|
||||||
|
if a > 0
|
||||||
|
return :positive
|
||||||
|
elsif a != 0
|
||||||
|
return :negative
|
||||||
|
else
|
||||||
|
return :zero
|
||||||
|
end
|
||||||
|
|
||||||
|
:unreachable
|
||||||
|
end
|
||||||
|
)
|
||||||
|
subject.catch(s).should_not be_valid
|
||||||
|
issue = s.issues.first
|
||||||
|
issue.location.to_s.should eq ":10:3"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't report if there is no unreachable code after if-then-else" do
|
||||||
|
s = Source.new %(
|
||||||
|
def foo
|
||||||
|
if a > 0
|
||||||
|
return :positive
|
||||||
|
else
|
||||||
|
return :negative
|
||||||
|
end
|
||||||
|
end
|
||||||
|
)
|
||||||
|
subject.catch(s).should be_valid
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't report if there is no unreachable in inner branch" do
|
||||||
|
s = Source.new %(
|
||||||
|
def foo
|
||||||
|
if a > 0
|
||||||
|
return :positive if a != 1
|
||||||
|
else
|
||||||
|
return :negative
|
||||||
|
end
|
||||||
|
|
||||||
|
:not_unreachable
|
||||||
|
end
|
||||||
|
)
|
||||||
|
subject.catch(s).should be_valid
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't report if there is no unreachable in exception handler" do
|
||||||
|
s = Source.new %(
|
||||||
|
def foo
|
||||||
|
puts :bar
|
||||||
|
rescue Exception
|
||||||
|
raise "Error!"
|
||||||
|
end
|
||||||
|
)
|
||||||
|
subject.catch(s).should be_valid
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't report if there is multiple conditions with return" do
|
||||||
|
s = Source.new %(
|
||||||
|
if :foo
|
||||||
|
if :bar
|
||||||
|
return :foobar
|
||||||
|
else
|
||||||
|
return :foobaz
|
||||||
|
end
|
||||||
|
elsif :fox
|
||||||
|
return :foofox
|
||||||
|
end
|
||||||
|
|
||||||
|
return :reachable
|
||||||
|
)
|
||||||
|
subject.catch(s).should be_valid
|
||||||
|
end
|
||||||
|
|
||||||
|
it "reports if there is unreachable code after unless" do
|
||||||
|
s = Source.new %(
|
||||||
|
unless :foo
|
||||||
|
return :bar
|
||||||
|
else
|
||||||
|
return :foo
|
||||||
|
end
|
||||||
|
|
||||||
|
:unreachable
|
||||||
|
)
|
||||||
|
subject.catch(s).should_not be_valid
|
||||||
|
issue = s.issues.first
|
||||||
|
issue.location.to_s.should eq ":7:1"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't report if there is no unreachable code after unless" do
|
||||||
|
s = Source.new %(
|
||||||
|
unless :foo
|
||||||
|
return :bar
|
||||||
|
end
|
||||||
|
|
||||||
|
:reachable
|
||||||
|
)
|
||||||
|
subject.catch(s).should be_valid
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "binary op" do
|
||||||
|
it "reports unreachable code in a binary operator" do
|
||||||
|
s = Source.new %(
|
||||||
|
(return 22) && puts "a"
|
||||||
|
)
|
||||||
|
subject.catch(s).should_not be_valid
|
||||||
|
|
||||||
|
issue = s.issues.first
|
||||||
|
issue.location.to_s.should eq ":1:16"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "reports unreachable code in inner binary operator" do
|
||||||
|
s = Source.new %(
|
||||||
|
do_something || (return 22) && puts "a"
|
||||||
|
)
|
||||||
|
subject.catch(s).should_not be_valid
|
||||||
|
|
||||||
|
issue = s.issues.first
|
||||||
|
issue.location.to_s.should eq ":1:32"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "reports unreachable code after the binary op" do
|
||||||
|
s = Source.new %(
|
||||||
|
(return 22) && break
|
||||||
|
:unreachable
|
||||||
|
)
|
||||||
|
subject.catch(s).should_not be_valid
|
||||||
|
|
||||||
|
issue = s.issues.first
|
||||||
|
issue.location.to_s.should eq ":2:1"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't report if return is not the right" do
|
||||||
|
s = Source.new %(
|
||||||
|
puts "a" && return
|
||||||
|
)
|
||||||
|
subject.catch(s).should be_valid
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't report unreachable code in multiple binary expressions" do
|
||||||
|
s = Source.new %(
|
||||||
|
foo || bar || baz
|
||||||
|
)
|
||||||
|
subject.catch(s).should be_valid
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "case" do
|
||||||
|
it "reports if there is unreachable code after case" do
|
||||||
|
s = Source.new %(
|
||||||
|
def foo
|
||||||
|
case cond
|
||||||
|
when 1
|
||||||
|
something
|
||||||
|
return
|
||||||
|
when 2
|
||||||
|
something2
|
||||||
|
return
|
||||||
|
else
|
||||||
|
something3
|
||||||
|
return
|
||||||
|
end
|
||||||
|
:unreachable
|
||||||
|
end
|
||||||
|
)
|
||||||
|
subject.catch(s).should_not be_valid
|
||||||
|
issue = s.issues.first
|
||||||
|
issue.location.to_s.should eq ":13:3"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't report if case does not have else" do
|
||||||
|
s = Source.new %(
|
||||||
|
def foo
|
||||||
|
case cond
|
||||||
|
when 1
|
||||||
|
something
|
||||||
|
return
|
||||||
|
when 2
|
||||||
|
something2
|
||||||
|
return
|
||||||
|
end
|
||||||
|
:reachable
|
||||||
|
end
|
||||||
|
)
|
||||||
|
subject.catch(s).should be_valid
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't report if one when does not return" do
|
||||||
|
s = Source.new %(
|
||||||
|
def foo
|
||||||
|
case cond
|
||||||
|
when 1
|
||||||
|
something
|
||||||
|
return
|
||||||
|
when 2
|
||||||
|
something2
|
||||||
|
else
|
||||||
|
something3
|
||||||
|
return
|
||||||
|
end
|
||||||
|
:reachable
|
||||||
|
end
|
||||||
|
)
|
||||||
|
subject.catch(s).should be_valid
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "exception handler" do
|
||||||
|
it "reports unreachable code if it returns in body and rescues" do
|
||||||
|
s = Source.new %(
|
||||||
|
def foo
|
||||||
|
begin
|
||||||
|
return false
|
||||||
|
rescue Error
|
||||||
|
return false
|
||||||
|
rescue Exception
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
:unreachable
|
||||||
|
end
|
||||||
|
)
|
||||||
|
subject.catch(s).should_not be_valid
|
||||||
|
|
||||||
|
issue = s.issues.first
|
||||||
|
issue.location.to_s.should eq ":9:3"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "reports unreachable code if it returns in rescues and else" do
|
||||||
|
s = Source.new %(
|
||||||
|
def foo
|
||||||
|
begin
|
||||||
|
do_something
|
||||||
|
rescue Error
|
||||||
|
return :error
|
||||||
|
else
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
:unreachable
|
||||||
|
end
|
||||||
|
)
|
||||||
|
subject.catch(s).should_not be_valid
|
||||||
|
|
||||||
|
issue = s.issues.first
|
||||||
|
issue.location.to_s.should eq ":9:3"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't report if there is no else and ensure doesn't return" do
|
||||||
|
s = Source.new %(
|
||||||
|
def foo
|
||||||
|
begin
|
||||||
|
return false
|
||||||
|
rescue Error
|
||||||
|
puts "error"
|
||||||
|
rescue Exception
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
:reachable
|
||||||
|
end
|
||||||
|
)
|
||||||
|
subject.catch(s).should be_valid
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't report if there is no else and body doesn't return" do
|
||||||
|
s = Source.new %(
|
||||||
|
def foo
|
||||||
|
begin
|
||||||
|
do_something
|
||||||
|
rescue Error
|
||||||
|
return true
|
||||||
|
rescue Exception
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
:reachable
|
||||||
|
end
|
||||||
|
)
|
||||||
|
subject.catch(s).should be_valid
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't report if there is else and ensure doesn't return" do
|
||||||
|
s = Source.new %(
|
||||||
|
def foo
|
||||||
|
begin
|
||||||
|
do_something
|
||||||
|
rescue Error
|
||||||
|
puts "yo"
|
||||||
|
else
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
:reachable
|
||||||
|
end
|
||||||
|
)
|
||||||
|
subject.catch(s).should be_valid
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't report if there is else and it doesn't return" do
|
||||||
|
s = Source.new %(
|
||||||
|
def foo
|
||||||
|
begin
|
||||||
|
do_something
|
||||||
|
rescue Error
|
||||||
|
return false
|
||||||
|
else
|
||||||
|
puts "yo"
|
||||||
|
end
|
||||||
|
:reachable
|
||||||
|
end
|
||||||
|
)
|
||||||
|
subject.catch(s).should be_valid
|
||||||
|
end
|
||||||
|
|
||||||
|
it "reports if there is unreachable code in rescue" do
|
||||||
|
s = Source.new %(
|
||||||
|
def method
|
||||||
|
rescue
|
||||||
|
return 22
|
||||||
|
:unreachable
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
|
subject.catch(s).should_not be_valid
|
||||||
|
|
||||||
|
issue = s.issues.first
|
||||||
|
issue.location.to_s.should eq ":4:3"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "while/until" do
|
||||||
|
it "reports if there is unreachable code after while" do
|
||||||
|
s = Source.new %(
|
||||||
|
def method
|
||||||
|
while something
|
||||||
|
if :foo
|
||||||
|
return :foo
|
||||||
|
else
|
||||||
|
return :foobar
|
||||||
|
end
|
||||||
|
end
|
||||||
|
:unreachable
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
|
subject.catch(s).should_not be_valid
|
||||||
|
|
||||||
|
issue = s.issues.first
|
||||||
|
issue.location.to_s.should eq ":9:3"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "reports if there is unreachable code after until" do
|
||||||
|
s = Source.new %(
|
||||||
|
def method
|
||||||
|
until something
|
||||||
|
if :foo
|
||||||
|
return :foo
|
||||||
|
else
|
||||||
|
return :foobar
|
||||||
|
end
|
||||||
|
end
|
||||||
|
:unreachable
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
|
subject.catch(s).should_not be_valid
|
||||||
|
|
||||||
|
issue = s.issues.first
|
||||||
|
issue.location.to_s.should eq ":9:3"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't report if there is reachable code after while with break" do
|
||||||
|
s = Source.new %(
|
||||||
|
while something
|
||||||
|
break
|
||||||
|
end
|
||||||
|
:reachable
|
||||||
|
)
|
||||||
|
|
||||||
|
subject.catch(s).should be_valid
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "rescue" do
|
||||||
|
it "reports unreachable code in rescue" do
|
||||||
|
s = Source.new %(
|
||||||
|
begin
|
||||||
|
|
||||||
|
rescue e
|
||||||
|
raise e
|
||||||
|
:unreachable
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
|
subject.catch(s).should_not be_valid
|
||||||
|
|
||||||
|
issue = s.issues.first
|
||||||
|
issue.location.to_s.should eq ":5:3"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't report if there is no unreachable code in rescue" do
|
||||||
|
s = Source.new %(
|
||||||
|
begin
|
||||||
|
|
||||||
|
rescue e
|
||||||
|
raise e
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
|
subject.catch(s).should be_valid
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when" do
|
||||||
|
it "reports unreachable code in when" do
|
||||||
|
s = Source.new %(
|
||||||
|
case
|
||||||
|
when valid?
|
||||||
|
return 22
|
||||||
|
:unreachable
|
||||||
|
else
|
||||||
|
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
|
subject.catch(s).should_not be_valid
|
||||||
|
|
||||||
|
issue = s.issues.first
|
||||||
|
issue.location.to_s.should eq ":4:3"
|
||||||
|
end
|
||||||
|
|
||||||
|
it "doesn't report if there is no unreachable code in when" do
|
||||||
|
s = Source.new %(
|
||||||
|
case
|
||||||
|
when valid?
|
||||||
|
return 22
|
||||||
|
else
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
|
subject.catch(s).should be_valid
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -129,7 +129,7 @@ module Ameba
|
||||||
Crystal::If,
|
Crystal::If,
|
||||||
Crystal::While,
|
Crystal::While,
|
||||||
Crystal::MacroLiteral,
|
Crystal::MacroLiteral,
|
||||||
Crystal::ControlExpression,
|
Crystal::Expressions,
|
||||||
]
|
]
|
||||||
|
|
||||||
def initialize(node)
|
def initialize(node)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
module Ameba::AST
|
module Ameba::AST
|
||||||
# A generic entity to represent a branchable Crystal node.
|
# A generic entity to represent a branchable Crystal node.
|
||||||
# For example, `Crystal::If`, `Crystal::Unless`, `Crystal::While`
|
# For example, `Crystal::If`, `Crystal::Unless`, `Crystal::While`
|
||||||
# are branchable.
|
# are branchables.
|
||||||
#
|
#
|
||||||
# ```
|
# ```
|
||||||
# white a > 100 # Branchable A
|
# white a > 100 # Branchable A
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
require "./util"
|
||||||
|
|
||||||
module Ameba::AST
|
module Ameba::AST
|
||||||
# Represents a flow expression in Crystal code.
|
# Represents a flow expression in Crystal code.
|
||||||
# For example,
|
# For example,
|
||||||
|
@ -14,12 +16,14 @@ module Ameba::AST
|
||||||
# a parent node, which allows easily search through the related statement
|
# a parent node, which allows easily search through the related statement
|
||||||
# (i.e. find unreachable code)
|
# (i.e. find unreachable code)
|
||||||
class FlowExpression
|
class FlowExpression
|
||||||
|
include Util
|
||||||
|
|
||||||
|
# Is true only if some of the nodes parents is a loop.
|
||||||
|
getter? in_loop : Bool
|
||||||
|
|
||||||
# The actual node of the flow expression.
|
# The actual node of the flow expression.
|
||||||
getter node : Crystal::ASTNode
|
getter node : Crystal::ASTNode
|
||||||
|
|
||||||
# Parent ast node.
|
|
||||||
getter parent_node : Crystal::ASTNode
|
|
||||||
|
|
||||||
delegate to_s, to: @node
|
delegate to_s, to: @node
|
||||||
delegate location, to: @node
|
delegate location, to: @node
|
||||||
|
|
||||||
|
@ -28,10 +32,10 @@ module Ameba::AST
|
||||||
# ```
|
# ```
|
||||||
# FlowExpression.new(node, parent_node)
|
# FlowExpression.new(node, parent_node)
|
||||||
# ```
|
# ```
|
||||||
def initialize(@node, @parent_node)
|
def initialize(@node, @in_loop)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns first node which can't be reached because of a flow expression.
|
# Returns nodes which can't be reached because of a flow expression inside.
|
||||||
# For example:
|
# For example:
|
||||||
#
|
#
|
||||||
# ```
|
# ```
|
||||||
|
@ -41,39 +45,21 @@ module Ameba::AST
|
||||||
#
|
#
|
||||||
# a + 2 # => unreachable assign node
|
# a + 2 # => unreachable assign node
|
||||||
# end
|
# end
|
||||||
# ```
|
def unreachable_nodes
|
||||||
#
|
unreachable_nodes = [] of Crystal::ASTNode
|
||||||
def find_unreachable_node
|
|
||||||
UnreachableNodeVisitor.new(node, parent_node)
|
|
||||||
.tap(&.accept parent_node)
|
|
||||||
.unreachable_nodes
|
|
||||||
.first?
|
|
||||||
end
|
|
||||||
|
|
||||||
# :nodoc:
|
case current_node = node
|
||||||
private class UnreachableNodeVisitor < Crystal::Visitor
|
when Crystal::Expressions
|
||||||
getter unreachable_nodes = Array(Crystal::ASTNode).new
|
control_flow_found = false
|
||||||
@after_control_flow_node = false
|
current_node.expressions.each do |exp|
|
||||||
@branch : AST::Branch?
|
unreachable_nodes << exp if control_flow_found
|
||||||
|
control_flow_found ||= flow_expression?(exp, in_loop?)
|
||||||
def initialize(@node : Crystal::ASTNode, parent_node)
|
end
|
||||||
@branch = Branch.of(@node, parent_node)
|
when Crystal::BinaryOp
|
||||||
|
unreachable_nodes << current_node.right if flow_expression?(current_node.left, in_loop?)
|
||||||
end
|
end
|
||||||
|
|
||||||
def visit(node : Crystal::ASTNode)
|
unreachable_nodes
|
||||||
if node.class == @node.class &&
|
|
||||||
node.location == @node.location
|
|
||||||
@after_control_flow_node = true
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
if @after_control_flow_node && !node.nop? && @branch.nil?
|
|
||||||
@unreachable_nodes << node
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
true
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -40,4 +40,101 @@ module Ameba::AST::Util
|
||||||
|
|
||||||
node_lines
|
node_lines
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Returns true if node is a flow command, false - otherwise.
|
||||||
|
# Node represents a flow command if it is a control expression,
|
||||||
|
# or special call node that interrupts execution (i.e. raise, exit, abort).
|
||||||
|
def flow_command?(node, in_loop?)
|
||||||
|
case node
|
||||||
|
when Crystal::Return
|
||||||
|
true
|
||||||
|
when Crystal::Break, Crystal::Next
|
||||||
|
in_loop?
|
||||||
|
when Crystal::Call
|
||||||
|
raise?(node) || exit?(node) || abort?(node)
|
||||||
|
else
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns true if node is a flow expression, false if not.
|
||||||
|
# Node represents a flow expression if it is full-filed by a flow command.
|
||||||
|
#
|
||||||
|
# For example, this node is a flow expressions, because each branch contains
|
||||||
|
# a flow command `return`:
|
||||||
|
#
|
||||||
|
# ```
|
||||||
|
# if a > 0
|
||||||
|
# return :positive
|
||||||
|
# elsif a < 0
|
||||||
|
# return :negative
|
||||||
|
# else
|
||||||
|
# return :zero
|
||||||
|
# end
|
||||||
|
# ```
|
||||||
|
#
|
||||||
|
# This node is a not a flow expression:
|
||||||
|
#
|
||||||
|
# ```
|
||||||
|
# if a > 0
|
||||||
|
# return :positive
|
||||||
|
# end
|
||||||
|
# ```
|
||||||
|
#
|
||||||
|
# That's because not all branches return(i.e. `else` is missing).
|
||||||
|
#
|
||||||
|
def flow_expression?(node, in_loop? = false)
|
||||||
|
return true if flow_command? node, in_loop?
|
||||||
|
|
||||||
|
case node
|
||||||
|
when Crystal::If, Crystal::Unless
|
||||||
|
flow_expressions? [node.then, node.else], in_loop?
|
||||||
|
when Crystal::BinaryOp
|
||||||
|
flow_expression? node.left, in_loop?
|
||||||
|
when Crystal::Case
|
||||||
|
flow_expressions? [node.whens, node.else].flatten, in_loop?
|
||||||
|
when Crystal::ExceptionHandler
|
||||||
|
flow_expressions? [node.else || node.body, node.rescues].flatten, in_loop?
|
||||||
|
when Crystal::While, Crystal::Until
|
||||||
|
flow_expression? node.body, in_loop?
|
||||||
|
when Crystal::Rescue, Crystal::When
|
||||||
|
flow_expression? node.body, in_loop?
|
||||||
|
when Crystal::Expressions
|
||||||
|
node.expressions.any? { |exp| flow_expression? exp, in_loop? }
|
||||||
|
else
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private def flow_expressions?(nodes, in_loop?)
|
||||||
|
nodes.all? { |exp| flow_expression? exp, in_loop? }
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns true if node represents `raise` method call.
|
||||||
|
def raise?(node)
|
||||||
|
node.is_a?(Crystal::Call) &&
|
||||||
|
node.name == "raise" && node.args.size == 1 && node.obj.nil?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns true if node represents `exit` method call.
|
||||||
|
def exit?(node)
|
||||||
|
node.is_a?(Crystal::Call) &&
|
||||||
|
node.name == "exit" && node.args.size <= 1 && node.obj.nil?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns true if node represents `abort` method call.
|
||||||
|
def abort?(node)
|
||||||
|
node.is_a?(Crystal::Call) &&
|
||||||
|
node.name == "abort" && node.args.size <= 2 && node.obj.nil?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns true if node represents a loop.
|
||||||
|
def loop?(node)
|
||||||
|
case node
|
||||||
|
when Crystal::While, Crystal::Until
|
||||||
|
true
|
||||||
|
when Crystal::Call
|
||||||
|
node.name == "loop" && node.args.size == 0 && node.obj.nil?
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,60 +1,67 @@
|
||||||
|
require "../util"
|
||||||
require "./base_visitor"
|
require "./base_visitor"
|
||||||
|
|
||||||
module Ameba::AST
|
module Ameba::AST
|
||||||
# AST Visitor that traverses all the flow expressions.
|
# AST Visitor that traverses all the flow expressions.
|
||||||
class FlowExpressionVisitor < BaseVisitor
|
class FlowExpressionVisitor < BaseVisitor
|
||||||
@node_stack = Array(Crystal::ASTNode).new
|
include Util
|
||||||
@flow_expression : FlowExpression?
|
|
||||||
|
|
||||||
|
@loop_stack = [] of Crystal::ASTNode
|
||||||
|
|
||||||
|
# Creates a new flow expression visitor.
|
||||||
def initialize(@rule, @source)
|
def initialize(@rule, @source)
|
||||||
@source.ast.accept self
|
@source.ast.accept self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# :nodoc:
|
||||||
def visit(node)
|
def visit(node)
|
||||||
@node_stack.push node
|
if flow_expression?(node, in_loop?)
|
||||||
end
|
@rule.test @source, node, FlowExpression.new(node, in_loop?)
|
||||||
|
|
||||||
def end_visit(node)
|
|
||||||
if @flow_expression.nil?
|
|
||||||
@node_stack.pop unless @node_stack.empty?
|
|
||||||
else
|
|
||||||
@flow_expression = nil
|
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
def visit(node : Crystal::ControlExpression)
|
|
||||||
on_flow_expression_start(node)
|
|
||||||
|
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# :nodoc:
|
||||||
|
def visit(node : Crystal::While)
|
||||||
|
on_loop_started(node)
|
||||||
|
end
|
||||||
|
|
||||||
|
# :nodoc:
|
||||||
|
def visit(node : Crystal::Until)
|
||||||
|
on_loop_started(node)
|
||||||
|
end
|
||||||
|
|
||||||
|
# :nodoc:
|
||||||
def visit(node : Crystal::Call)
|
def visit(node : Crystal::Call)
|
||||||
if raise?(node) || exit?(node) || abort?(node)
|
on_loop_started(node) if loop?(node)
|
||||||
on_flow_expression_start(node)
|
|
||||||
else
|
|
||||||
@node_stack.push node
|
|
||||||
end
|
|
||||||
|
|
||||||
true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private def on_flow_expression_start(node)
|
# :nodoc:
|
||||||
if parent_node = @node_stack.last?
|
def end_visit(node : Crystal::While)
|
||||||
@flow_expression = FlowExpression.new(node, parent_node)
|
on_loop_ended(node)
|
||||||
@rule.test @source, node, @flow_expression
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private def raise?(node)
|
# :nodoc:
|
||||||
node.name == "raise" && node.args.size == 1 && node.obj.nil?
|
def end_visit(node : Crystal::Until)
|
||||||
|
on_loop_ended(node)
|
||||||
end
|
end
|
||||||
|
|
||||||
private def exit?(node)
|
# :nodoc:
|
||||||
node.name == "exit" && node.args.size <= 1 && node.obj.nil?
|
def end_visit(node : Crystal::Call)
|
||||||
|
on_loop_ended(node) if loop?(node)
|
||||||
end
|
end
|
||||||
|
|
||||||
private def abort?(node)
|
private def on_loop_started(node)
|
||||||
node.name == "abort" && node.args.size <= 2 && node.obj.nil?
|
@loop_stack.push(node)
|
||||||
|
end
|
||||||
|
|
||||||
|
private def on_loop_ended(node)
|
||||||
|
@loop_stack.pop?
|
||||||
|
end
|
||||||
|
|
||||||
|
private def in_loop?
|
||||||
|
@loop_stack.any?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -43,6 +43,8 @@ module Ameba::Rule::Lint
|
||||||
# ```
|
# ```
|
||||||
#
|
#
|
||||||
struct UnreachableCode < Base
|
struct UnreachableCode < Base
|
||||||
|
include AST::Util
|
||||||
|
|
||||||
properties do
|
properties do
|
||||||
description "Reports unreachable code"
|
description "Reports unreachable code"
|
||||||
end
|
end
|
||||||
|
@ -54,7 +56,7 @@ module Ameba::Rule::Lint
|
||||||
end
|
end
|
||||||
|
|
||||||
def test(source, node, flow_expression : AST::FlowExpression)
|
def test(source, node, flow_expression : AST::FlowExpression)
|
||||||
if unreachable_node = flow_expression.find_unreachable_node
|
if unreachable_node = flow_expression.unreachable_nodes.first?
|
||||||
issue_for unreachable_node, MSG
|
issue_for unreachable_node, MSG
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue