Skip to content

Add Lint/UnusedBlockArgument rule #320

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Dec 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions spec/ameba/rule/lint/unused_argument_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -115,21 +115,21 @@ module Ameba::Rule::Lint
subject.catch(s).should be_valid
end

it "reports if block arg is not used" do
it "doesn't report if block arg is not used" do
s = Source.new %(
def method(&block)
end
)
subject.catch(s).should_not be_valid
subject.catch(s).should be_valid
end

it "reports if unused and there is yield" do
it "doesn't report if unused and there is yield" do
s = Source.new %(
def method(&block)
yield 1
end
)
subject.catch(s).should_not be_valid
subject.catch(s).should be_valid
end

it "doesn't report if it's an anonymous block" do
Expand Down
110 changes: 110 additions & 0 deletions spec/ameba/rule/lint/unused_block_argument_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
require "../../../spec_helper"

module Ameba::Rule::Lint
subject = UnusedBlockArgument.new

describe UnusedBlockArgument do
it "doesn't report if it is an instance var argument" do
s = Source.new %(
class A
def initialize(&@callback)
end
end
)
subject.catch(s).should be_valid
end

it "doesn't report if anonymous" do
s = Source.new %(
def method(a, b, c, &)
end
)
subject.catch(s).should be_valid
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't we use that newer matcher expect_no_issues/expect_issue instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, we should but I was too lazy to rewrite those, xmas & all that jazz, sorry!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are still many specs to rewrite anyway...

end

it "doesn't report if argument name starts with a `_`" do
s = Source.new %(
def method(a, b, c, &_block)
end
)
subject.catch(s).should be_valid
end

it "doesn't report if it is a block and used" do
s = Source.new %(
def method(a, b, c, &block)
block.call
end
)
subject.catch(s).should be_valid
end

it "reports if block arg is not used" do
s = Source.new %(
def method(a, b, c, &block)
end
)
subject.catch(s).should_not be_valid
end

it "reports if unused and there is yield" do
s = Source.new %(
def method(a, b, c, &block)
3.times do |i|
i.try do
yield i
end
end
end
)
subject.catch(s).should_not be_valid
end

it "doesn't report if anonymous and there is yield" do
s = Source.new %(
def method(a, b, c, &)
yield 1
end
)
subject.catch(s).should be_valid
end

it "doesn't report if variable is referenced implicitly" do
s = Source.new %(
class Bar < Foo
def method(a, b, c, &block)
super
end
end
)
subject.catch(s).should be_valid
end

context "super" do
it "reports if variable is not referenced implicitly by super" do
s = Source.new %(
class Bar < Foo
def method(a, b, c, &block)
super a, b, c
end
end
)
subject.catch(s).should_not be_valid
s.issues.first.message.should eq "Unused block argument `block`. If it's necessary, " \
"use `_block` as an argument name to indicate " \
"that it won't be used."
end
end

context "macro" do
it "doesn't report if it is a used macro block argument" do
s = Source.new %(
macro my_macro(&block)
{% block %}
end
)
subject.catch(s).should be_valid
end
end
end
end
11 changes: 11 additions & 0 deletions src/ameba/ast/scope.cr
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ module Ameba::AST
# Represents a context of the local variable visibility.
# This is where the local variables belong to.
class Scope
# Whether the scope yields.
setter yields = false

# Link to local variables
getter variables = [] of Variable

Expand Down Expand Up @@ -143,6 +146,14 @@ module Ameba::AST
end || variable.used_in_macro?
end

# Returns `true` if current scope (or any of inner scopes) yields,
# `false` otherwise.
def yields?(check_inner_scopes = true)
return true if @yields
return inner_scopes.any?(&.yields?) if check_inner_scopes
false
end

# Returns `true` if current scope is a def, `false` otherwise.
def def?
node.is_a?(Crystal::Def)
Expand Down
11 changes: 8 additions & 3 deletions src/ameba/ast/visitors/scope_visitor.cr
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ module Ameba::AST
MacroFor,
}

SUPER_NODE_NAME = "super"
RECORD_NODE_NAME = "record"
SPECIAL_NODE_NAMES = %w[super previous_def]
RECORD_NODE_NAME = "record"

