Merge pull request #324 from crystal-ameba/Sija/extend-several-rule-with-corrections

Extend `Lint/UnusedArgument` and `Lint/UnusedBlockArgument` rules with corrections
This commit is contained in:
Sijawusz Pur Rahnama 2022-12-22 18:42:35 +01:00 committed by GitHub
commit 4f8b79ec6b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 190 additions and 149 deletions

View file

@ -6,7 +6,7 @@ module Ameba::Rule::Lint
describe UnusedArgument do describe UnusedArgument do
it "doesn't report if arguments are used" do it "doesn't report if arguments are used" do
s = Source.new %( expect_no_issues subject, <<-CRYSTAL
def method(a, b, c) def method(a, b, c)
a + b + c a + b + c
end end
@ -16,155 +16,158 @@ module Ameba::Rule::Lint
end end
->(i : Int32) { i + 1 } ->(i : Int32) { i + 1 }
) CRYSTAL
subject.catch(s).should be_valid
end end
it "reports if method argument is unused" do it "reports if method argument is unused" do
s = Source.new %( source = expect_issue subject, <<-CRYSTAL
def method(a, b, c) def method(a, b, c)
# ^ error: Unused argument `c`. If it's necessary, use `_c` as an argument name to indicate that it won't be used.
a + b a + b
end end
) CRYSTAL
subject.catch(s).should_not be_valid
s.issues.first.message.should eq "Unused argument `c`. If it's necessary, use `_c` " \ expect_correction source, <<-CRYSTAL
"as an argument name to indicate that it won't be used." def method(a, b, _c)
a + b
end
CRYSTAL
end end
it "reports if block argument is unused" do it "reports if block argument is unused" do
s = Source.new %( source = expect_issue subject, <<-CRYSTAL
[1,2].each_with_index do |a, i| [1, 2].each_with_index do |a, i|
# ^ error: Unused argument `i`. [...]
a a
end end
) CRYSTAL
subject.catch(s).should_not be_valid
s.issues.first.message.should eq "Unused argument `i`. If it's necessary, use `_` " \ expect_correction source, <<-CRYSTAL
"as an argument name to indicate that it won't be used." [1, 2].each_with_index do |a, _|
a
end
CRYSTAL
end end
it "reports if proc argument is unused" do it "reports if proc argument is unused" do
s = Source.new %( source = expect_issue subject, <<-CRYSTAL
-> (a : Int32, b : String) do -> (a : Int32, b : String) do
# ^ error: Unused argument `b`. If it's necessary, use `_b` as an argument name to indicate that it won't be used.
a = a + 1 a = a + 1
end end
) CRYSTAL
subject.catch(s).should_not be_valid
s.issues.first.message.should eq "Unused argument `b`. If it's necessary, use `_b` " \ expect_correction source, <<-CRYSTAL
"as an argument name to indicate that it won't be used." -> (a : Int32, _b : String) do
a = a + 1
end
CRYSTAL
end end
it "reports multiple unused args" do it "reports multiple unused args" do
s = Source.new %( source = expect_issue subject, <<-CRYSTAL
def method(a, b, c) def method(a, b, c)
# ^ error: Unused argument `a`. If it's necessary, use `_a` as an argument name to indicate that it won't be used.
# ^ error: Unused argument `b`. If it's necessary, use `_b` as an argument name to indicate that it won't be used.
# ^ error: Unused argument `c`. If it's necessary, use `_c` as an argument name to indicate that it won't be used.
nil nil
end end
) CRYSTAL
subject.catch(s).should_not be_valid
s.issues[0].message.should eq "Unused argument `a`. If it's necessary, use `_a` " \ expect_correction source, <<-CRYSTAL
"as an argument name to indicate that it won't be used." def method(_a, _b, _c)
s.issues[1].message.should eq "Unused argument `b`. If it's necessary, use `_b` " \ nil
"as an argument name to indicate that it won't be used." end
s.issues[2].message.should eq "Unused argument `c`. If it's necessary, use `_c` " \ CRYSTAL
"as an argument name to indicate that it won't be used."
end end
it "doesn't report if it is an instance var argument" do it "doesn't report if it is an instance var argument" do
s = Source.new %( expect_no_issues subject, <<-CRYSTAL
class A class A
def method(@name) def method(@name)
end end
end end
) CRYSTAL
subject.catch(s).should be_valid
end end
it "doesn't report if a typed argument is used" do it "doesn't report if a typed argument is used" do
s = Source.new %( expect_no_issues subject, <<-CRYSTAL
def method(x : Int32) def method(x : Int32)
3.times do 3.times do
puts x puts x
end end
end end
) CRYSTAL
subject.catch(s).should be_valid
end end
it "doesn't report if an argument with default value is used" do it "doesn't report if an argument with default value is used" do
s = Source.new %( expect_no_issues subject, <<-CRYSTAL
def method(x = 1) def method(x = 1)
puts x puts x
end end
) CRYSTAL
subject.catch(s).should be_valid
end end
it "doesn't report if argument starts with a _" do it "doesn't report if argument starts with a _" do
s = Source.new %( expect_no_issues subject, <<-CRYSTAL
def method(_x) def method(_x)
end end
) CRYSTAL
subject.catch(s).should be_valid
end end
it "doesn't report if it is a block and used" do it "doesn't report if it is a block and used" do
s = Source.new %( expect_no_issues subject, <<-CRYSTAL
def method(&block) def method(&block)
block.call block.call
end end
) CRYSTAL
subject.catch(s).should be_valid
end end
it "doesn't report if block arg is not used" do it "doesn't report if block arg is not used" do
s = Source.new %( expect_no_issues subject, <<-CRYSTAL
def method(&block) def method(&block)
end end
) CRYSTAL
subject.catch(s).should be_valid
end end
it "doesn't report if unused and there is yield" do it "doesn't report if unused and there is yield" do
s = Source.new %( expect_no_issues subject, <<-CRYSTAL
def method(&block) def method(&block)
yield 1 yield 1
end end
) CRYSTAL
subject.catch(s).should be_valid
end end
it "doesn't report if it's an anonymous block" do it "doesn't report if it's an anonymous block" do
s = Source.new %( expect_no_issues subject, <<-CRYSTAL
def method(&) def method(&)
yield 1 yield 1
end end
) CRYSTAL
subject.catch(s).should be_valid
end end
it "doesn't report if variable is referenced implicitly" do it "doesn't report if variable is referenced implicitly" do
s = Source.new %( expect_no_issues subject, <<-CRYSTAL
class Bar < Foo class Bar < Foo
def method(a, b) def method(a, b)
super super
end end
end end
) CRYSTAL
subject.catch(s).should be_valid
end end
it "doesn't report if arg if referenced in case" do it "doesn't report if arg if referenced in case" do
s = Source.new %( expect_no_issues subject, <<-CRYSTAL
def foo(a) def foo(a)
case a case a
when /foo/ when /foo/
end end
end end
) CRYSTAL
subject.catch(s).should be_valid
end end
it "doesn't report if enum in a record" do it "doesn't report if enum in a record" do
s = Source.new %( expect_no_issues subject, <<-CRYSTAL
class Class class Class
record Record do record Record do
enum Enum enum Enum
@ -172,59 +175,49 @@ module Ameba::Rule::Lint
end end
end end
end end
) CRYSTAL
subject.catch(s).should be_valid
end end
context "super" do context "super" do
it "reports if variable is not referenced implicitly by super" do it "reports if variable is not referenced implicitly by super" do
s = Source.new %( source = expect_issue subject, <<-CRYSTAL
class Bar < Foo class Bar < Foo
def method(a, b) def method(a, b)
# ^ error: Unused argument `b`. If it's necessary, use `_b` as an argument name to indicate that it won't be used.
super a super a
end end
end end
) CRYSTAL
subject.catch(s).should_not be_valid
s.issues.first.message.should eq "Unused argument `b`. If it's necessary, use `_b` " \
"as an argument name to indicate that it won't be used."
end
it "reports rule, location and message" do expect_correction source, <<-CRYSTAL
s = Source.new %( class Bar < Foo
def method(a) def method(a, _b)
super a
end
end end
), "source.cr" CRYSTAL
subject.catch(s).should_not be_valid
issue = s.issues.first
issue.rule.should_not be_nil
issue.message.should eq "Unused argument `a`. If it's necessary, use `_a` " \
"as an argument name to indicate that it won't be used."
issue.location.to_s.should eq "source.cr:1:12"
end end
end end
context "macro" do context "macro" do
it "doesn't report if it is a used macro argument" do it "doesn't report if it is a used macro argument" do
s = Source.new %( expect_no_issues subject, <<-CRYSTAL
macro my_macro(arg) macro my_macro(arg)
{% arg %} {% arg %}
end end
) CRYSTAL
subject.catch(s).should be_valid
end end
it "doesn't report if it is a used macro block argument" do it "doesn't report if it is a used macro block argument" do
s = Source.new %( expect_no_issues subject, <<-CRYSTAL
macro my_macro(&block) macro my_macro(&block)
{% block %} {% block %}
end end
) CRYSTAL
subject.catch(s).should be_valid
end end
it "doesn't report used macro args with equal names in record" do it "doesn't report used macro args with equal names in record" do
s = Source.new %( expect_no_issues subject, <<-CRYSTAL
record X do record X do
macro foo(a, b) macro foo(a, b)
{{ a }} + {{ b }} {{ a }} + {{ b }}
@ -234,12 +227,11 @@ module Ameba::Rule::Lint
{{ a }} + {{ b }} + {{ c }} {{ a }} + {{ b }} + {{ c }}
end end
end end
) CRYSTAL
subject.catch(s).should be_valid
end end
it "doesn't report used args in macro literals" do it "doesn't report used args in macro literals" do
s = Source.new %( expect_no_issues subject, <<-CRYSTAL
def print(f : Array(U)) forall U def print(f : Array(U)) forall U
f.size.times do |i| f.size.times do |i|
{% if U == Float64 %} {% if U == Float64 %}
@ -249,65 +241,73 @@ module Ameba::Rule::Lint
{% end %} {% end %}
end end
end end
) CRYSTAL
subject.catch(s).should be_valid
end end
end end
context "properties" do context "properties" do
describe "#ignore_defs" do describe "#ignore_defs" do
it "lets the rule to ignore def scopes if true" do it "lets the rule to ignore def scopes if true" do
subject.ignore_defs = true rule = UnusedArgument.new
s = Source.new %( rule.ignore_defs = true
expect_no_issues rule, <<-CRYSTAL
def method(a) def method(a)
end end
) CRYSTAL
subject.catch(s).should be_valid
end end
it "lets the rule not to ignore def scopes if false" do it "lets the rule not to ignore def scopes if false" do
subject.ignore_defs = false rule = UnusedArgument.new
s = Source.new %( rule.ignore_defs = false
expect_issue rule, <<-CRYSTAL
def method(a) def method(a)
# ^ error: Unused argument `a`. If it's necessary, use `_a` as an argument name to indicate that it won't be used.
end end
) CRYSTAL
subject.catch(s).should_not be_valid
end end
end end
context "#ignore_blocks" do context "#ignore_blocks" do
it "lets the rule to ignore block scopes if true" do it "lets the rule to ignore block scopes if true" do
subject.ignore_blocks = true rule = UnusedArgument.new
s = Source.new %( rule.ignore_blocks = true
expect_no_issues rule, <<-CRYSTAL
3.times { |i| puts "yo!" } 3.times { |i| puts "yo!" }
) CRYSTAL
subject.catch(s).should be_valid
end end
it "lets the rule not to ignore block scopes if false" do it "lets the rule not to ignore block scopes if false" do
subject.ignore_blocks = false rule = UnusedArgument.new
s = Source.new %( rule.ignore_blocks = false
expect_issue rule, <<-CRYSTAL
3.times { |i| puts "yo!" } 3.times { |i| puts "yo!" }
) # ^ error: Unused argument `i`. If it's necessary, use `_` as an argument name to indicate that it won't be used.
subject.catch(s).should_not be_valid CRYSTAL
end end
end end
context "#ignore_procs" do context "#ignore_procs" do
it "lets the rule to ignore proc scopes if true" do it "lets the rule to ignore proc scopes if true" do
subject.ignore_procs = true rule = UnusedArgument.new
s = Source.new %( rule.ignore_procs = true
expect_no_issues rule, <<-CRYSTAL
->(a : Int32) {} ->(a : Int32) {}
) CRYSTAL
subject.catch(s).should be_valid
end end
it "lets the rule not to ignore proc scopes if false" do it "lets the rule not to ignore proc scopes if false" do
subject.ignore_procs = false rule = UnusedArgument.new
s = Source.new %( rule.ignore_procs = false
expect_issue rule, <<-CRYSTAL
->(a : Int32) {} ->(a : Int32) {}
) # ^ error: Unused argument `a`. If it's necessary, use `_a` as an argument name to indicate that it won't be used.
subject.catch(s).should_not be_valid CRYSTAL
end end
end end
end end

