Skip to content

Commit

Permalink
Add Lint/Unused{Local,Instance,Class}VariableAccess (#552)
Browse files Browse the repository at this point in the history
* ImplicitReturnVisitor keeps track of whether the current node is in a macro or not

Fixes for ImplicitReturnVisitor macro implicit returns

* Add `Lint/UnusedLocalVariableAccess`

* Add `Lint/UnusedInstanceVariableAccess`

* Add `Lint/UnusedClassVariableAccess`

* Update src/ameba/rule/lint/unused_local_variable_access.cr

Co-authored-by: Sijawusz Pur Rahnama <[email protected]>

* Update src/ameba/rule/lint/unused_instance_variable_access.cr

Co-authored-by: Sijawusz Pur Rahnama <[email protected]>

* Fix heredoc interpolations

* Update specs from review

* Apply suggestions from code review

Co-authored-by: Sijawusz Pur Rahnama <[email protected]>

* Update specs, disable unused cvars in macros

* Update spec/ameba/rule/lint/unused_instance_variable_access_spec.cr

Co-authored-by: Sijawusz Pur Rahnama <[email protected]>

* Reorder spec

---------

Co-authored-by: Sijawusz Pur Rahnama <[email protected]>
  • Loading branch information
nobodywasishere and Sija authored Feb 17, 2025
1 parent 3332098 commit 70dc11d
Show file tree
Hide file tree
Showing 10 changed files with 423 additions and 15 deletions.
45 changes: 45 additions & 0 deletions spec/ameba/rule/lint/unused_class_variable_access_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
require "../../../spec_helper"

module Ameba::Rule::Lint
describe UnusedClassVariableAccess do
subject = UnusedClassVariableAccess.new

it "passes if class variables are used for assignment" do
expect_no_issues subject, <<-CRYSTAL
class MyClass
foo = @@ivar
end
CRYSTAL
end

it "passes if an class variable is used as a target in multi-assignment" do
expect_no_issues subject, <<-CRYSTAL
class MyClass
@@foo, @@bar = 1, 2
end
CRYSTAL
end

it "fails if class variables are unused in void context of class" do
expect_issue subject, <<-CRYSTAL
class Actor
@@name : String = "George"
@@name
# ^^^^^^ error: Value from class variable access is unused
end
CRYSTAL
end

it "fails if class variables are unused in void context of method" do
expect_issue subject, <<-'CRYSTAL'
def hello : String
@@name
# ^^^^^^ error: Value from class variable access is unused

"Hello, #{@@name}!"
end
CRYSTAL
end
end
end
64 changes: 64 additions & 0 deletions spec/ameba/rule/lint/unused_instance_variable_access_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
require "../../../spec_helper"

module Ameba::Rule::Lint
describe UnusedInstanceVariableAccess do
subject = UnusedInstanceVariableAccess.new

it "passes if instance variables are used for assignment" do
expect_no_issues subject, <<-CRYSTAL
class MyClass
foo = @ivar
end
CRYSTAL
end

it "passes if an instance variable is used as a target in multi-assignment" do
expect_no_issues subject, <<-CRYSTAL
class MyClass
@foo, @bar = 1, 2
end
CRYSTAL
end

it "fails if instance variables are unused in void context of class" do
expect_issue subject, <<-CRYSTAL
class Actor
@name : String = "George"
@name
# ^^^^^ error: Value from instance variable access is unused
end
CRYSTAL
end

it "fails if instance variables are unused in void context of method" do
expect_issue subject, <<-'CRYSTAL'
def hello : String
@name
# ^^^^^ error: Value from instance variable access is unused

"Hello, #{@name}!"
end
CRYSTAL
end

it "passes if @type is unused within a macro expression" do
expect_no_issues subject, <<-CRYSTAL
def foo
{% @type %}
:bar
end
CRYSTAL
end

it "fails if instance variable is unused within a macro expression" do
expect_issue subject, <<-CRYSTAL
def foo
{% @bar %}
# ^^^^ error: Value from instance variable access is unused
:baz
end
CRYSTAL
end
end
end
69 changes: 69 additions & 0 deletions spec/ameba/rule/lint/unused_local_variable_access_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
require "../../../spec_helper"

module Ameba::Rule::Lint
describe UnusedLocalVariableAccess do
subject = UnusedLocalVariableAccess.new

it "passes if local variables are used in assign" do
expect_no_issues subject, <<-CRYSTAL
foo = 1
foo += 1
foo, bar = 2, 3
CRYSTAL
end

it "passes if a local variable is a call argument" do
expect_no_issues subject, <<-CRYSTAL
foo = 1
puts foo
CRYSTAL
end

it "passes if local variable on left side of a comparison" do
expect_no_issues subject, <<-CRYSTAL
def hello
foo = 1
foo || (puts "foo is falsey")
foo
end
CRYSTAL
end

it "passes if skip_file is used in a macro" do
expect_no_issues subject, <<-CRYSTAL
{% skip_file %}
CRYSTAL
end

it "passes if debug is used in a macro" do
expect_no_issues subject, <<-CRYSTAL
{% debug %}
CRYSTAL
end

it "fails if a local variable is in a void context" do
expect_issue subject, <<-CRYSTAL
foo = 1
begin
foo
# ^^^ error: Value from local variable access is unused
puts foo
end
CRYSTAL
end

it "fails if a parameter is in a void context" do
expect_issue subject, <<-CRYSTAL
def foo(bar)
if bar > 0
bar
# ^^^ error: Value from local variable access is unused
end
nil
end
CRYSTAL
end
end
end
50 changes: 39 additions & 11 deletions src/ameba/ast/visitors/implicit_return_visitor.cr
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ module Ameba::AST
class ImplicitReturnVisitor < BaseVisitor
# When greater than zero, indicates the current node's return value is used
@stack : Int32 = 0
@in_macro : Bool = false

# The stack is swapped out here as `Crystal::Expressions` are isolated from
# their parents scope. Only the last line in an expressions node can be
Expand Down Expand Up @@ -39,7 +40,9 @@ module Ameba::AST
def visit(node : Crystal::BinaryOp) : Bool
report_implicit_return(node)

if node.right.is_a?(Crystal::Call)
if node.right.is_a?(Crystal::Call) ||
node.right.is_a?(Crystal::Expressions) ||
node.right.is_a?(Crystal::ControlExpression)
incr_stack { node.left.accept(self) }
else
node.left.accept(self)
Expand Down Expand Up @@ -91,7 +94,6 @@ module Ameba::AST
def visit(node : Crystal::MultiAssign) : Bool
report_implicit_return(node)

node.targets.each &.accept(self)
incr_stack { node.values.each &.accept(self) }

false
Expand Down Expand Up @@ -147,6 +149,9 @@ module Ameba::AST
node.args.each &.accept(self)
node.double_splat.try &.accept(self)
node.block_arg.try &.accept(self)
end

swap_stack do
node.body.accept(self)
end

Expand Down Expand Up @@ -280,6 +285,14 @@ module Ameba::AST
false
end

def visit(node : Crystal::Block) : Bool
report_implicit_return(node)

node.body.accept(self)

false
end

def visit(node : Crystal::ControlExpression) : Bool
report_implicit_return(node)

Expand Down Expand Up @@ -329,10 +342,12 @@ module Ameba::AST
def visit(node : Crystal::MacroExpression) : Bool
report_implicit_return(node)

if node.output?
incr_stack { node.exp.accept(self) }
else
node.exp.accept(self)
in_macro do
if node.output?
incr_stack { node.exp.accept(self) }
else
swap_stack { node.exp.accept(self) }
end
end

false
Expand All @@ -341,17 +356,23 @@ module Ameba::AST
def visit(node : Crystal::MacroIf) : Bool
report_implicit_return(node)

incr_stack { node.cond.accept(self) }
node.then.accept(self)
node.else.accept(self)
in_macro do
swap_stack do
incr_stack { node.cond.accept(self) }
node.then.accept(self)
node.else.accept(self)
end
end

false
end

def visit(node : Crystal::MacroFor) : Bool
report_implicit_return(node)

node.body.accept(self)
in_macro do
swap_stack { node.body.accept(self) }
end

false
end
Expand Down Expand Up @@ -399,7 +420,7 @@ module Ameba::AST
end

private def report_implicit_return(node) : Nil
@rule.test(@source, node) unless @stack.positive?
@rule.test(@source, node, @in_macro) unless @stack.positive?
end

# Indicates that any nodes visited within the block are captured / used.
Expand All @@ -415,5 +436,12 @@ module Ameba::AST
yield old_stack
@stack = old_stack
end

private def in_macro(&) : Nil
old_value = @in_macro
@in_macro = true
yield
@in_macro = old_value
end
end
end
61 changes: 61 additions & 0 deletions src/ameba/rule/lint/unused_class_variable_access.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
module Ameba::Rule::Lint
# A rule that disallows unused class variable access.
#
# For example, this is considered invalid:
#
# ```
# class MyClass
# @@my_var : String = "hello"
#
# @@my_var
#
# def hello : String
# @@my_var
#
# "hello, world!"
# end
# end
# ```
#
# And these are considered valid:
#
# ```
# class MyClass
# @@my_var : String = "hello"
#
# @@my_other_var = @@my_var
#
# def hello : String
# return @@my_var if @@my_var == "hello"
#
# "hello, world!"
# end
# end
# ```
#
# YAML configuration example:
#
# ```
# Lint/UnusedClassVariableAccess:
# Enabled: true
# ```
class UnusedClassVariableAccess < Base
properties do
since_version "1.7.0"
description "Disallows unused access to class variables"
end

MSG = "Value from class variable access is unused"

def test(source : Source)
AST::ImplicitReturnVisitor.new(self, source)
end

def test(source, node : Crystal::ClassVar, in_macro : Bool)
# Class variables aren't supported in macros
return if in_macro

issue_for node, MSG
end
end
end
2 changes: 1 addition & 1 deletion src/ameba/rule/lint/unused_comparison.cr
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ module Ameba::Rule::Lint
AST::ImplicitReturnVisitor.new(self, source)
end

def test(source, node : Crystal::Call)
def test(source, node : Crystal::Call, in_macro : Bool)
if node.name.in?(COMPARISON_OPERATORS) && node.args.size == 1
issue_for node, MSG
end
Expand Down
4 changes: 2 additions & 2 deletions src/ameba/rule/lint/unused_generic_or_union.cr
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@ module Ameba::Rule::Lint
AST::ImplicitReturnVisitor.new(self, source)
end

def test(source, node : Crystal::Call)
def test(source, node : Crystal::Call, in_macro : Bool)
issue_for node, MSG_UNION if path_or_generic_union?(node)
end

def test(source, node : Crystal::Generic)
def test(source, node : Crystal::Generic, in_macro : Bool)
issue_for node, MSG_GENERIC
end

Expand Down
Loading

0 comments on commit 70dc11d

Please sign in to comment.