@scope_queue = [] of Scope
@current_scope : Scope
Expand Down Expand Up @@ -64,6 +64,11 @@ module Ameba::AST
end
{% end %}

# :nodoc:
def visit(node : Crystal::Yield)
@current_scope.yields = true
end

# :nodoc:
def visit(node : Crystal::Def)
node.name == "->" || on_scope_enter(node)
Expand Down Expand Up @@ -134,7 +139,7 @@ module Ameba::AST
def visit(node : Crystal::Call)
case
when @current_scope.def?
if node.name == SUPER_NODE_NAME && node.args.empty?
if node.name.in?(SPECIAL_NODE_NAMES) && node.args.empty?
@current_scope.arguments.each do |arg|
variable = arg.variable
variable.reference(variable.node, @current_scope).explicit = false
Expand Down
4 changes: 4 additions & 0 deletions src/ameba/rule/lint/unused_argument.cr
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ module Ameba::Rule::Lint
end

def test(source, node : Crystal::Def, scope : AST::Scope)
# `Lint/UnusedBlockArgument` rule covers this case explicitly
if block_arg = node.block_arg
scope.arguments.reject!(&.node.== block_arg)
end
ignore_defs? || find_unused_arguments source, scope
end

Expand Down
62 changes: 62 additions & 0 deletions src/ameba/rule/lint/unused_block_argument.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
module Ameba::Rule::Lint
# A rule that reports unused block arguments.
# For example, this is considered invalid:
#
# ```
# def foo(a, b, &block)
# a + b
# end
#
# def bar(&block)
# yield 42
# end
# ```
#
# and should be written as:
#
# ```
# def foo(a, b, &_block)
# a + b
# end
#
# def bar(&)
# yield 42
# end
# ```
#
# YAML configuration example:
#
# ```
# Lint/UnusedBlockArgument:
# Enabled: true
# ```
class UnusedBlockArgument < Base
properties do
description "Disallows unused block arguments"
end

MSG_UNUSED = "Unused block argument `%1$s`. If it's necessary, use `_%1$s` " \
"as an argument name to indicate that it won't be used."

MSG_YIELDED = "Use `&` as an argument name to indicate that it won't be referenced."

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

def test(source, node : Crystal::Def, scope : AST::Scope)
return unless block_arg = node.block_arg
return unless block_arg = scope.arguments.find(&.node.== block_arg)

return if block_arg.anonymous?
return if scope.references?(block_arg.variable)

if scope.yields?
issue_for block_arg.node, MSG_YIELDED
else
return if block_arg.ignored?
issue_for block_arg.node, MSG_UNUSED % block_arg.name
end
end
end
end
6 changes: 1 addition & 5 deletions src/ameba/rule/style/type_names.cr
Original file line number Diff line number Diff line change
Expand Up @@ -58,16 +58,12 @@ module Ameba::Rule::Style

MSG = "Type name should be camelcased: %s, but it was %s"

private def check_node(source, node)
def test(source, node : Crystal::Alias | Crystal::ClassDef | Crystal::ModuleDef | Crystal::LibDef | Crystal::EnumDef)
name = node.name.to_s
expected = name.camelcase
return if name == expected

issue_for node, MSG % {expected, name}
end

def test(source, node : Crystal::Alias | Crystal::ClassDef | Crystal::ModuleDef | Crystal::LibDef | Crystal::EnumDef)
check_node(source, node)
end
end
end
12 changes: 5 additions & 7 deletions src/ameba/rule/style/variable_names.cr
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,16 @@ module Ameba::Rule::Style

MSG = "Var name should be underscore-cased: %s, not %s"

private def check_node(source, node)
return if (expected = node.name.underscore) == node.name

issue_for node, MSG % {expected, node.name}
end

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

def test(source, node : Crystal::Var | Crystal::InstanceVar | Crystal::ClassVar)
check_node source, node
name = node.name.to_s
expected = name.underscore
return if name == expected

issue_for node, MSG % {expected, name}
end

private class VarVisitor < AST::NodeVisitor
Expand Down