View file

@ -5,105 +5,118 @@ module Ameba::Rule::Lint
describe UnusedBlockArgument do describe UnusedBlockArgument do
it "doesn't report if it is an instance var argument" do it "doesn't report if it is an instance var argument" do
s = Source.new %( expect_no_issues subject, <<-CRYSTAL
class A class A
def initialize(&@callback) def initialize(&@callback)
end end
end end
) CRYSTAL
subject.catch(s).should be_valid
end end
it "doesn't report if anonymous" do it "doesn't report if anonymous" do
s = Source.new %( expect_no_issues subject, <<-CRYSTAL
def method(a, b, c, &) def method(a, b, c, &)
end end
) CRYSTAL
subject.catch(s).should be_valid
end end
it "doesn't report if argument name starts with a `_`" do it "doesn't report if argument name starts with a `_`" do
s = Source.new %( expect_no_issues subject, <<-CRYSTAL
def method(a, b, c, &_block) def method(a, b, c, &_block)
end end
) CRYSTAL
subject.catch(s).should be_valid
end end
it "doesn't report if it is a block and used" do it "doesn't report if it is a block and used" do
s = Source.new %( expect_no_issues subject, <<-CRYSTAL
def method(a, b, c, &block) def method(a, b, c, &block)
block.call block.call
end end
) CRYSTAL
subject.catch(s).should be_valid
end end
it "reports if block arg is not used" do it "reports if block arg is not used" do
s = Source.new %( source = expect_issue subject, <<-CRYSTAL
def method(a, b, c, &block) def method(a, b, c, &block)
# ^ error: Unused block argument `block`. [...]
end end
) CRYSTAL
subject.catch(s).should_not be_valid
expect_correction source, <<-CRYSTAL
def method(a, b, c, &_block)
end
CRYSTAL
end end
it "reports if unused and there is yield" do it "reports if unused and there is yield" do
s = Source.new %( source = expect_issue subject, <<-CRYSTAL
def method(a, b, c, &block) def method(a, b, c, &block)
# ^ error: Use `&` as an argument name to indicate that it won't be referenced.
3.times do |i| 3.times do |i|
i.try do i.try do
yield i yield i
end end
end end
end end
) CRYSTAL
subject.catch(s).should_not be_valid
expect_correction source, <<-CRYSTAL
def method(a, b, c, &)
3.times do |i|
i.try do
yield i
end
end
end
CRYSTAL
end end
it "doesn't report if anonymous and there is yield" do it "doesn't report if anonymous and there is yield" do
s = Source.new %( expect_no_issues subject, <<-CRYSTAL
def method(a, b, c, &) def method(a, b, c, &)
yield 1 yield 1
end end
) CRYSTAL
subject.catch(s).should be_valid
end end
it "doesn't report if variable is referenced implicitly" do it "doesn't report if variable is referenced implicitly" do
s = Source.new %( expect_no_issues subject, <<-CRYSTAL
class Bar < Foo class Bar < Foo
def method(a, b, c, &block) def method(a, b, c, &block)
super super
end end
end end
) CRYSTAL
subject.catch(s).should be_valid
end end
context "super" do context "super" do
it "reports if variable is not referenced implicitly by super" do it "reports if variable is not referenced implicitly by super" do
s = Source.new %( source = expect_issue subject, <<-CRYSTAL
class Bar < Foo class Bar < Foo
def method(a, b, c, &block) def method(a, b, c, &block)
# ^ error: Unused block argument `block`. [...]
super a, b, c super a, b, c
end end
end end
) CRYSTAL
subject.catch(s).should_not be_valid
s.issues.first.message.should eq "Unused block argument `block`. If it's necessary, " \ expect_correction source, <<-CRYSTAL
"use `_block` as an argument name to indicate " \ class Bar < Foo
"that it won't be used." def method(a, b, c, &_block)
super a, b, c
end
end
CRYSTAL
end end
end end
context "macro" do context "macro" do
it "doesn't report if it is a used macro block argument" do it "doesn't report if it is a used macro block argument" do
s = Source.new %( expect_no_issues subject, <<-CRYSTAL
macro my_macro(&block) macro my_macro(&block)
{% block %} {% block %}
end end
) CRYSTAL
subject.catch(s).should be_valid
end end
end end
end end

