Variable scope & useless assignments (#41)

* AST::Visitor -> AST::NodeVisitor

* Scope & ScopeVisitor

* Useless assignment rule

* Instance vars and useless assignments

* Multiple assigns one by one

* Support outer scope

* Variable used in the useless assignment

* Support OpAssign & MultiAssign

* Captured by block

* Variable, Assignment, Reference & Refactoring

* Variable has references, Assignment can be referenced

* Branch entity

* Handle useless assignments in branches

* Handle assignments in a loop

* Handle branch equality

* Handle special var `$?` assignment

* Improve captured by block stuff

* Avoid assignments in property definitions

(UselessAssign rule reports an warning)

* Support MacroIf and MacroFor branches

* Handle assignments with shadowed vars in inner scopes

* Add method arguments as scope variables

* Handle case if branch is blank

* Top level scope

* Handle case when branch is nop?
This commit is contained in:
V. Elenhaupt 2018-05-03 18:57:47 +03:00 committed by GitHub
parent 60c1b86890
commit 6475c2bb25
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 2521 additions and 124 deletions

View file

@ -0,0 +1,364 @@
require "../../spec_helper"
private def branch_of_assign_in_def(source)
nodes = as_nodes source
Ameba::AST::Branch.of(nodes.assign_nodes.first, nodes.def_nodes.first)
end
module Ameba::AST
describe Branch do
describe ".of" do
context "Crystal::If" do
it "constructs a branch in If.cond" do
branch = branch_of_assign_in_def %(
def method
if a = get_something # --> Crystal::Assign
puts a
end
end
)
branch.to_s.should eq "a = get_something"
end
it "constructs a branch in If.then" do
branch = branch_of_assign_in_def %(
def method
if true
a = 2 # --> Crystal::Assign
end
end
)
branch.to_s.should eq "a = 2"
end
it "constructs a branch in If.else" do
branch = branch_of_assign_in_def %(
def method
if true
nil
else
a = 2 # --> Crystal::Assign
end
end
)
branch.to_s.should eq "a = 2"
end
it "constructs a branch in inline If" do
branch = branch_of_assign_in_def %(
def method(a)
a = 0 if a == 2 # --> Crystal::Assign
end
)
branch.to_s.should eq "a = 0"
end
end
context "Crystal::Unless" do
it "constructs a branch in Unless.cond" do
branch = branch_of_assign_in_def %(
def method
unless a = get_something # --> Crystal::Assign
puts a
end
end
)
branch.to_s.should eq "a = get_something"
end
it "constructs a branch in Unless.then" do
branch = branch_of_assign_in_def %(
def method
unless true
a = 2 # --> Crystal::Assign
end
end
)
branch.to_s.should eq "a = 2"
end
it "constructs a new branch in Unless.else" do
branch = branch_of_assign_in_def %(
def method
unless true
nil
else
a = 2 # --> Crystal::Assign
end
end
)
branch.to_s.should eq "a = 2"
end
it "constructs a branch in inline Unless" do
branch = branch_of_assign_in_def %(
def method(a)
(a = 0; b = 3) unless a == 2 # --> Crystal::Expressions
end
)
branch.to_s.should eq "(a = 0\nb = 3)"
end
end
context "Crystal::BinaryOp" do
it "constructs a branch in left node" do
branch = branch_of_assign_in_def %(
def method(a)
(a = 2) && do_something
end
)
branch.to_s.should eq "(a = 2)"
end
it "constructs a branch in right node" do
branch = branch_of_assign_in_def %(
def method(a)
do_something || (a = 0)
end
)
branch.to_s.should eq "(a = 0)"
end
end
context "Crystal::Case" do
# FIXME:
# https://github.com/crystal-lang/crystal/pull/6032
# it "constructs a branch in cond" do
# branch = branch_of_assign_in_def %(
# def method(a)
# case (a = 2)
# when true then nil
# end
# end
# )
# branch.to_s.should eq "(a = 2)"
# end
it "constructs a branch in when" do
branch = branch_of_assign_in_def %(
def method(a)
case a
when a = 3 then nil
end
end
)
branch.to_s.should eq "when a = 3\n nil\n"
end
it "constructs a branch in else" do
branch = branch_of_assign_in_def %(
def method(a)
case a
when true then nil
else a = 4
end
end
)
branch.to_s.should eq "a = 4"
end
end
context "Crystal::While" do
it "constructs a branch in cond" do
branch = branch_of_assign_in_def %(
def method(a)
while a = 1
nil
end
end
)
branch.to_s.should eq "a = 1"
end
it "constructs a branch in body" do
branch = branch_of_assign_in_def %(
def method(a)
while true
b = (a = 1)
end
end
)
branch.to_s.should eq "b = (a = 1)"
end
end
context "Crystal::Until" do
it "constructs a branch in cond" do
branch = branch_of_assign_in_def %(
def method(a)
until a = 1
nil
end
end
)
branch.to_s.should eq "a = 1"
end
it "constructs a branch in body" do
branch = branch_of_assign_in_def %(
def method(a)
until false
b = (a = 1)
end
end
)
branch.to_s.should eq "b = (a = 1)"
end
end
context "Crystal::ExceptionHandler" do
it "constructs a branch in body" do
branch = branch_of_assign_in_def %(
def method(a)
a = 1
rescue
nil
end
)
branch.to_s.should eq "a = 1"
end
it "constructs a branch in a rescue" do
branch = branch_of_assign_in_def %(
def method(a)
rescue
a = 1
end
)
branch.to_s.should eq "a = 1"
end
it "constructs a branch in else" do
branch = branch_of_assign_in_def %(
def method(a)
rescue
else
a = 1
end
)
branch.to_s.should eq "a = 1"
end
it "constructs a branch in ensure" do
branch = branch_of_assign_in_def %(
def method(a)
rescue
ensure
a = 1
end
)
branch.to_s.should eq "a = 1"
end
end
context "Crystal::MacroIf" do
it "constructs a branch in cond" do
branch = branch_of_assign_in_def %(
def method(a)
{% if a = 2 %}
{% end %}
end
)
branch.to_s.should eq "a = 2"
end
it "constructs a branch in then" do
nodes = as_nodes %(
def method(a)
{% if true %}
a = 2
{% end %}
end
)
branch = Branch.of(nodes.macro_literal_nodes.first, nodes.def_nodes.first)
branch.to_s.strip.should eq "a = 2"
end
end
context "Crystal::MacroFor" do
it "constructs a branch in body" do
nodes = as_nodes %(
def method(a)
{% for x in [1, 2, 3] %}
a = 2
{% end %}
end
)
branch = Branch.of(nodes.macro_literal_nodes.first, nodes.def_nodes.first)
branch.to_s.strip.should eq "a = 2"
end
end
it "returns nil if branch does not exist" do
nodes = as_nodes %(
def method
a = 2
end
)
branch = Branch.of(nodes.assign_nodes.first, nodes.def_nodes.first)
branch.should be_nil
end
end
describe "#initialize" do
it "creates new branch" do
nodes = as_nodes %(
if true
a = 2
end
)
branchable = Branchable.new nodes.if_nodes.first
branch = Branch.new nodes.assign_nodes.first, branchable
branch.node.should_not be_nil
end
end
describe "delegation" do
it "delegates to_s to node" do
nodes = as_nodes %(
if true
a = 2
end
)
branchable = Branchable.new nodes.if_nodes.first
branch = Branch.new nodes.assign_nodes.first, branchable
branch.to_s.should eq branch.node.to_s
end
it "delegates location to node" do
nodes = as_nodes %(
if true
a = 2
end
)
branchable = Branchable.new nodes.if_nodes.first
branch = Branch.new nodes.assign_nodes.first, branchable
branch.location.should eq branch.node.location
end
end
describe "#in_loop?" do
it "returns true if branch is in a loop" do
nodes = as_nodes %(
while true
a = 1
end
)
branchable = Branchable.new nodes.while_nodes.first
branch = Branch.new nodes.assign_nodes.first, branchable
branch.in_loop?.should be_true
end
it "returns false if branch is not in a loop" do
nodes = as_nodes %(
if a > 2
a = 1
end
)
branchable = Branchable.new nodes.if_nodes.first
branch = Branch.new nodes.assign_nodes.first, branchable
branch.in_loop?.should be_false
end
end
end
end

View file

@ -0,0 +1,48 @@
require "../../spec_helper"
module Ameba::AST
describe Branchable do
describe "#initialize" do
it "creates a new branchable" do
branchable = Branchable.new as_node %(a = 2 if true)
branchable.node.should_not be_nil
end
end
describe "delegation" do
it "delegates to_s to @node" do
node = as_node %(a = 2 if true)
branchable = Branchable.new node
branchable.to_s.should eq node.to_s
end
it "delegates location to @node" do
node = as_node %(a = 2 if true)
branchable = Branchable.new node
branchable.location.should eq node.location
end
end
describe "#loop?" do
it "returns true if it is a while loop" do
branchable = Branchable.new as_node %(while true; a = 2; end)
branchable.loop?.should be_true
end
it "returns true if it is the until loop" do
branchable = Branchable.new as_node %(until false; a = 2; end)
branchable.loop?.should be_true
end
it "returns true if it is loop" do
branchable = Branchable.new as_node %(loop {})
branchable.loop?.should be_true
end
it "returns false otherwise" do
branchable = Branchable.new as_node %(a = 2 if true)
branchable.loop?.should be_false
end
end
end
end

View file

@ -0,0 +1,72 @@
require "../../spec_helper"
module Ameba::AST
describe Scope do
describe "#initialize" do
source = "a = 2"
it "assigns outer scope" do
root = Scope.new as_node(source)
child = Scope.new as_node(source), root
child.outer_scope.should_not be_nil
end
it "assigns node" do
scope = Scope.new as_node(source)
scope.node.should_not be_nil
end
end
end
describe "#add_variable" do
it "adds a new variable to the scope" do
scope = Scope.new as_node("")
scope.add_variable(Crystal::Var.new "foo")
scope.variables.any?.should be_true
end
end
describe "#find_variable" do
it "returns the variable in the scope by name" do
scope = Scope.new as_node("foo = 1")
scope.add_variable Crystal::Var.new "foo"
scope.find_variable("foo").should_not be_nil
end
it "returns nil if variable not exist in this scope" do
scope = Scope.new as_node("foo = 1")
scope.find_variable("bar").should be_nil
end
end
describe "#assign_variable" do
it "creates a new assignment" do
scope = Scope.new as_node("foo = 1")
scope.add_variable Crystal::Var.new "foo"
scope.assign_variable(Crystal::Var.new "foo")
scope.find_variable("foo").not_nil!.assignments.size.should eq 1
end
it "does not create the assignment if variable is wrong" do
scope = Scope.new as_node("foo = 1")
scope.add_variable Crystal::Var.new "foo"
scope.assign_variable(Crystal::Var.new "bar")
scope.find_variable("foo").not_nil!.assignments.size.should eq 0
end
end
describe "#block?" do
it "returns true if Crystal::Block" do
nodes = as_nodes %(
3.times {}
)
scope = Scope.new nodes.block_nodes.first
scope.block?.should be_true
end
it "returns false otherwise" do
scope = Scope.new as_node "a = 1"
scope.block?.should be_false
end
end
end

View file

@ -0,0 +1,90 @@
require "../../../spec_helper"
module Ameba::AST
describe Assignment do
node = Crystal::NilLiteral.new
scope = Scope.new as_node "foo = 1"
variable = Variable.new(Crystal::Var.new("foo"), scope)
describe "#initialize" do
it "creates a new assignment with node and var" do
assignment = Assignment.new(node, variable)
assignment.node.should_not be_nil
end
end
describe "#reference=" do
it "creates a new reference" do
assignment = Assignment.new(node, variable)
assignment.referenced = true
assignment.referenced?.should be_true
end
end
describe "delegation" do
it "delegates location" do
assignment = Assignment.new(node, variable)
assignment.location.should eq node.location
end
it "delegates to_s" do
assignment = Assignment.new(node, variable)
assignment.to_s.should eq node.to_s
end
it "delegates scope" do
assignment = Assignment.new(node, variable)
assignment.scope.should eq variable.scope
end
end
describe "#branch" do
it "returns the branch of the assignment" do
nodes = as_nodes %(
def method(a)
if a
a = 3 # --> Crystal::Expressions
puts a
end
end
)
scope = Scope.new nodes.def_nodes.first
variable = Variable.new(nodes.var_nodes.first, scope)
assignment = Assignment.new(nodes.assign_nodes.first, variable)
assignment.branch.should_not be_nil
assignment.branch.not_nil!.node.class.should eq Crystal::Expressions
end
it "returns inner branch" do
nodes = as_nodes %(
def method(a, b)
if a
if b
a = 3 # --> Crystal::Assign
end
end
end
)
scope = Scope.new nodes.def_nodes.first
variable = Variable.new(nodes.var_nodes.first, scope)
assignment = Assignment.new(nodes.assign_nodes.first, variable)
assignment.branch.should_not be_nil
assignment.branch.not_nil!.node.class.should eq Crystal::Assign
end
it "returns nil if assignment does not have a branch" do
nodes = as_nodes %(
def method(a)
a = 2
end
)
scope = Scope.new nodes.def_nodes.first
variable = Variable.new(nodes.var_nodes.first, scope)
assignment = Assignment.new(nodes.assign_nodes.first, variable)
assignment.branch.should be_nil
end
end
end
end

View file

@ -0,0 +1,10 @@
require "../../../spec_helper"
module Ameba::AST
describe Reference do
it "is derived from a Variable" do
node = Crystal::Var.new "foo"
Reference.new(node, Scope.new as_node "foo = 1").is_a?(Variable).should be_true
end
end
end

View file

@ -0,0 +1,154 @@
require "../../../spec_helper"
module Ameba::AST
describe Variable do
var_node = Crystal::Var.new("foo")
scope = Scope.new as_node "foo = 1"
describe "#initialize" do
it "creates a new variable" do
variable = Variable.new(var_node, scope)
variable.node.should_not be_nil
end
end
describe "delegation" do
it "delegates location" do
variable = Variable.new(var_node, scope)
variable.location.should eq var_node.location
end
it "delegates name" do
variable = Variable.new(var_node, scope)
variable.name.should eq var_node.name
end
it "delegates to_s" do
variable = Variable.new(var_node, scope)
variable.to_s.should eq var_node.to_s
end
end
describe "#special?" do
it "returns truthy if it is a special `$?` var" do
variable = Variable.new Crystal::Var.new("$?"), scope
variable.special?.should be_truthy
end
it "returns falsey otherwise" do
variable = Variable.new Crystal::Var.new("a"), scope
variable.special?.should be_falsey
end
end
describe "#assign" do
assign_node = as_node("foo=1")
it "assigns the variable (creates a new assignment)" do
variable = Variable.new(var_node, scope)
variable.assign(assign_node)
variable.assignments.any?.should be_true
end
it "can create multiple assignments" do
variable = Variable.new(var_node, scope)
variable.assign(assign_node)
variable.assign(assign_node)
variable.assignments.size.should eq 2
end
end
describe "#reference" do
it "references the existed assignment" do
variable = Variable.new(var_node, scope)
variable.assign(as_node "foo=1")
variable.reference(var_node, scope)
variable.references.any?.should be_true
end
end
describe "#captured_by_block?" do
it "returns truthy if the variable is captured by block" do
nodes = as_nodes %(
def method
a = 2
3.times { |i| a = a + i }
end
)
scope = Scope.new nodes.def_nodes.first
var_node = nodes.var_nodes.first
scope.add_variable var_node
scope.inner_scopes << Scope.new(nodes.block_nodes.first, scope)
variable = Variable.new(var_node, scope)
variable.reference nodes.var_nodes.last, scope.inner_scopes.last
variable.captured_by_block?.should be_truthy
end
it "returns falsey if the variable is not captured by the block" do
scope = Scope.new as_node %(
def method
a = 1
end
)
scope.add_variable Crystal::Var.new "a"
variable = scope.variables.first
variable.captured_by_block?.should be_falsey
end
end
describe "#target_of?" do
it "returns true if the variable is a target of Crystal::Assign node" do
assign_node = as_nodes("foo=1").assign_nodes.last
variable = Variable.new assign_node.target.as(Crystal::Var), scope
variable.target_of?(assign_node).should be_true
end
it "returns true if the variable is a target of Crystal::OpAssign node" do
assign_node = as_nodes("foo=1;foo+=1").op_assign_nodes.last
variable = Variable.new assign_node.target.as(Crystal::Var), scope
variable.target_of?(assign_node).should be_true
end
it "returns true if the variable is a target of Crystal::MultiAssign node" do
assign_node = as_nodes("a,b,c={1,2,3}").multi_assign_nodes.last
assign_node.targets.size.should_not eq 0
assign_node.targets.each do |target|
variable = Variable.new target.as(Crystal::Var), scope
variable.target_of?(assign_node).should be_true
end
end
it "returns false if the node is not assign" do
variable = Variable.new(Crystal::Var.new("v"), scope)
variable.target_of?(as_node "nil").should be_false
end
it "returns false if the variable is not a target of the assign" do
variable = Variable.new(Crystal::Var.new("foo"), scope)
variable.target_of?(as_node("bar = 1")).should be_false
end
end
describe "#eql?" do
var = Crystal::Var.new("foo").at(Crystal::Location.new(nil, 1, 2))
variable = Variable.new var, scope
it "is false if node is not a Crystal::Var" do
variable.eql?(as_node("nil")).should be_false
end
it "is false if node name is different" do
variable.eql?(Crystal::Var.new "bar").should be_false
end
it "is false if node has a different location" do
variable.eql?(Crystal::Var.new "foo").should be_false
end
it "is true otherwise" do
variable.eql?(variable.node).should be_true
end
end
end
end

View file

@ -1,14 +1,14 @@
require "../../spec_helper"
require "../../../spec_helper"
module Ameba::AST
rule = DummyRule.new
source = Source.new ""
describe "Traverse" do
describe NodeVisitor do
{% for name in NODES %}
describe "{{name}}" do
it "allow to visit {{name}} node" do
visitor = Visitor.new rule, source
visitor = NodeVisitor.new rule, source
nodes = Crystal::Parser.new("").parse
nodes.accept visitor
end

View file

@ -0,0 +1,58 @@
require "../../../spec_helper"
module Ameba::AST
describe ScopeVisitor do
it "creates a scope for the def" do
rule = ScopeRule.new
ScopeVisitor.new rule, Source.new %(
def method
end
)
rule.scopes.size.should eq 1
end
it "creates a scope for the proc" do
rule = ScopeRule.new
ScopeVisitor.new rule, Source.new %(
-> {}
)
rule.scopes.size.should eq 1
end
it "creates a scope for the block" do
rule = ScopeRule.new
ScopeVisitor.new rule, Source.new %(
3.times {}
)
rule.scopes.size.should eq 2
end
context "inner scopes" do
it "creates scope for block inside def" do
rule = ScopeRule.new
ScopeVisitor.new rule, Source.new %(
def method
3.times {}
end
)
rule.scopes.size.should eq 2
rule.scopes.last.outer_scope.should_not be_nil
rule.scopes.first.outer_scope.should eq rule.scopes.last
end
it "creates scope for block inside block" do
rule = ScopeRule.new
ScopeVisitor.new rule, Source.new %(
3.times do
2.times {}
end
)
rule.scopes.size.should eq 3
inner_block = rule.scopes.first
outer_block = rule.scopes.last
inner_block.outer_scope.should_not eq outer_block
outer_block.outer_scope.should be_nil
end
end
end
end

View file

@ -35,7 +35,7 @@ module Ameba::Rule
subject.catch(s).should be_valid
end
it "failes if there is unneeded directive" do
it "fails if there is unneeded directive" do
s = Source.new %Q(
# ameba:disable #{NamedRule.name}
a = 1

View file

@ -0,0 +1,813 @@
require "../../spec_helper"
module Ameba::Rule
describe UselessAssign do
subject = UselessAssign.new
it "does not report used assigments" do
s = Source.new %(
def method
a = 2
a
end
)
subject.catch(s).should be_valid
end
it "reports a useless assignment in a method" do
s = Source.new %(
def method
a = 2
end
)
subject.catch(s).should_not be_valid
end
it "reports a useless assignment in a proc" do
s = Source.new %(
->() {
a = 2
}
)
subject.catch(s).should_not be_valid
end
it "reports a useless assignment in a block" do
s = Source.new %(
def method
3.times do
a = 1
end
end
)
subject.catch(s).should_not be_valid
end
it "reports a useless assignment in a proc inside def" do
s = Source.new %(
def method
->() {
a = 2
}
end
)
subject.catch(s).should_not be_valid
end
it "reports a useless assignment in a proc inside a block" do
s = Source.new %(
def method
3.times do
->() {
a = 2
}
end
end
)
subject.catch(s).should_not be_valid
end
it "reports rule, position and a message" do
s = Source.new %(
def method
a = 2
end
), "source.cr"
subject.catch(s).should_not be_valid
error = s.errors.first
error.rule.should_not be_nil
error.location.to_s.should eq "source.cr:3:11"
error.message.should eq "Useless assignment to variable `a`"
end
it "does not report useless assignment of instance var" do
s = Source.new %(
class Cls
def initialize(@name)
end
end
)
subject.catch(s).should be_valid
end
it "does not report if assignment used in the inner block scope" do
s = Source.new %(
def method
var = true
3.times { var = false }
end
)
subject.catch(s).should be_valid
end
it "reports if assigned is not referenced in the inner block scope" do
s = Source.new %(
def method
var = true
3.times {}
end
)
subject.catch(s).should_not be_valid
end
it "doesn't report if assignment in referenced in inner block" do
s = Source.new %(
def method
two = true
3.times do
mutex.synchronize do
two = 2
end
end
two.should be_true
end
)
subject.catch(s).should be_valid
end
it "reports if first assignment is useless" do
s = Source.new %(
def method
var = true
var = false
var
end
)
subject.catch(s).should_not be_valid
s.errors.first.location.to_s.should eq ":3:11"
end
it "reports if variable reassigned and not used" do
s = Source.new %(
def method
var = true
var = false
end
)
subject.catch(s).should_not be_valid
end
it "does not report if variable used in a condition" do
s = Source.new %(
def method
a = 1
if a
nil
end
end
)
subject.catch(s).should be_valid
end
it "reports second assignment as useless" do
s = Source.new %(
def method
a = 1
a = a + 1
end
)
subject.catch(s).should_not be_valid
end
it "does not report if variable is referenced in other assignment" do
s = Source.new %(
def method
if f = get_something
@f = f
end
end
)
subject.catch(s).should be_valid
end
it "does not report if variable is referenced in a setter" do
s = Source.new %(
def method
foo = 2
table[foo] ||= "bar"
end
)
subject.catch(s).should be_valid
end
it "does not report if variable is reassigned but not referenced" do
s = Source.new %(
def method
foo = 1
puts foo
foo = 2
end
)
subject.catch(s).should_not be_valid
end
it "does not report if variable is referenced in a call" do
s = Source.new %(
def method
if f = FORMATTER
@formatter = f.new
end
end
)
subject.catch(s).should be_valid
end
it "does not report if a setter is invoked with operator assignment" do
s = Source.new %(
def method
obj = {} of Symbol => Int32
obj[:name] = 3
end
)
subject.catch(s).should be_valid
end
it "does not report if global var" do
s = Source.new %(
def method
$? = 3
end
)
subject.catch(s).should be_valid
end
it "does not report if assignment is referenced in a proc" do
s = Source.new %(
def method
called = false
->() { called = true }
called
end
)
subject.catch(s).should be_valid
end
it "reports if variable is shadowed in inner scope" do
s = Source.new %(
def method
i = 1
3.times do |i|
i + 1
end
end
)
subject.catch(s).should_not be_valid
end
it "does not report if parameter is referenced after the branch" do
s = Source.new %(
def method(param)
3.times do
param = 3
end
param
end
)
subject.catch(s).should be_valid
end
context "op assigns" do
it "does not report if variable is referenced below the op assign" do
s = Source.new %(
def method
a = 1
a += 1
a
end
)
subject.catch(s).should be_valid
end
it "does not report if variable is referenced in op assign few times" do
s = Source.new %(
def method
a = 1
a += 1
a += 1
a = a + 1
a
end
)
subject.catch(s).should be_valid
end
it "reports if variable is not referenced below the op assign" do
s = Source.new %(
def method
a = 1
a += 1
end
)
subject.catch(s).should_not be_valid
end
it "reports rule, location and a message" do
s = Source.new %(
def method
b = 2
a = 3
a += 1
end
), "source.cr"
subject.catch(s).should_not be_valid
error = s.errors.last
error.rule.should_not be_nil
error.location.to_s.should eq "source.cr:5:13"
error.message.should eq "Useless assignment to variable `a`"
end
end
context "multi assigns" do
it "does not report if all assigns are referenced" do
s = Source.new %(
def method
a, b = {1, 2}
a + b
end
)
subject.catch(s).should be_valid
end
it "reports if one assign is not referenced" do
s = Source.new %(
def method
a, b = {1, 2}
a
end
)
subject.catch(s).should_not be_valid
error = s.errors.first
error.location.to_s.should eq ":3:16"
error.message.should eq "Useless assignment to variable `b`"
end
it "reports if both assigns are reassigned and useless" do
s = Source.new %(
def method
a, b = {1, 2}
a, b = {3, 4}
end
)
subject.catch(s).should_not be_valid
end
it "reports if both assigns are not referenced" do
s = Source.new %(
def method
a, b = {1, 2}
end
)
subject.catch(s).should_not be_valid
error = s.errors.first
error.location.to_s.should eq ":3:13"
error.message.should eq "Useless assignment to variable `a`"
error = s.errors.last
error.location.to_s.should eq ":3:16"
error.message.should eq "Useless assignment to variable `b`"
end
end
context "top level" do
it "reports if assignment is not referenced" do
s = Source.new %(
a = 1
a = 2
)
subject.catch(s).should_not be_valid
s.errors.size.should eq 2
s.errors.first.location.to_s.should eq ":2:11"
s.errors.last.location.to_s.should eq ":3:11"
end
it "doesn't report if assignments are referenced" do
s = Source.new %(
a = 1
a += 1
a
b, c = {1, 2}
b
c
)
subject.catch(s).should be_valid
end
it "doesn't report if assignment is captured by block" do
s = Source.new %(
a = 1
3.times do
a = 2
end
)
subject.catch(s).should be_valid
end
end
context "branching" do
context "if-then-else" do
it "doesn't report if assignment is consumed by branches" do
s = Source.new %(
def method
a = 0
if something
a = 1
else
a = 2
end
a
end
)
subject.catch(s).should be_valid
end
it "doesn't report if assignment is in one branch" do
s = Source.new %(
def method
a = 0
if something
a = 1
else
nil
end
a
end
)
subject.catch(s).should be_valid
end
it "doesn't report if assignment is in one line branch" do
s = Source.new %(
def method
a = 0
a = 1 if something
a
end
)
subject.catch(s).should be_valid
end
it "reports if assignment is useless in the branch" do
s = Source.new %(
def method(a)
if a
a = 2
end
end
)
subject.catch(s).should_not be_valid
end
it "reports if only last assignment is referenced in a branch" do
s = Source.new %(
def method(a)
a = 1
if a
a = 2
a = 3
end
a
end
)
subject.catch(s).should_not be_valid
s.errors.size.should eq 1
s.errors.first.location.to_s.should eq ":5:17"
end
it "does not report of assignments are referenced in all branches" do
s = Source.new %(
def method
if matches
matches = owner.lookup_matches signature
else
matches = owner.lookup_matches signature
end
matches
end
)
subject.catch(s).should be_valid
end
it "does not report referenced assignments in inner branches" do
s = Source.new %(
def method
has_newline = false
if something
do_something unless false
has_newline = false
else
do_something if true
has_newline = true
end
has_newline
end
)
subject.catch(s).should be_valid
end
end
context "unless-then-else" do
it "doesn't report if assignment is consumed by branches" do
s = Source.new %(
def method
a = 0
unless something
a = 1
else
a = 2
end
a
end
)
subject.catch(s).should be_valid
end
it "reports if there is a useless assignment in a branch" do
s = Source.new %(
def method
a = 0
unless something
a = 1
a = 2
else
a = 2
end
a
end
)
subject.catch(s).should_not be_valid
s.errors.size.should eq 1
s.errors.first.location.to_s.should eq ":5:17"
end
end
context "case" do
it "does not report if assignment is referenced" do
s = Source.new %(
def method(a)
case a
when /foo/
a = 1
when /bar/
a = 2
end
puts a
end
)
subject.catch(s).should be_valid
end
it "reports if assignment is useless" do
s = Source.new %(
def method(a)
case a
when /foo/
a = 1
when /bar/
a = 2
end
end
)
subject.catch(s).should_not be_valid
s.errors.size.should eq 2
s.errors.first.location.to_s.should eq ":5:17"
s.errors.last.location.to_s.should eq ":7:17"
end
end
context "binary operator" do
it "does not report if assignment is referenced" do
s = Source.new %(
def method(a)
(a = 1) && (b = 1)
a + b
end
)
subject.catch(s).should be_valid
end
it "reports if assignment is useless" do
s = Source.new %(
def method(a)
(a = 1) || (b = 1)
a
end
)
subject.catch(s).should_not be_valid
s.errors.size.should eq 1
s.errors.first.location.to_s.should eq ":3:27"
end
end
context "while" do
it "does not report if assignment is referenced" do
s = Source.new %(
def method(a)
while a < 10
a = a + 1
end
a
end
)
subject.catch(s).should be_valid
end
it "reports if assignment is useless" do
s = Source.new %(
def method(a)
while a < 10
b = a
end
end
)
subject.catch(s).should_not be_valid
s.errors.size.should eq 1
s.errors.first.location.to_s.should eq ":4:17"
end
it "does not report if assignment is referenced in a loop" do
s = Source.new %(
def method
a = 3
result = 0
while result < 10
result += a
a = a + 1
end
result
end
)
subject.catch(s).should be_valid
end
it "does not report if assignment is referenced as param in a loop" do
s = Source.new %(
def method(a)
result = 0
while result < 10
result += a
a = a + 1
end
result
end
)
subject.catch(s).should be_valid
end
it "does not report if assignment is referenced in loop and inner branch" do
s = Source.new %(
def method(a)
result = 0
while result < 10
result += a
if result > 0
a = a + 1
else
a = 3
end
end
result
end
)
subject.catch(s).should be_valid
end
it "works properly if there is branch with blank node" do
s = Source.new %(
def visit
count = 0
while true
break if count == 1
case something
when :any
else
:anything_else
end
count += 1
end
end
)
subject.catch(s).should be_valid
end
end
context "until" do
it "does not report if assignment is referenced" do
s = Source.new %(
def method(a)
until a > 10
a = a + 1
end
a
end
)
subject.catch(s).should be_valid
end
it "reports if assignment is useless" do
s = Source.new %(
def method(a)
until a > 10
b = a + 1
end
end
)
subject.catch(s).should_not be_valid
s.errors.size.should eq 1
s.errors.first.location.to_s.should eq ":4:17"
end
end
context "exception handler" do
it "does not report if assignment is referenced in body" do
s = Source.new %(
def method(a)
a = 2
rescue
a
end
)
subject.catch(s).should be_valid
end
it "doesn't report if assignment is referenced in ensure" do
s = Source.new %(
def method(a)
a = 2
ensure
a
end
)
subject.catch(s).should be_valid
end
it "doesn't report if assignment is referenced in else" do
s = Source.new %(
def method(a)
a = 2
rescue
else
a
end
)
subject.catch(s).should be_valid
end
it "reports if assignment is useless" do
s = Source.new %(
def method(a)
rescue
a = 2
end
)
subject.catch(s).should_not be_valid
s.errors.size.should eq 1
s.errors.first.location.to_s.should eq ":4:15"
end
end
end
context "macro" do
it "doesn't report if assignment is referenced in macro" do
s = Source.new %(
def method
a = 2
{% if flag?(:bits64) %}
a.to_s
{% else %}
a
{% end %}
end
)
subject.catch(s).should be_valid
end
it "doesn't report referenced assignments in macro literal" do
s = Source.new %(
def method
a = 2
{% if flag?(:bits64) %}
a = 3
{% else %}
a = 4
{% end %}
puts a
end
)
subject.catch(s).should be_valid
end
end
end
end