View file

@ -63,7 +63,18 @@ module Ameba::Rule::Lint
next if scope.references?(argument.variable) next if scope.references?(argument.variable)
name_suggestion = scope.node.is_a?(Crystal::Block) ? '_' : "_#{argument.name}" name_suggestion = scope.node.is_a?(Crystal::Block) ? '_' : "_#{argument.name}"
issue_for argument.node, MSG % {argument.name, name_suggestion} message = MSG % {argument.name, name_suggestion}
location = argument.node.location
end_location = location.try &.adjust(column_number: argument.name.size - 1)
if location && end_location
issue_for argument.node, message do |corrector|
corrector.replace(location, end_location, name_suggestion)
end
else
issue_for argument.node, message
end
end end
end end
end end

View file

@ -31,6 +31,8 @@ module Ameba::Rule::Lint
# Enabled: true # Enabled: true
# ``` # ```
class UnusedBlockArgument < Base class UnusedBlockArgument < Base
include AST::Util
properties do properties do
description "Disallows unused block arguments" description "Disallows unused block arguments"
end end
@ -51,11 +53,26 @@ module Ameba::Rule::Lint
return if block_arg.anonymous? return if block_arg.anonymous?
return if scope.references?(block_arg.variable) return if scope.references?(block_arg.variable)
location = block_arg.node.location
end_location = location.try &.adjust(column_number: block_arg.name.size - 1)
if scope.yields? if scope.yields?
issue_for block_arg.node, MSG_YIELDED if location && end_location
issue_for block_arg.node, MSG_YIELDED do |corrector|
corrector.remove(location, end_location)
end
else
issue_for block_arg.node, MSG_YIELDED
end
else else
return if block_arg.ignored? return if block_arg.ignored?
issue_for block_arg.node, MSG_UNUSED % block_arg.name if location && end_location
issue_for block_arg.node, MSG_UNUSED % block_arg.name do |corrector|
corrector.insert_before(location, '_')
end
else
issue_for block_arg.node, MSG_UNUSED % block_arg.name
end
end end
end end
end end