Merge pull request #207 from crystal-ameba/release/0.14.0

Release 0.14.0
This commit is contained in:
Vitalii Elenhaupt 2021-02-24 10:09:47 +02:00 committed by GitHub
commit d8c32f0045
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
106 changed files with 2045 additions and 448 deletions

View file

@ -4,7 +4,7 @@
<p align="center">Code style linter for Crystal<p>
<p align="center">
<sup>
<i> (a single-celled animal that catches food and moves about by extending fingerlike projections of protoplasm) </i>
<i>(a single-celled animal that catches food and moves about by extending fingerlike projections of protoplasm)</i>
</sup>
</p>
<p align="center">
@ -35,7 +35,7 @@
## About
Ameba is a static code analysis tool for the Crystal language.
It enforces a consistent [Crystal code style](https://crystal-lang.org/docs/conventions/coding_style.html),
It enforces a consistent [Crystal code style](https://crystal-lang.org/reference/conventions/coding_style.html),
also catches code smells and wrong code constructions.
See also [Roadmap](https://github.com/crystal-ameba/ameba/wiki).
@ -46,7 +46,7 @@ Run `ameba` binary within your project directory to catch code issues:
```sh
$ ameba
Inspecting 107 files.
Inspecting 107 files
...............F.....................F....................................................................
@ -61,9 +61,7 @@ src/ameba/formatter/base_formatter.cr:12:7
^
Finished in 542.64 milliseconds
129 inspected, 2 failures.
129 inspected, 2 failures
```
### Run in parallel

View file

@ -1,5 +1,5 @@
name: ameba
version: 0.13.4
version: 0.14.0
authors:
- Vitalii Elenhaupt <velenhaupt@gmail.com>

View file

@ -113,7 +113,7 @@ module Ameba::AST
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
scope.variables.empty?.should be_false
end
end

View file

@ -48,7 +48,7 @@ module Ameba::AST
it "assigns the variable (creates a new assignment)" do
variable = Variable.new(var_node, scope)
variable.assign(assign_node, scope)
variable.assignments.any?.should be_true
variable.assignments.empty?.should be_false
end
it "can create multiple assignments" do
@ -64,7 +64,7 @@ module Ameba::AST
variable = Variable.new(var_node, scope)
variable.assign(as_node("foo=1"), scope)
variable.reference(var_node, scope)
variable.references.any?.should be_true
variable.references.empty?.should be_false
end
it "adds a reference to the scope" do

View file

@ -0,0 +1,17 @@
require "../../../spec_helper"
module Ameba::AST
describe TopLevelNodesVisitor do
describe "#require_nodes" do
it "returns require node" do
source = Source.new %(
require "foo"
def bar; end
)
visitor = TopLevelNodesVisitor.new(source.ast)
visitor.require_nodes.size.should eq 1
visitor.require_nodes.first.to_s.should eq %q(require "foo")
end
end
end
end

View file

@ -42,6 +42,16 @@ module Ameba::Cli
c.except.should eq %w(RULE1 RULE2)
end
it "defaults rules? flag to false" do
c = Cli.parse_args %w(file.cr)
c.rules?.should eq false
end
it "accepts --rules flag" do
c = Cli.parse_args %w(--rules)
c.rules?.should eq true
end
it "defaults all? flag to false" do
c = Cli.parse_args %w(file.cr)
c.all?.should eq false

View file

@ -120,7 +120,7 @@ module Ameba
it "returns list of sources" do
config.sources.size.should be > 0
config.sources.first.should be_a Source
config.sources.any? { |s| s.fullpath == __FILE__ }.should be_true
config.sources.any?(&.fullpath.==(__FILE__)).should be_true
end
it "returns a list of sources mathing globs" do
@ -130,7 +130,7 @@ module Ameba
it "returns a lisf of sources excluding 'Excluded'" do
config.excluded = %w(**/config_spec.cr)
config.sources.any? { |s| s.fullpath == __FILE__ }.should be_false
config.sources.any?(&.fullpath.==(__FILE__)).should be_false
end
end

View file

@ -8,7 +8,7 @@ module Ameba::Formatter
describe "#started" do
it "writes started message" do
subject.started [Source.new ""]
output.to_s.should eq "Inspecting 1 file.\n\n"
output.to_s.should eq "Inspecting 1 file\n\n"
end
end
@ -29,7 +29,7 @@ module Ameba::Formatter
describe "#finished" do
it "writes a final message" do
subject.finished [Source.new ""]
output.to_s.should contain "1 inspected, 0 failures."
output.to_s.should contain "1 inspected, 0 failures"
end
it "writes the elapsed time" do
@ -45,7 +45,7 @@ module Ameba::Formatter
end
subject.finished [s]
log = output.to_s
log.should contain "1 inspected, 2 failures."
log.should contain "1 inspected, 2 failures"
log.should contain "DummyRuleError"
log.should contain "NamedRuleError"
end
@ -60,7 +60,7 @@ module Ameba::Formatter
end
subject.finished [s]
log = output.to_s
log.should contain "> a = 22"
log.should contain "> \e[97ma = 22"
log.should contain " \e[33m^\e[0m"
end
@ -99,7 +99,7 @@ module Ameba::Formatter
s.add_issue(DummyRule.new, location: {1, 1},
message: "DummyRuleError", status: :disabled)
subject.finished [s]
output.to_s.should contain "1 inspected, 0 failures."
output.to_s.should contain "1 inspected, 0 failures"
end
end
end

View file

@ -8,6 +8,65 @@ module Ameba::Formatter
subject = Subject.new
describe Util do
describe "#deansify" do
it "returns given string without ANSI codes" do
str = String.build do |io|
io << "foo".colorize.green.underline
io << '-'
io << "bar".colorize.red.underline
end
subject.deansify("foo-bar").should eq "foo-bar"
subject.deansify(str).should eq "foo-bar"
end
end
describe "#trim" do
it "trims string longer than :max_length" do
subject.trim(("+" * 300), 1).should eq "+"
subject.trim(("+" * 300), 3).should eq "+++"
subject.trim(("+" * 300), 5).should eq "+ ..."
subject.trim(("+" * 300), 7).should eq "+++ ..."
end
it "leaves intact string shorter than :max_length" do
subject.trim(("+" * 3), 100).should eq "+++"
end
it "allows to use custom ellipsis" do
subject.trim(("+" * 300), 3, "").should eq "++…"
end
end
describe "#context" do
it "returns correct pre/post context lines" do
source = Source.new <<-EOF
# pre:1
# pre:2
# pre:3
# pre:4
# pre:5
a = 1
# post:1
# post:2
# post:3
# post:4
# post:5
EOF
subject.context(source.lines, lineno: 6, context_lines: 3)
.should eq({<<-PRE.lines, <<-POST.lines
# pre:3
# pre:4
# pre:5
PRE
# post:1
# post:2
# post:3
POST
})
end
end
describe "#affected_code" do
it "returns nil if there is no such a line number" do
source = Source.new %(
@ -22,7 +81,38 @@ module Ameba::Formatter
a = 1
)
location = Crystal::Location.new("filename", 1, 1)
subject.affected_code(source, location).should eq "> a = 1\n \e[33m^\e[0m"
subject.deansify(subject.affected_code(source, location))
.should eq "> a = 1\n ^\n"
end
it "returns correct line if it is found" do
source = Source.new <<-EOF
# pre:1
# pre:2
# pre:3
# pre:4
# pre:5
a = 1
# post:1
# post:2
# post:3
# post:4
# post:5
EOF
location = Crystal::Location.new("filename", 6, 1)
subject.deansify(subject.affected_code(source, location, context_lines: 3))
.should eq <<-STR
> # pre:3
> # pre:4
> # pre:5
> a = 1
^
> # post:1
> # post:2
> # post:3
STR
end
end
end

View file

@ -42,9 +42,22 @@ module Ameba
location: nil,
end_location: nil,
message: "",
status: :enabled
status: :disabled
issue.status.should eq :enabled
issue.status.should eq Issue::Status::Disabled
issue.disabled?.should be_true
issue.enabled?.should be_false
end
it "sets status to :enabled by default" do
issue = Issue.new rule: DummyRule.new,
location: nil,
end_location: nil,
message: ""
issue.status.should eq Issue::Status::Enabled
issue.enabled?.should be_true
issue.disabled?.should be_false
end
end
end

View file

@ -0,0 +1,48 @@
require "../../../spec_helper"
module Ameba::Rule::Lint
subject = DuplicatedRequire.new
describe DuplicatedRequire do
it "passes if there are no duplicated requires" do
source = Source.new %(
require "math"
require "big"
require "big/big_decimal"
)
subject.catch(source).should be_valid
end
it "reports if there are a duplicated requires" do
source = Source.new %(
require "big"
require "math"
require "big"
)
subject.catch(source).should_not be_valid
end
it "reports rule, pos and message" do
source = Source.new %(
require "./thing"
require "./thing"
require "./another_thing"
require "./another_thing"
), "source.cr"
subject.catch(source).should_not be_valid
issue = source.issues.first
issue.rule.should_not be_nil
issue.location.to_s.should eq "source.cr:2:1"
issue.end_location.to_s.should eq ""
issue.message.should eq "Duplicated require of `./thing`"
issue = source.issues.last
issue.rule.should_not be_nil
issue.location.to_s.should eq "source.cr:4:1"
issue.end_location.to_s.should eq ""
issue.message.should eq "Duplicated require of `./another_thing`"
end
end
end

View file

@ -77,7 +77,7 @@ module Ameba::Rule::Lint
issue.rule.should_not be_nil
issue.location.to_s.should eq "source.cr:2:14"
issue.end_location.to_s.should eq "source.cr:2:30"
issue.message.should eq "Use each instead of each_with_object"
issue.message.should eq "Use `each` instead of `each_with_object`"
end
end
end

View file

@ -0,0 +1,127 @@
require "../../../spec_helper"
module Ameba::Rule::Lint
describe SpecFocus do
subject = SpecFocus.new
it "does not report if spec is not focused" do
s = Source.new %(
context "context" {}
describe "describe" {}
it "it" {}
pending "pending" {}
), path: "source_spec.cr"
subject.catch(s).should be_valid
end
it "reports if there is a focused context" do
s = Source.new %(
context "context", focus: true do
end
), path: "source_spec.cr"
subject.catch(s).should_not be_valid
end
it "reports if there is a focused describe block" do
s = Source.new %(
describe "describe", focus: true do
end
), path: "source_spec.cr"
subject.catch(s).should_not be_valid
end
it "reports if there is a focused it block" do
s = Source.new %(
it "it", focus: true do
end
), path: "source_spec.cr"
subject.catch(s).should_not be_valid
end
it "reports if there is a focused pending block" do
s = Source.new %(
pending "pending", focus: true do
end
), path: "source_spec.cr"
subject.catch(s).should_not be_valid
end
it "reports if there is a spec item with `focus: false`" do
s = Source.new %(
it "it", focus: false do
end
), path: "source_spec.cr"
subject.catch(s).should_not be_valid
end
it "does not report if there is non spec block with :focus" do
s = Source.new %(
some_method "foo", focus: true do
end
), path: "source_spec.cr"
subject.catch(s).should be_valid
end
it "does not report if there is a tagged item with :focus" do
s = Source.new %(
it "foo", tags: "focus" do
end
), path: "source_spec.cr"
subject.catch(s).should be_valid
end
it "does not report if there are focused spec items without blocks" do
s = Source.new %(
describe "foo", focus: true
context "foo", focus: true
it "foo", focus: true
pending "foo", focus: true
), path: "source_spec.cr"
subject.catch(s).should be_valid
end
it "does not report if there are focused items out of spec file" do
s = Source.new %(
describe "foo", focus: true {}
context "foo", focus: true {}
it "foo", focus: true {}
pending "foo", focus: true {}
)
subject.catch(s).should be_valid
end
it "reports rule, pos and message" do
s = Source.new %(
it "foo", focus: true do
it "bar", focus: true {}
end
), path: "source_spec.cr"
subject.catch(s).should_not be_valid
s.issues.size.should eq(2)
first, second = s.issues
first.rule.should_not be_nil
first.location.to_s.should eq "source_spec.cr:1:11"
first.end_location.to_s.should eq ""
first.message.should eq "Focused spec item detected"
second.rule.should_not be_nil
second.location.to_s.should eq "source_spec.cr:2:13"
second.end_location.to_s.should eq ""
second.message.should eq "Focused spec item detected"
end
end
end

View file

@ -0,0 +1,46 @@
require "../../../spec_helper"
module Ameba::Rule::Performance
subject = AnyInsteadOfEmpty.new
describe AnyInsteadOfEmpty do
it "passes if there is no potential performance improvements" do
source = Source.new %(
[1, 2, 3].any?(&.zero?)
[1, 2, 3].any?(String)
[1, 2, 3].any?(1..3)
[1, 2, 3].any? { |e| e > 1 }
)
subject.catch(source).should be_valid
end
it "reports if there is any? call without a block nor argument" do
source = Source.new %(
[1, 2, 3].any?
)
subject.catch(source).should_not be_valid
end
context "macro" do
it "reports in macro scope" do
source = Source.new %(
{{ [1, 2, 3].any? }}
)
subject.catch(source).should_not be_valid
end
end
it "reports rule, pos and message" do
source = Source.new path: "source.cr", code: %(
[1, 2, 3].any?
)
subject.catch(source).should_not be_valid
issue = source.issues.first
issue.rule.should_not be_nil
issue.location.to_s.should eq "source.cr:1:11"
issue.end_location.to_s.should eq "source.cr:1:15"
issue.message.should eq "Use `!{...}.empty?` instead of `{...}.any?`"
end
end
end

View file

@ -0,0 +1,69 @@
require "../../../spec_helper"
module Ameba::Rule::Performance
subject = ChainedCallWithNoBang.new
describe ChainedCallWithNoBang do
it "passes if there is no potential performance improvements" do
source = Source.new %(
(1..3).select { |e| e > 1 }.sort!
(1..3).select { |e| e > 1 }.sort_by!(&.itself)
(1..3).select { |e| e > 1 }.uniq!
(1..3).select { |e| e > 1 }.shuffle!
(1..3).select { |e| e > 1 }.reverse!
(1..3).select { |e| e > 1 }.rotate!
)
subject.catch(source).should be_valid
end
it "reports if there is select followed by reverse" do
source = Source.new %(
[1, 2, 3].select { |e| e > 1 }.reverse
)
subject.catch(source).should_not be_valid
end
it "reports if there is select followed by reverse followed by other call" do
source = Source.new %(
[1, 2, 3].select { |e| e > 2 }.reverse.size
)
subject.catch(source).should_not be_valid
end
context "properties" do
it "allows to configure `call_names`" do
source = Source.new %(
[1, 2, 3].select { |e| e > 2 }.reverse
)
rule = ChainedCallWithNoBang.new
rule.call_names = %w(uniq)
rule.catch(source).should be_valid
end
end
it "reports rule, pos and message" do
source = Source.new path: "source.cr", code: <<-CODE
[1, 2, 3].select { |e| e > 1 }.reverse
CODE
subject.catch(source).should_not be_valid
source.issues.size.should eq 1
issue = source.issues.first
issue.rule.should_not be_nil
issue.location.to_s.should eq "source.cr:1:32"
issue.end_location.to_s.should eq "source.cr:1:39"
issue.message.should eq "Use bang method variant `reverse!` after chained `select` call"
end
context "macro" do
it "doesn't report in macro scope" do
source = Source.new %(
{{ [1, 2, 3].select { |e| e > 2 }.reverse }}
)
subject.catch(source).should be_valid
end
end
end
end

View file

@ -0,0 +1,50 @@
require "../../../spec_helper"
module Ameba::Rule::Performance
subject = CompactAfterMap.new
describe CompactAfterMap do
it "passes if there is no potential performance improvements" do
source = Source.new %(
(1..3).compact_map(&.itself)
)
subject.catch(source).should be_valid
end
it "passes if there is map followed by a bang call" do
source = Source.new %(
(1..3).map(&.itself).compact!
)
subject.catch(source).should be_valid
end
it "reports if there is map followed by compact call" do
source = Source.new %(
(1..3).map(&.itself).compact
)
subject.catch(source).should_not be_valid
end
context "macro" do
it "doesn't report in macro scope" do
source = Source.new %(
{{ [1, 2, 3].map(&.to_s).compact }}
)
subject.catch(source).should be_valid
end
end
it "reports rule, pos and message" do
s = Source.new %(
(1..3).map(&.itself).compact
), "source.cr"
subject.catch(s).should_not be_valid
issue = s.issues.first
issue.rule.should_not be_nil
issue.location.to_s.should eq "source.cr:1:8"
issue.end_location.to_s.should eq "source.cr:1:29"
issue.message.should eq "Use `compact_map {...}` instead of `map {...}.compact`"
end
end
end

View file

@ -0,0 +1,43 @@
require "../../../spec_helper"
module Ameba::Rule::Performance
subject = FlattenAfterMap.new
describe FlattenAfterMap do
it "passes if there is no potential performance improvements" do
source = Source.new %(
%w[Alice Bob].flat_map(&.chars)
)
subject.catch(source).should be_valid
end
it "reports if there is map followed by flatten call" do
source = Source.new %(
%w[Alice Bob].map(&.chars).flatten
)
subject.catch(source).should_not be_valid
end
context "macro" do
it "doesn't report in macro scope" do
source = Source.new %(
{{ %w[Alice Bob].map(&.chars).flatten }}
)
subject.catch(source).should be_valid
end
end
it "reports rule, pos and message" do
s = Source.new %(
%w[Alice Bob].map(&.chars).flatten
), "source.cr"
subject.catch(s).should_not be_valid
issue = s.issues.first
issue.rule.should_not be_nil
issue.location.to_s.should eq "source.cr:1:15"
issue.end_location.to_s.should eq "source.cr:1:35"
issue.message.should eq "Use `flat_map {...}` instead of `map {...}.flatten`"
end
end
end

View file

@ -0,0 +1,59 @@
require "../../../spec_helper"
module Ameba::Rule::Performance
subject = MapInsteadOfBlock.new
describe MapInsteadOfBlock do
it "passes if there is no potential performance improvements" do
source = Source.new %(
(1..3).join(&.to_s)
(1..3).sum(&.*(2))
(1..3).product(&.*(2))
)
subject.catch(source).should be_valid
end
it "reports if there is map followed by join without a block" do
source = Source.new %(
(1..3).map(&.to_s).join
)
subject.catch(source).should_not be_valid
end
it "reports if there is map followed by join without a block (with argument)" do
source = Source.new %(
(1..3).map(&.to_s).join('.')
)
subject.catch(source).should_not be_valid
end
it "reports if there is map followed by join with a block" do
source = Source.new %(
(1..3).map(&.to_s).join(&.itself)
)
subject.catch(source).should_not be_valid
end
context "macro" do
it "doesn't report in macro scope" do
source = Source.new %(
{{ [1, 2, 3].map(&.to_s).join }}
)
subject.catch(source).should be_valid
end
end
it "reports rule, pos and message" do
s = Source.new %(
(1..3).map(&.to_s).join
), "source.cr"
subject.catch(s).should_not be_valid
issue = s.issues.first
issue.rule.should_not be_nil
issue.location.to_s.should eq "source.cr:1:8"
issue.end_location.to_s.should eq "source.cr:1:24"
issue.message.should eq "Use `join {...}` instead of `map {...}.join`"
end
end
end

View file

@ -0,0 +1,64 @@
require "../../../spec_helper"
module Ameba::Rule::Style
subject = IsAFilter.new
describe IsAFilter do
it "passes if there is no potential performance improvements" do
source = Source.new %(
[1, 2, nil].select(Int32)
[1, 2, nil].reject(Nil)
)
subject.catch(source).should be_valid
end
it "reports if there is .is_a? call within select" do
source = Source.new %(
[1, 2, nil].select(&.is_a?(Int32))
)
subject.catch(source).should_not be_valid
end
it "reports if there is .nil? call within reject" do
source = Source.new %(
[1, 2, nil].reject(&.nil?)
)
subject.catch(source).should_not be_valid
end
context "properties" do
it "allows to configure filter_names" do
source = Source.new %(
[1, 2, nil].reject(&.nil?)
)
rule = IsAFilter.new
rule.filter_names = %w(select)
rule.catch(source).should be_valid
end
end
context "macro" do
it "reports in macro scope" do
source = Source.new %(
{{ [1, 2, nil].reject(&.nil?) }}
)
subject.catch(source).should_not be_valid
end
end
it "reports rule, pos and message" do
source = Source.new path: "source.cr", code: %(
[1, 2, nil].reject(&.nil?)
)
subject.catch(source).should_not be_valid
source.issues.size.should eq 1
issue = source.issues.first
issue.rule.should_not be_nil
issue.location.to_s.should eq "source.cr:1:13"
issue.end_location.to_s.should eq "source.cr:1:26"
issue.message.should eq "Use `reject(Nil)` instead of `reject {...}`"
end
end
end

View file

@ -0,0 +1,214 @@
require "../../../spec_helper"
module Ameba::Rule::Style
subject = VerboseBlock.new
describe VerboseBlock do
it "passes if there is no potential performance improvements" do
source = Source.new %(
(1..3).any?(&.odd?)
(1..3).join('.', &.to_s)
(1..3).each_with_index { |i, idx| i * idx }
(1..3).map { |i| typeof(i) }
(1..3).map { |i| i || 0 }
(1..3).map { |i| :foo }
(1..3).map { |i| :foo.to_s.split.join('.') }
(1..3).map { :foo }
)
subject.catch(source).should be_valid
end
it "passes if the block argument is used within the body" do
source = Source.new %(
(1..3).map { |i| i * i }
(1..3).map { |j| j * j.to_i64 }
(1..3).map { |k| k.to_i64 * k }
(1..3).map { |l| l.to_i64 * l.to_i64 }
(1..3).map { |m| m.to_s[start: m.to_i64, count: 3]? }
(1..3).map { |n| n.to_s.split.map { |z| n.to_i * z.to_i }.join }
)
subject.catch(source).should be_valid
end
it "reports if there is a call with a collapsible block" do
source = Source.new %(
(1..3).any? { |i| i.odd? }
)
subject.catch(source).should_not be_valid
end
it "reports if there is a call with an argument + collapsible block" do
source = Source.new %(
(1..3).join('.') { |i| i.to_s }
)
subject.catch(source).should_not be_valid
end
it "reports if there is a call with a collapsible block (with chained call)" do
source = Source.new %(
(1..3).map { |i| i.to_s.split.reverse.join.strip }
)
subject.catch(source).should_not be_valid
end
context "properties" do
it "#exclude_calls_with_block" do
source = Source.new %(
(1..3).in_groups_of(1) { |i| i.map(&.to_s) }
)
rule = VerboseBlock.new
rule
.tap(&.exclude_calls_with_block = true)
.catch(source).should be_valid
rule
.tap(&.exclude_calls_with_block = false)
.catch(source).should_not be_valid
end
it "#exclude_multiple_line_blocks" do
source = Source.new %(
(1..3).any? do |i|
i.odd?
end
)
rule = VerboseBlock.new
rule
.tap(&.exclude_multiple_line_blocks = true)
.catch(source).should be_valid
rule
.tap(&.exclude_multiple_line_blocks = false)
.catch(source).should_not be_valid
end
it "#exclude_prefix_operators" do
source = Source.new %(
(1..3).sum { |i| +i }
(1..3).sum { |i| -i }
)
rule = VerboseBlock.new
rule
.tap(&.exclude_prefix_operators = true)
.catch(source).should be_valid
rule
.tap(&.exclude_prefix_operators = false)
.catch(source).should_not be_valid
end
it "#exclude_operators" do
source = Source.new %(
(1..3).sum { |i| i * 2 }
)
rule = VerboseBlock.new
rule
.tap(&.exclude_operators = true)
.catch(source).should be_valid
rule
.tap(&.exclude_operators = false)
.catch(source).should_not be_valid
end
it "#exclude_setters" do
source = Source.new %(
Char::Reader.new("abc").tap { |reader| reader.pos = 0 }
)
rule = VerboseBlock.new
rule
.tap(&.exclude_setters = true)
.catch(source).should be_valid
rule
.tap(&.exclude_setters = false)
.catch(source).should_not be_valid
end
it "#max_line_length" do
source = Source.new %(
(1..3).tap &.tap &.tap &.tap &.tap &.tap &.tap do |i|
i.to_s.reverse.strip.blank?
end
)
rule = VerboseBlock.new
rule
.tap(&.max_line_length = 60)
.catch(source).should be_valid
rule
.tap(&.max_line_length = nil)
.catch(source).should_not be_valid
end
it "#max_length" do
source = Source.new %(
(1..3).tap { |i| i.to_s.split.reverse.join.strip.blank? }
)
rule = VerboseBlock.new
rule
.tap(&.max_length = 30)
.catch(source).should be_valid
rule
.tap(&.max_length = nil)
.catch(source).should_not be_valid
end
end
context "macro" do
it "reports in macro scope" do
source = Source.new %(
{{ (1..3).any? { |i| i.odd? } }}
)
subject.catch(source).should_not be_valid
end
end
it "reports call args and named_args" do
short_block_variants = {
%|map(&.to_s.[start: 0.to_i64, count: 3]?)|,
%|map(&.to_s.[0.to_i64, count: 3]?)|,
%|map(&.to_s.[0.to_i64, 3]?)|,
%|map(&.to_s.[start: 0.to_i64, count: 3]=("foo"))|,
%|map(&.to_s.[0.to_i64, count: 3]=("foo"))|,
%|map(&.to_s.[0.to_i64, 3]=("foo"))|,
%|map(&.to_s.camelcase(lower: true))|,
%|map(&.to_s.camelcase)|,
%|map(&.to_s.gsub('_', '-'))|,
%|map(&.in?(*{1, 2, 3}, **{foo: :bar}))|,
%|map(&.in?(1, *foo, 3, **bar))|,
%|join(separator: '.', &.to_s)|,
}
source = Source.new path: "source.cr", code: %(
(1..3).map { |i| i.to_s[start: 0.to_i64, count: 3]? }
(1..3).map { |i| i.to_s[0.to_i64, count: 3]? }
(1..3).map { |i| i.to_s[0.to_i64, 3]? }
(1..3).map { |i| i.to_s[start: 0.to_i64, count: 3] = "foo" }
(1..3).map { |i| i.to_s[0.to_i64, count: 3] = "foo" }
(1..3).map { |i| i.to_s[0.to_i64, 3] = "foo" }
(1..3).map { |i| i.to_s.camelcase(lower: true) }
(1..3).map { |i| i.to_s.camelcase }
(1..3).map { |i| i.to_s.gsub('_', '-') }
(1..3).map { |i| i.in?(*{1, 2, 3}, **{foo: :bar}) }
(1..3).map { |i| i.in?(1, *foo, 3, **bar) }
(1..3).join(separator: '.') { |i| i.to_s }
)
subject.catch(source).should_not be_valid
source.issues.size.should eq(short_block_variants.size)
source.issues.each_with_index do |issue, i|
issue.message.should eq(VerboseBlock::MSG % short_block_variants[i])
end
end
it "reports rule, pos and message" do
source = Source.new path: "source.cr", code: %(
(1..3).any? { |i| i.odd? }
)
subject.catch(source).should_not be_valid
source.issues.size.should eq 1
issue = source.issues.first
issue.rule.should_not be_nil
issue.location.to_s.should eq "source.cr:1:8"
issue.end_location.to_s.should eq "source.cr:1:26"
issue.message.should eq "Use short block notation instead: `any?(&.odd?)`"
end
end
end

View file

@ -4,9 +4,7 @@ module Ameba
describe Severity do
describe "#symbol" do
it "returns the symbol for each severity in the enum" do
Severity.values.each do |severity|
severity.symbol.should_not be_nil
end
Severity.values.each(&.symbol.should_not(be_nil))
end
it "returns symbol for Error" do

View file

@ -23,6 +23,18 @@ module Ameba
end
end
describe "#spec?" do
it "returns true if the source is a spec file" do
s = Source.new "", "./source_spec.cr"
s.spec?.should be_true
end
it "returns false if the source is not a spec file" do
s = Source.new "", "./source.cr"
s.spec?.should be_false
end
end
describe "#matches_path?" do
it "returns true if source's path is matched" do
s = Source.new "", "source.cr"

View file

@ -3,7 +3,7 @@ require "../src/ameba"
module Ameba
# Dummy Rule which does nothing.
struct DummyRule < Rule::Base
class DummyRule < Rule::Base
properties do
description : String = "Dummy rule that does nothing."
end
@ -35,7 +35,7 @@ module Ameba
end
end
struct NamedRule < Rule::Base
class NamedRule < Rule::Base
properties do
description "A rule with a custom name."
end
@ -45,7 +45,7 @@ module Ameba
end
end
struct ErrorRule < Rule::Base
class ErrorRule < Rule::Base
properties do
description "Always adds an error at 1:1"
end
@ -55,7 +55,7 @@ module Ameba
end
end
struct ScopeRule < Rule::Base
class ScopeRule < Rule::Base
@[YAML::Field(ignore: true)]
getter scopes = [] of AST::Scope
@ -68,7 +68,7 @@ module Ameba
end
end
struct FlowExpressionRule < Rule::Base
class FlowExpressionRule < Rule::Base
@[YAML::Field(ignore: true)]
getter expressions = [] of AST::FlowExpression
@ -81,7 +81,7 @@ module Ameba
end
end
struct RedundantControlExpressionRule < Rule::Base
class RedundantControlExpressionRule < Rule::Base
@[YAML::Field(ignore: true)]
getter nodes = [] of Crystal::ASTNode
@ -95,7 +95,7 @@ module Ameba
end
# A rule that always raises an error
struct RaiseRule < Rule::Base
class RaiseRule < Rule::Base
property should_raise = false
properties do

View file

@ -20,7 +20,6 @@ require "./ameba/formatter/*"
#
# Ameba.run config
# ```
#
module Ameba
extend self
@ -35,7 +34,6 @@ module Ameba
# Ameba.run
# Ameba.run config
# ```
#
def run(config = Config.load)
Runner.new(config).run
end

View file

@ -44,7 +44,6 @@ module Ameba::AST
# end
# end
# ```
#
def in_loop?
@parent.loop?
end

View file

@ -37,8 +37,7 @@ module Ameba::AST
# Returns true if this node or one of the parent branchables is a loop, false otherwise.
def loop?
return true if loop?(node)
parent.try(&.loop?) || false
loop?(node) || parent.try(&.loop?) || false
end
end
end

View file

@ -59,8 +59,6 @@ module Ameba::AST
end
when Crystal::BinaryOp
unreachable_nodes << current_node.right if flow_expression?(current_node.left, in_loop?)
else
# nop
end
unreachable_nodes

View file

@ -78,7 +78,7 @@ module Ameba::AST
# scope.find_variable("foo")
# ```
def find_variable(name : String)
variables.find { |v| v.name == name } || outer_scope.try &.find_variable(name)
variables.find(&.name.==(name)) || outer_scope.try &.find_variable(name)
end
# Creates a new assignment for the variable.
@ -117,8 +117,8 @@ module Ameba::AST
# Returns true instance variable assinged in this scope.
def assigns_ivar?(name)
arguments.find { |arg| arg.name == name } &&
ivariables.find { |var| var.name == "@#{name}" }
arguments.find(&.name.== name) &&
ivariables.find(&.name.== "@#{name}")
end
# Returns true if and only if current scope represents some
@ -143,7 +143,7 @@ module Ameba::AST
# Returns true if current scope is a def, false if not.
def def?
node.is_a? Crystal::Def
node.is_a?(Crystal::Def)
end
# Returns true if this scope is a top level scope, false if not.
@ -173,7 +173,7 @@ module Ameba::AST
# the same Crystal node as `@node`.
def eql?(node)
node == @node &&
!node.location.nil? &&
node.location &&
node.location == @node.location
end
end

View file

@ -97,7 +97,6 @@ module Ameba::AST::Util
# ```
#
# 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

View file

@ -28,7 +28,6 @@ module Ameba::AST
# ```
# Assignment.new(node, variable, scope)
# ```
#
def initialize(@node, @variable, @scope)
if scope = @variable.scope
@branch = Branch.of(@node, scope)
@ -49,7 +48,7 @@ module Ameba::AST
# a ||= 1
# ```
def op_assign?
node.is_a? Crystal::OpAssign
node.is_a?(Crystal::OpAssign)
end
# Returns true if this assignment is in a branch, false if not.
@ -95,7 +94,6 @@ module Ameba::AST
# puts(b)
# end
# ```
#
def transformed?
return false unless (assign = node).is_a?(Crystal::Assign)
return false unless (value = assign.value).is_a?(Crystal::Call)

View file

@ -27,7 +27,6 @@ module Ameba::AST
# ```
# Variable.new(node, scope)
# ```
#
def initialize(@node, @scope)
end
@ -45,7 +44,6 @@ module Ameba::AST
# variable.assign(node2)
# variable.assignment.size # => 2
# ```
#
def assign(node, scope)
assignments << Assignment.new(node, self, scope)
@ -60,7 +58,7 @@ module Ameba::AST
# variable.referenced? # => true
# ```
def referenced?
references.any?
!references.empty?
end
# Creates a reference to this variable in some scope.
@ -69,7 +67,6 @@ module Ameba::AST
# variable = Variable.new(node, scope)
# variable.reference(var_node, some_scope)
# ```
#
def reference(node : Crystal::Var, scope : Scope)
Reference.new(node, scope).tap do |reference|
references << reference
@ -91,8 +88,8 @@ module Ameba::AST
next if consumed_branches.includes?(assignment.branch)
assignment.referenced = true
break unless assignment.branch
consumed_branches << assignment.branch.not_nil!
break unless branch = assignment.branch
consumed_branches << branch
end
end
@ -175,6 +172,10 @@ module Ameba::AST
node.accept self
end
def references?(node : Crystal::Var)
@macro_literals.any?(&.value.includes?(node.name))
end
def visit(node : Crystal::ASTNode)
true
end
@ -203,7 +204,7 @@ module Ameba::AST
private def update_assign_reference!
if @assign_before_reference.nil? &&
references.size <= assignments.size &&
assignments.none? { |ass| ass.op_assign? }
assignments.none?(&.op_assign?)
@assign_before_reference = assignments.find { |ass| !ass.in_branch? }.try &.node
end
end

View file

@ -15,7 +15,6 @@ module Ameba::AST
# ```
# visitor = Ameba::AST::NodeVisitor.new(rule, source)
# ```
#
def initialize(@rule, @source)
@source.ast.accept self
end

View file

@ -18,7 +18,6 @@ module Ameba::AST
if flow_expression?(node, in_loop?)
@rule.test @source, node, FlowExpression.new(node, in_loop?)
end
true
end
@ -61,7 +60,7 @@ module Ameba::AST
end
private def in_loop?
@loop_stack.any?
!@loop_stack.empty?
end
end
end

View file

@ -40,7 +40,7 @@ module Ameba::AST
@skip : Array(Crystal::ASTNode.class)?
def initialize(@rule, @source, skip = nil)
@skip = skip.try &.map { |el| el.as(Crystal::ASTNode.class) }
@skip = skip.try &.map(&.as(Crystal::ASTNode.class))
super @rule, @source
end
@ -54,7 +54,7 @@ module Ameba::AST
{% end %}
def visit(node)
return true unless (skip = @skip)
return true unless skip = @skip
!skip.includes?(node.class)
end
end

View file

@ -28,8 +28,6 @@ module Ameba::AST
when Crystal::Case then traverse_case node
when Crystal::BinaryOp then traverse_binary_op node
when Crystal::ExceptionHandler then traverse_exception_handler node
else
# ok
end
end

View file

@ -7,7 +7,6 @@ module Ameba::AST
RECORD_NODE_NAME = "record"
@scope_queue = [] of Scope
@current_scope : Scope
def initialize(@rule, @source)
@ -169,7 +168,6 @@ module Ameba::AST
variable.reference(variable.node, @current_scope).explicit = false
end
end
true
when @current_scope.top_level? && record_macro?(node)
false

View file

@ -0,0 +1,28 @@
module Ameba::AST
# AST Visitor that visits certain nodes at a top level, which
# can characterize the source (i.e. require statements, modules etc.)
class TopLevelNodesVisitor < Crystal::Visitor
getter require_nodes = [] of Crystal::Require
# Creates a new instance of visitor
def initialize(@scope : Crystal::ASTNode)
@scope.accept(self)
end
# :nodoc:
def visit(node : Crystal::Require)
require_nodes << node
end
# If a top level node is Crystal::Expressions traverse the children.
def visit(node : Crystal::Expressions)
true
end
# A general visit method for rest of the nodes.
# Returns false meaning all child nodes will not be traversed.
def visit(node : Crystal::ASTNode)
false
end
end
end

View file

@ -8,12 +8,21 @@ module Ameba::Cli
def run(args = ARGV)
opts = parse_args args
config = Config.load opts.config, opts.colors?
config.globs = opts.globs.not_nil! if opts.globs
config.severity = opts.fail_level.not_nil! if opts.fail_level
if globs = opts.globs
config.globs = globs
end
if fail_level = opts.fail_level
config.severity = fail_level
end
configure_formatter(config, opts)
configure_rules(config, opts)
if opts.rules?
print_rules(config)
end
runner = Ameba.run(config)
if location = opts.location_to_explain
@ -34,6 +43,7 @@ module Ameba::Cli
property except : Array(String)?
property location_to_explain : NamedTuple(file: String, line: Int32, column: Int32)?
property fail_level : Severity?
property? rules = false
property? all = false
property? colors = true
property? without_affected_code = false
@ -44,13 +54,14 @@ module Ameba::Cli
parser.banner = "Usage: ameba [options] [file1 file2 ...]"
parser.on("-v", "--version", "Print version") { print_version }
parser.on("-h", "--help", "Show this help") { show_help parser }
parser.on("-h", "--help", "Show this help") { print_help(parser) }
parser.on("-r", "--rules", "Show all available rules") { opts.rules = true }
parser.on("-s", "--silent", "Disable output") { opts.formatter = :silent }
parser.unknown_args do |f|
if f.size == 1 && f.first =~ /.+:\d+:\d+/
configure_explain_opts(f.first, opts)
else
opts.globs = f if f.any?
opts.globs = f unless f.empty?
end
end
@ -66,12 +77,12 @@ module Ameba::Cli
parser.on("--only RULE1,RULE2,...",
"Run only given rules (or groups)") do |rules|
opts.only = rules.split ","
opts.only = rules.split(',')
end
parser.on("--except RULE1,RULE2,...",
"Disable the given rules (or groups)") do |rules|
opts.except = rules.split ","
opts.except = rules.split(',')
end
parser.on("--all", "Enables all available rules") do
@ -84,7 +95,8 @@ module Ameba::Cli
opts.config = ""
end
parser.on("--fail-level SEVERITY", "Change the level of failure to exit. Defaults to Convention") do |level|
parser.on("--fail-level SEVERITY",
"Change the level of failure to exit. Defaults to Convention") do |level|
opts.fail_level = Severity.parse(level)
end
@ -107,13 +119,13 @@ module Ameba::Cli
end
private def configure_rules(config, opts)
if only = opts.only
config.rules.map! { |r| r.enabled = false; r }
case
when only = opts.only
config.rules.each(&.enabled = false)
config.update_rules(only, enabled: true)
elsif opts.all?
config.rules.map! { |r| r.enabled = true; r }
when opts.all?
config.rules.each(&.enabled = true)
end
config.update_rules(opts.except, enabled: false)
end
@ -121,7 +133,8 @@ module Ameba::Cli
if name = opts.formatter
config.formatter = name
end
config.formatter.config[:without_affected_code] = opts.without_affected_code?
config.formatter.config[:without_affected_code] =
opts.without_affected_code?
end
private def configure_explain_opts(loc, opts)
@ -132,10 +145,15 @@ module Ameba::Cli
end
private def parse_explain_location(arg)
location = arg.split(":", remove_empty: true).map &.strip
location = arg.split(':', remove_empty: true).map! &.strip
raise ArgumentError.new unless location.size === 3
file, line, column = location
{file: file, line: line.to_i, column: column.to_i}
{
file: file,
line: line.to_i,
column: column.to_i,
}
rescue
raise "location should have PATH:line:column format"
end
@ -145,8 +163,18 @@ module Ameba::Cli
exit 0
end
private def show_help(parser)
private def print_help(parser)
puts parser
exit 0
end
private def print_rules(config)
config.rules.each do |rule|
puts \
"#{rule.name.colorize(:white)} " \
"[#{rule.severity.symbol.to_s.colorize(:green)}] - " \
"#{rule.description.colorize(:dark_gray)}"
end
exit 0
end
end

View file

@ -31,7 +31,6 @@ class Ameba::Config
!lib
)
setter formatter : Formatter::BaseFormatter?
getter rules : Array(Rule::Base)
property severity = Severity::Convention
@ -66,7 +65,9 @@ class Ameba::Config
@excluded = load_array_section(config, "Excluded")
@globs = load_array_section(config, "Globs", DEFAULT_GLOBS)
self.formatter = load_formatter_name(config)
if formatter_name = load_formatter_name(config)
self.formatter = formatter_name
end
end
# Loads YAML configuration file by `path`.
@ -74,7 +75,6 @@ class Ameba::Config
# ```
# config = Ameba::Config.load
# ```
#
def self.load(path = PATH, colors = true)
Colorize.enabled = colors
content = File.exists?(path) ? File.read path : "{}"
@ -84,7 +84,7 @@ class Ameba::Config
end
def self.formatter_names
AVAILABLE_FORMATTERS.keys.join("|")
AVAILABLE_FORMATTERS.keys.join('|')
end
# Returns a list of sources matching globs and excluded sections.
@ -96,7 +96,6 @@ class Ameba::Config
# config.excluded = ["spec"]
# config.sources # => list of sources pointing to files found by the wildcards
# ```
#
def sources
(find_files_by_globs(globs) - find_files_by_globs(excluded))
.map { |path| Source.new File.read(path), path }
@ -111,8 +110,8 @@ class Ameba::Config
# config.formatter
# ```
#
def formatter
@formatter ||= Formatter::DotFormatter.new
property formatter : Formatter::BaseFormatter do
Formatter::DotFormatter.new
end
# Sets formatter by name.
@ -121,7 +120,6 @@ class Ameba::Config
# config = Ameba::Config.load
# config.formatter = :progress
# ```
#
def formatter=(name : String | Symbol)
if f = AVAILABLE_FORMATTERS[name]?
@formatter = f.new
@ -136,15 +134,13 @@ class Ameba::Config
# config = Ameba::Config.load
# config.update_rule "MyRuleName", enabled: false
# ```
#
def update_rule(name, enabled = true, excluded = nil)
index = @rules.index { |r| r.name == name }
raise ArgumentError.new("Rule `#{name}` does not exist") unless index
rule = @rules.find(&.name.==(name))
raise ArgumentError.new("Rule `#{name}` does not exist") unless rule
rule = @rules[index]
rule.enabled = enabled
rule.excluded = excluded
@rules[index] = rule
rule
.tap(&.enabled = enabled)
.tap(&.excluded = excluded)
end
# Updates rules properties.
@ -159,20 +155,22 @@ class Ameba::Config
# ```
# config.update_rules %w(Group1 Group2), enabled: true
# ```
#
def update_rules(names, **args)
def update_rules(names, enabled = true, excluded = nil)
names.try &.each do |name|
if group = @rule_groups[name]?
group.each { |rule| update_rule(rule.name, **args) }
if rules = @rule_groups[name]?
rules.each do |rule|
rule.enabled = enabled
rule.excluded = excluded
end
else
update_rule name, **args
update_rule name, enabled, excluded
end
end
end
private def load_formatter_name(config)
name = config["Formatter"]?.try &.["Name"]?
name ? name.to_s : nil
name.try(&.to_s)
end
private def load_array_section(config, section_name, default = [] of String)
@ -269,7 +267,7 @@ class Ameba::Config
include YAML::Serializable::Strict
def self.new(config = nil)
if (raw = config.try &.raw).is_a? Hash
if (raw = config.try &.raw).is_a?(Hash)
yaml = raw[rule_name]?.try &.to_yaml
end
from_yaml yaml || "{}"

View file

@ -6,14 +6,15 @@ module Ameba::Formatter
class DotFormatter < BaseFormatter
include Util
@started_at : Time?
@started_at : Time::Span?
@mutex = Thread::Mutex.new
# Reports a message when inspection is started.
def started(sources)
@started_at = Time.utc # Time.monotonic
@started_at = Time.monotonic
output << started_message(sources.size)
output.puts started_message(sources.size)
output.puts
end
# Reports a result of the inspection of a corresponding source.
@ -35,32 +36,35 @@ module Ameba::Formatter
next if issue.disabled?
next if (location = issue.location).nil?
output << "#{location}\n".colorize(:cyan)
output << "[#{issue.rule.severity.symbol}] #{issue.rule.name}: #{issue.message}\n".colorize(:red)
output.puts location.colorize(:cyan)
output.puts \
"[#{issue.rule.severity.symbol}] " \
"#{issue.rule.name}: " \
"#{issue.message}".colorize(:red)
if show_affected_code && (code = affected_code(source, location))
if show_affected_code && (code = affected_code(source, location, issue.end_location))
output << code.colorize(:default)
end
output << "\n"
output.puts
end
end
output << finished_in_message(@started_at, Time.utc) # Time.monotonic
output << final_message(sources, failed_sources)
output.puts finished_in_message(@started_at, Time.monotonic)
output.puts final_message(sources, failed_sources)
end
private def started_message(size)
if size == 1
"Inspecting 1 file.\n\n".colorize(:default)
"Inspecting 1 file".colorize(:default)
else
"Inspecting #{size} files.\n\n".colorize(:default)
"Inspecting #{size} files".colorize(:default)
end
end
private def finished_in_message(started, finished)
if started && finished
"Finished in #{to_human(finished - started)} \n\n".colorize(:default)
"Finished in #{to_human(finished - started)}".colorize(:default)
end
end
@ -86,11 +90,11 @@ module Ameba::Formatter
private def final_message(sources, failed_sources)
total = sources.size
failures = failed_sources.map { |f| f.issues.size }.sum
failures = failed_sources.sum(&.issues.size)
color = failures == 0 ? :green : :red
s = failures != 1 ? "s" : ""
"#{total} inspected, #{failures} failure#{s}.\n".colorize color
"#{total} inspected, #{failures} failure#{s}".colorize(color)
end
end
end

View file

@ -4,9 +4,8 @@ module Ameba::Formatter
# A formatter that shows the detailed explanation of the issue at
# a specific location.
class ExplainFormatter
LINE_BREAK = "\n"
HEADING = "## "
PREFIX = " "
HEADING = "## "
PREFIX = " "
include Util
@ -21,36 +20,36 @@ module Ameba::Formatter
# ExplainFormatter.new output,
# {file: path, line: line_number, column: column_number}
# ```
#
def initialize(@output, loc)
@location = Crystal::Location.new(loc[:file], loc[:line], loc[:column])
def initialize(@output, location)
@location = Crystal::Location.new(location[:file], location[:line], location[:column])
end
# Reports the explainations at the *@location*.
def finished(sources)
source = sources.find { |s| s.path == @location.filename }
source = sources.find(&.path.==(@location.filename))
return unless source
source.issues.each do |issue|
if (location = issue.location) &&
location.line_number == @location.line_number &&
location.column_number == @location.column_number
explain(source, issue)
end
end
issue = source.issues.find(&.location.==(@location))
return unless issue
explain(source, issue)
end
private def explain(source, issue)
rule = issue.rule
location, end_location =
issue.location, issue.end_location
return unless location
output_title "ISSUE INFO"
output_paragraph [
issue.message.colorize(:red).to_s,
@location.to_s.colorize(:cyan).to_s,
location.to_s.colorize(:cyan).to_s,
]
if affected_code = affected_code(source, @location)
if affected_code = affected_code(source, location, end_location, context_lines: 3)
output_title "AFFECTED CODE"
output_paragraph affected_code
end
@ -65,19 +64,19 @@ module Ameba::Formatter
end
private def output_title(title)
output << HEADING.colorize(:yellow) << title.colorize(:yellow) << LINE_BREAK
output << LINE_BREAK
output << HEADING.colorize(:yellow) << title.colorize(:yellow) << '\n'
output << '\n'
end
private def output_paragraph(paragraph : String)
output_paragraph(paragraph.split(LINE_BREAK))
output_paragraph(paragraph.lines)
end
private def output_paragraph(paragraph : Array(String))
paragraph.each do |line|
output << PREFIX << line << LINE_BREAK
output << PREFIX << line << '\n'
end
output << LINE_BREAK
output << '\n'
end
end
end

View file

@ -9,7 +9,7 @@ module Ameba::Formatter
@mutex.synchronize do
output.printf "%s:%d:%d: %s: [%s] %s\n",
source.path, loc.line_number, loc.column_number, e.rule.severity.symbol,
e.rule.name, e.message.gsub("\n", " ")
e.rule.name, e.message.gsub('\n', " ")
end
end
end

View file

@ -8,19 +8,20 @@ module Ameba::Formatter
def finished(sources)
super
issues = sources.map(&.issues).flatten
issues = sources.flat_map(&.issues)
unless issues.any? { |issue| !issue.disabled? }
@output << "No issues found. File is not generated.\n"
@output.puts "No issues found. File is not generated."
return
end
if issues.any? { |issue| issue.syntax? }
@output << "Unable to generate TODO file. Please fix syntax issues.\n"
if issues.any?(&.syntax?)
@output.puts "Unable to generate TODO file. Please fix syntax issues."
return
end
file = generate_todo_config issues
@output << "Created #{file.path}\n"
@output.puts "Created #{file.path}"
file
end
@ -40,7 +41,7 @@ module Ameba::Formatter
private def rule_issues_map(issues)
Hash(Rule::Base, Array(Issue)).new.tap do |h|
issues.each do |issue|
next if issue.disabled? || issue.rule.is_a? Rule::Lint::Syntax
next if issue.disabled? || issue.rule.is_a?(Rule::Lint::Syntax)
(h[issue.rule] ||= Array(Issue).new) << issue
end
end
@ -57,10 +58,9 @@ module Ameba::Formatter
end
private def rule_todo(rule, issues)
rule.excluded =
issues.map(&.location.try &.filename.try &.to_s)
.compact
.uniq!
rule.excluded = issues
.compact_map(&.location.try &.filename.try &.to_s)
.uniq!
{rule.name => rule}.to_yaml
end

View file

@ -1,22 +1,109 @@
module Ameba::Formatter
module Util
def affected_code(source, location, max_length = 100, placeholder = " ...", prompt = "> ")
line, column = location.line_number, location.column_number
affected_line = source.lines[line - 1]?
def deansify(message : String?) : String?
message.try &.gsub(/\x1b[^m]*m/, "").presence
end
return if affected_line.nil? || affected_line.strip.empty?
def trim(str, max_length = 120, ellipsis = " ...")
if (str.size - ellipsis.size) > max_length
str = str[0, max_length]
if str.size > ellipsis.size
str = str[0...-ellipsis.size] + ellipsis
end
end
str
end
if affected_line.size > max_length && column < max_length
affected_line = affected_line[0, max_length - placeholder.size - 1] + placeholder
def context(lines, lineno, context_lines = 3, remove_empty = true)
pre_context, post_context = %w[], %w[]
lines.each_with_index do |line, i|
case i + 1
when lineno - context_lines...lineno
pre_context << line
when lineno + 1..lineno + context_lines
post_context << line
end
end
stripped = affected_line.lstrip
position = column - (affected_line.size - stripped.size) + prompt.size
if remove_empty
# remove empty lines at the beginning ...
while pre_context.first?.try(&.blank?)
pre_context.shift
end
# ... and the end
while post_context.last?.try(&.blank?)
post_context.pop
end
end
{pre_context, post_context}
end
def affected_code(source, location, end_location = nil, context_lines = 0, max_length = 120, ellipsis = " ...", prompt = "> ")
lines = source.lines
lineno, column =
location.line_number, location.column_number
return unless affected_line = lines[lineno - 1]?.presence
if column < max_length
affected_line = trim(affected_line, max_length, ellipsis)
end
show_context = context_lines > 0
if show_context
pre_context, post_context =
context(lines, lineno, context_lines)
position = prompt.size + column
position -= 1
else
affected_line_size, affected_line =
affected_line.size, affected_line.lstrip
position = column - (affected_line_size - affected_line.size) + prompt.size
position -= 1
end
String.build do |str|
str << prompt << stripped << "\n"
str << " " * (position - 1)
if show_context
pre_context.try &.each do |line|
line = trim(line, max_length, ellipsis)
str << prompt
str.puts(line.colorize(:dark_gray))
end
end
str << prompt
str.puts(affected_line.colorize(:white))
str << (" " * position)
str << "^".colorize(:yellow)
if end_location
end_lineno = end_location.line_number
end_column = end_location.column_number
if end_lineno == lineno && end_column > column
end_position = end_column - column
end_position -= 1
str << ("-" * end_position).colorize(:dark_gray)
str << "^".colorize(:yellow)
end
end
str.puts
if show_context
post_context.try &.each do |line|
line = trim(line, max_length, ellipsis)
str << prompt
str.puts(line.colorize(:dark_gray))
end
end
end
end
end

View file

@ -7,12 +7,11 @@ module Ameba
# ```
# find_files_by_globs(["**/*.cr", "!lib"])
# ```
#
def find_files_by_globs(globs)
rejected = rejected_globs(globs)
selected = globs - rejected
expand(selected) - expand(rejected.map! { |p| p[1..-1] })
expand(selected) - expand(rejected.map!(&.[1..-1]))
end
# Expands globs. Globs can point to files or even directories.
@ -20,12 +19,11 @@ module Ameba
# ```
# expand(["spec/*.cr", "src"]) # => all files in src folder + first level specs
# ```
#
def expand(globs)
globs.map do |glob|
globs.flat_map do |glob|
glob += "/**/*.cr" if File.directory?(glob)
Dir[glob]
end.flatten.uniq!
end.uniq!
end
private def rejected_globs(globs)

View file

@ -36,9 +36,8 @@ module Ameba
# Time.epoch(1483859302)
# end
# ```
#
def location_disabled?(location, rule)
return false if Rule::SPECIAL.includes?(rule.name)
return false if rule.name.in?(Rule::SPECIAL)
return false unless line_number = location.try &.line_number.try &.- 1
return false unless line = lines[line_number]?
@ -65,7 +64,6 @@ module Ameba
# line = "# # ameba:disable Rule1, Rule2"
# parse_inline_directive(line) # => nil
# ```
#
def parse_inline_directive(line)
if directive = COMMENT_DIRECTIVE_REGEX.match(line)
return if commented_out?(line.gsub(directive[0], ""))
@ -89,8 +87,10 @@ module Ameba
private def line_disabled?(line, rule)
return false unless directive = parse_inline_directive(line)
Action.parse?(directive[:action]).try(&.disable?) &&
(directive[:rules].includes?(rule.name) || directive[:rules].includes?(rule.group))
return false unless Action.parse?(directive[:action]).try(&.disable?)
directive[:rules].includes?(rule.name) ||
directive[:rules].includes?(rule.group)
end
private def commented_out?(line)

View file

@ -1,22 +1,31 @@
module Ameba
# Represents an issue reported by Ameba.
record Issue,
struct Issue
enum Status
Enabled
Disabled
end
# A rule that triggers this issue.
rule : Rule::Base,
getter rule : Rule::Base
# Location of the issue.
location : Crystal::Location?,
getter location : Crystal::Location?
# End location of the issue.
end_location : Crystal::Location?,
getter end_location : Crystal::Location?
# Issue message.
message : String,
getter message : String
# Issue status.
status : Symbol? do
def disabled?
status == :disabled
getter status : Status
delegate :enabled?, :disabled?,
to: status
def initialize(@rule, @location, @end_location, @message, status : Status? = nil)
@status = status || Status::Enabled
end
def syntax?

View file

@ -5,37 +5,46 @@ module Ameba
getter issues = [] of Issue
# Adds a new issue to the list of issues.
def add_issue(rule, location : Crystal::Location?, end_location : Crystal::Location?, message, status = nil)
status ||= :disabled if location_disabled?(location, rule)
issues << Issue.new rule, location, end_location, message, status
def add_issue(rule, location, end_location, message, status : Issue::Status? = nil) : Issue
status ||=
Issue::Status::Disabled if location_disabled?(location, rule)
Issue.new(rule, location, end_location, message, status).tap do |issue|
issues << issue
end
end
# Adds a new issue for AST *node*.
def add_issue(rule, node : Crystal::ASTNode, message, **args)
add_issue rule, node.location, node.end_location, message, **args
# Adds a new issue for Crystal AST *node*.
def add_issue(rule, node : Crystal::ASTNode, message, status : Issue::Status? = nil) : Issue
add_issue rule, node.location, node.end_location, message, status
end
# Adds a new issue for Crystal *token*.
def add_issue(rule, token : Crystal::Token, message, **args)
add_issue rule, token.location, nil, message, **args
def add_issue(rule, token : Crystal::Token, message, status : Issue::Status? = nil) : Issue
add_issue rule, token.location, nil, message, status
end
# Adds a new issue for *location* defined by line and column numbers.
def add_issue(rule, location : Tuple(Int32, Int32), message, **args)
location = Crystal::Location.new path, *location
add_issue rule, location, nil, message, **args
def add_issue(rule, location : {Int32, Int32}, message, status : Issue::Status? = nil) : Issue
location =
Crystal::Location.new(path, *location)
add_issue rule, location, nil, message, status
end
# Adds a new issue for *location* and *end_location* defined by line and column numbers.
def add_issue(rule, location : Tuple(Int32, Int32), end_location : Tuple(Int32, Int32), message, **args)
location = Crystal::Location.new path, *location
end_location = Crystal::Location.new path, *end_location
add_issue rule, location, end_location, message, **args
def add_issue(rule, location : {Int32, Int32}, end_location : {Int32, Int32}, message, status : Issue::Status? = nil) : Issue
location =
Crystal::Location.new(path, *location)
end_location =
Crystal::Location.new(path, *end_location)
add_issue rule, location, end_location, message, status
end
# Returns true if the list of not disabled issues is empty, false otherwise.
# Returns `true` if the list of not disabled issues is empty, `false` otherwise.
def valid?
issues.reject(&.disabled?).empty?
issues.none?(&.enabled?)
end
end
end

View file

@ -10,7 +10,7 @@ module Ameba::Rule
# inherits from this struct:
#
# ```
# struct MyRule < Ameba::Rule::Base
# class MyRule < Ameba::Rule::Base
# def test(source)
# if invalid?(source)
# issue_for line, column, "Something wrong."
@ -26,8 +26,7 @@ module Ameba::Rule
# Enforces rules to implement an abstract `#test` method which
# is designed to test the source passed in. If source has issues
# that are tested by this rule, it should add an issue.
#
abstract struct Base
abstract class Base
include Config::RuleConfig
# This method is designed to test the source passed in. If source has issues
@ -50,22 +49,20 @@ module Ameba::Rule
# source = MyRule.new.catch(source)
# source.valid?
# ```
#
def catch(source : Source)
source.tap { |s| test s }
source.tap { test source }
end
# Returns a name of this rule, which is basically a class name.
#
# ```
# struct MyRule < Ameba::Rule::Base
# class MyRule < Ameba::Rule::Base
# def test(source)
# end
# end
#
# MyRule.new.name # => "MyRule"
# ```
#
def name
{{@type}}.rule_name
end
@ -73,13 +70,12 @@ module Ameba::Rule
# Returns a group this rule belong to.
#
# ```
# struct MyGroup::MyRule < Ameba::Rule::Base
# class MyGroup::MyRule < Ameba::Rule::Base
# # ...
# end
#
# MyGroup::MyRule.new.group # => "MyGroup"
# ```
#
def group
{{@type}}.group_name
end
@ -91,11 +87,10 @@ module Ameba::Rule
# ```
# my_rule.excluded?(source) # => true or false
# ```
#
def excluded?(source)
excluded.try &.any? do |path|
source.matches_path?(path) ||
Dir.glob(path).any? { |glob| source.matches_path? glob }
Dir.glob(path).any? { |glob| source.matches_path?(glob) }
end
end
@ -105,13 +100,12 @@ module Ameba::Rule
# ```
# my_rule.special? # => true or false
# ```
#
def special?
SPECIAL.includes? name
name.in?(SPECIAL)
end
def ==(other)
name == other.try &.name
name == other.try(&.name)
end
def hash
@ -123,11 +117,11 @@ module Ameba::Rule
end
protected def self.rule_name
name.gsub("Ameba::Rule::", "").gsub("::", "/")
name.gsub("Ameba::Rule::", "").gsub("::", '/')
end
protected def self.group_name
rule_name.split("/")[0...-1].join("/")
rule_name.split('/')[0...-1].join('/')
end
protected def self.subclasses
@ -146,7 +140,7 @@ module Ameba::Rule
# module Ameba
# # This is a test rule.
# # Does nothing.
# struct MyRule < Ameba::Rule::Base
# class MyRule < Ameba::Rule::Base
# def test(source)
# end
# end
@ -156,8 +150,11 @@ module Ameba::Rule
# ```
def self.parsed_doc
source = File.read(path_to_source_file)
nodes = Crystal::Parser.new(source).tap(&.wants_doc = true).parse
type_name = rule_name.split("/").last?
nodes = Crystal::Parser.new(source)
.tap(&.wants_doc = true)
.parse
type_name = rule_name.split('/').last?
DocFinder.new(nodes, type_name).doc
end
@ -190,7 +187,6 @@ module Ameba::Rule
# ```
# Ameba::Rule.rules # => [Rule1, Rule2, ....]
# ```
#
def self.rules
Base.subclasses
end

View file

@ -8,8 +8,7 @@ module Ameba::Rule::Layout
# Enabled: true
# MaxLength: 100
# ```
#
struct LineLength < Base
class LineLength < Base
properties do
enabled false
description "Disallows lines longer than `MaxLength` number of symbols"
@ -20,9 +19,7 @@ module Ameba::Rule::Layout
def test(source)
source.lines.each_with_index do |line, index|
next unless line.size > max_length
issue_for({index + 1, max_length + 1}, MSG)
issue_for({index + 1, max_length + 1}, MSG) if line.size > max_length
end
end
end

View file

@ -7,8 +7,7 @@ module Ameba::Rule::Layout
# Layout/TrailingBlankLines:
# Enabled: true
# ```
#
struct TrailingBlankLines < Base
class TrailingBlankLines < Base
properties do
description "Disallows trailing blank lines"
end
@ -24,9 +23,9 @@ module Ameba::Rule::Layout
source_lines_size = source_lines.size
return if source_lines_size == 1 && last_source_line.empty?
last_line_not_empty = !last_source_line.empty?
if source_lines_size >= 1 && (source_lines.last(2).join.strip.empty? || last_line_not_empty)
issue_for({source_lines_size - 1, 1}, last_line_not_empty ? MSG_FINAL_NEWLINE : MSG)
last_line_empty = last_source_line.empty?
if source_lines_size >= 1 && (source_lines.last(2).join.blank? || !last_line_empty)
issue_for({source_lines_size - 1, 1}, last_line_empty ? MSG : MSG_FINAL_NEWLINE)
end
end
end

View file

@ -7,8 +7,7 @@ module Ameba::Rule::Layout
# Layout/TrailingWhitespace:
# Enabled: true
# ```
#
struct TrailingWhitespace < Base
class TrailingWhitespace < Base
properties do
description "Disallows trailing whitespaces"
end
@ -17,8 +16,7 @@ module Ameba::Rule::Layout
def test(source)
source.lines.each_with_index do |line, index|
next unless line =~ /\s$/
issue_for({index + 1, line.size}, MSG)
issue_for({index + 1, line.size}, MSG) if line =~ /\s$/
end
end
end

View file

@ -17,8 +17,7 @@ module Ameba::Rule::Lint
# Lint/BadDirective:
# Enabled: true
# ```
#
struct BadDirective < Base
class BadDirective < Base
properties do
description "Reports bad comment directives"
end
@ -48,7 +47,9 @@ module Ameba::Rule::Lint
private def check_rules(source, token, rules)
bad_names = rules - ALL_RULE_NAMES - ALL_GROUP_NAMES
issue_for token, "Such rules do not exist: %s" % bad_names.join(", ") unless bad_names.empty?
return if bad_names.empty?
issue_for token, "Such rules do not exist: %s" % bad_names.join(", ")
end
end
end

View file

@ -19,23 +19,21 @@ module Ameba::Rule::Lint
# Lint/ComparisonToBoolean:
# Enabled: true
# ```
#
struct ComparisonToBoolean < Base
class ComparisonToBoolean < Base
properties do
enabled false
description "Disallows comparison to booleans"
end
MSG = "Comparison to a boolean is pointless"
MSG = "Comparison to a boolean is pointless"
OP_NAMES = %w(== != ===)
def test(source, node : Crystal::Call)
comparison = %w(== != ===).includes?(node.name)
to_boolean = node.args.first?.try &.is_a?(Crystal::BoolLiteral) ||
comparison = node.name.in?(OP_NAMES)
to_boolean = node.args.first?.try(&.is_a?(Crystal::BoolLiteral)) ||
node.obj.is_a?(Crystal::BoolLiteral)
return unless comparison && to_boolean
issue_for node, MSG
issue_for node, MSG if comparison && to_boolean
end
end
end

View file

@ -10,8 +10,7 @@ module Ameba::Rule::Lint
# Lint/DebuggerStatement:
# Enabled: true
# ```
#
struct DebuggerStatement < Base
class DebuggerStatement < Base
properties do
description "Disallows calls to debugger"
end

View file

@ -0,0 +1,31 @@
module Ameba::Rule::Lint
# A rule that reports duplicated require statements.
#
# ```
# require "./thing"
# require "./stuff"
# require "./thing" # duplicated require
# ```
#
# YAML configuration example:
#
# ```
# Lint/DuplicatedRequire:
# Enabled: true
# ```
class DuplicatedRequire < Base
properties do
description "Reports duplicated require statements"
end
MSG = "Duplicated require of `%s`"
def test(source)
nodes = AST::TopLevelNodesVisitor.new(source.ast).require_nodes
nodes.each_with_object([] of String) do |node, processed_require_strings|
issue_for(node, MSG % node.string) if processed_require_strings.includes?(node.string)
processed_require_strings << node.string
end
end
end
end

View file

@ -38,8 +38,7 @@ module Ameba::Rule::Lint
# Lint/EmptyEnsure
# Enabled: true
# ```
#
struct EmptyEnsure < Base
class EmptyEnsure < Base
properties do
description "Disallows empty ensure statement"
end

View file

@ -27,13 +27,12 @@ module Ameba::Rule::Lint
# Lint/EmptyExpression:
# Enabled: true
# ```
#
struct EmptyExpression < Base
class EmptyExpression < Base
include AST::Util
properties do
description "Disallows empty expressions"
enabled false
description "Disallows empty expressions"
end
MSG = "Avoid empty expression %s"
@ -41,8 +40,7 @@ module Ameba::Rule::Lint
def test(source, node : Crystal::NilLiteral)
exp = node_source(node, source.lines).try &.join
return if exp.nil? || exp == "nil"
return if exp.in?(nil, "nil")
issue_for node, MSG % exp
end

View file

@ -37,7 +37,7 @@ module Ameba::Rule::Lint
# Lint/EmptyLoop:
# Enabled: true
# ```
struct EmptyLoop < Base
class EmptyLoop < Base
include AST::Util
properties do

View file

@ -19,8 +19,7 @@ module Ameba::Rule::Lint
# Lint/HashDuplicatedKey:
# Enabled: true
# ```
#
struct HashDuplicatedKey < Base
class HashDuplicatedKey < Base
properties do
description "Disallows duplicated keys in hash literals"
end
@ -28,7 +27,7 @@ module Ameba::Rule::Lint
MSG = "Duplicated keys in hash literal: %s"
def test(source, node : Crystal::HashLiteral)
return unless (keys = duplicated_keys(node.entries)).any?
return if (keys = duplicated_keys(node.entries)).empty?
issue_for node, MSG % keys.join(", ")
end
@ -36,7 +35,7 @@ module Ameba::Rule::Lint
private def duplicated_keys(entries)
entries.map(&.key)
.group_by(&.itself)
.select { |_, v| v.size > 1 }
.tap(&.select! { |_, v| v.size > 1 })
.map { |k, _| k }
end
end

View file

@ -19,8 +19,7 @@ module Ameba::Rule::Lint
# Lint/LiteralInCondition:
# Enabled: true
# ```
#
struct LiteralInCondition < Base
class LiteralInCondition < Base
include AST::Util
properties do
@ -31,8 +30,7 @@ module Ameba::Rule::Lint
MSG = "Literal value found in conditional"
def check_node(source, node)
return unless literal?(node.cond)
issue_for node, MSG
issue_for node, MSG if literal?(node.cond)
end
def test(source, node : Crystal::If)

View file

@ -15,8 +15,7 @@ module Ameba::Rule::Lint
# Lint/LiteralInInterpolation
# Enabled: true
# ```
#
struct LiteralInInterpolation < Base
class LiteralInInterpolation < Base
include AST::Util
properties do

View file

@ -23,12 +23,12 @@ module Ameba::Rule::Lint
# StringArrayUnwantedSymbols: ',"'
# SymbolArrayUnwantedSymbols: ',:'
# ```
#
struct PercentArrays < Base
class PercentArrays < Base
properties do
description "Disallows some unwanted symbols in percent array literals"
string_array_unwanted_symbols ",\""
symbol_array_unwanted_symbols ",:"
string_array_unwanted_symbols %(,")
symbol_array_unwanted_symbols %(,:)
end
MSG = "Symbols `%s` may be unwanted in %s array literals"
@ -49,8 +49,6 @@ module Ameba::Rule::Lint
issue_for start_token.not_nil!, issue.not_nil!
end
issue = start_token = nil
else
# nop
end
end
end
@ -61,14 +59,11 @@ module Ameba::Rule::Lint
check_array_entry entry, string_array_unwanted_symbols, "%w"
when .starts_with? "%i"
check_array_entry entry, symbol_array_unwanted_symbols, "%i"
else
# nop
end
end
private def check_array_entry(entry, symbols, literal)
return unless entry =~ /[#{symbols}]/
MSG % {symbols, literal}
MSG % {symbols, literal} if entry =~ /[#{symbols}]/
end
end
end

View file

@ -22,8 +22,7 @@ module Ameba::Rule::Lint
# Lint/RandZero:
# Enabled: true
# ```
#
struct RandZero < Base
class RandZero < Base
properties do
description "Disallows rand zero calls"
end
@ -34,9 +33,9 @@ module Ameba::Rule::Lint
return unless node.name == "rand" &&
node.args.size == 1 &&
(arg = node.args.first) &&
(arg.is_a? Crystal::NumberLiteral) &&
arg.is_a?(Crystal::NumberLiteral) &&
(value = arg.value) &&
(value == "0" || value == "1")
value.in?("0", "1")
issue_for node, MSG % node
end

View file

@ -20,8 +20,7 @@ module Ameba::Rule::Lint
# Lint/RedundantStringCoersion
# Enabled: true
# ```
#
struct RedundantStringCoercion < Base
class RedundantStringCoercion < Base
include AST::Util
properties do
@ -31,7 +30,9 @@ module Ameba::Rule::Lint
MSG = "Redundant use of `Object#to_s` in interpolation"
def test(source, node : Crystal::StringInterpolation)
string_coercion_nodes(node).each { |n| issue_for n.name_location, n.end_location, MSG }
string_coercion_nodes(node).each do |n|
issue_for n.name_location, n.end_location, MSG
end
end
private def string_coercion_nodes(node)

View file

@ -26,8 +26,7 @@ module Ameba::Rule::Lint
# Lint/RedundantWithIndex:
# Enabled: true
# ```
#
struct RedundantWithIndex < Base
class RedundantWithIndex < Base
properties do
description "Disallows redundant `with_index` calls"
end
@ -35,15 +34,14 @@ module Ameba::Rule::Lint
def test(source, node : Crystal::Call)
args, block = node.args, node.block
return if args.size > 1 || block.nil? || with_index_arg?(block.not_nil!)
return if block.nil? || args.size > 1
return if with_index_arg?(block)
case node.name
when "with_index"
report source, node, "Remove redundant with_index"
when "each_with_index"
report source, node, "Use each instead of each_with_index"
else
# nop
end
end

View file

@ -27,21 +27,20 @@ module Ameba::Rule::Lint
# Lint/RedundantWithObject:
# Enabled: true
# ```
#
struct RedundantWithObject < Base
class RedundantWithObject < Base
properties do
description "Disallows redundant `with_object` calls"
end
MSG = "Use `each` instead of `each_with_object`"
def test(source, node : Crystal::Call)
return if node.name != "each_with_object" ||
node.args.size != 1 ||
node.block.nil? ||
with_index_arg?(node.block.not_nil!)
issue_for node.name_location,
node.name_end_location,
"Use each instead of each_with_object"
issue_for node.name_location, node.name_end_location, MSG
end
private def with_index_arg?(block : Crystal::Block)

View file

@ -35,8 +35,7 @@ module Ameba::Rule::Lint
# Lint/ShadowedArgument:
# Enabled: true
# ```
#
struct ShadowedArgument < Base
class ShadowedArgument < Base
properties do
description "Disallows shadowed arguments"
end

View file

@ -33,8 +33,7 @@ module Ameba::Rule::Lint
# Lint/ShadowedException:
# Enabled: true
# ```
#
struct ShadowedException < Base
class ShadowedException < Base
properties do
description "Disallows rescued exception that get shadowed"
end
@ -42,18 +41,17 @@ module Ameba::Rule::Lint
MSG = "Exception handler has shadowed exceptions: %s"
def test(source, node : Crystal::ExceptionHandler)
return unless excs = node.rescues
return unless excs = node.rescues.try &.map(&.types)
return if (excs = shadowed excs).empty?
if (excs = shadowed excs.map(&.types)).any?
issue_for node, MSG % excs.join(", ")
end
issue_for node, MSG % excs.join(", ")
end
private def shadowed(exceptions, exception_found = false)
previous_exceptions = [] of String
exceptions.reduce([] of String) do |shadowed, excs|
excs = excs ? excs.map(&.to_s) : ["Exception"]
excs = excs.try(&.map(&.to_s)) || %w[Exception]
if exception_found
shadowed.concat excs

View file

@ -30,8 +30,7 @@ module Ameba::Rule::Lint
# Lint/ShadowingOuterLocalVar:
# Enabled: true
# ```
#
struct ShadowingOuterLocalVar < Base
class ShadowingOuterLocalVar < Base
properties do
description "Disallows the usage of the same name as outer local variables" \
" for block or proc arguments."

View file

@ -49,8 +49,7 @@ module Ameba::Rule::Lint
# Lint/SharedVarInFiber:
# Enabled: true
# ```
#
struct SharedVarInFiber < Base
class SharedVarInFiber < Base
properties do
description "Disallows shared variables in fibers."
end
@ -77,7 +76,7 @@ module Ameba::Rule::Lint
declared_in = variable.assignments.first?.try &.branch
variable.assignments
.reject { |assign| assign.scope.spawn_block? }
.reject(&.scope.spawn_block?)
.any? do |assign|
assign.branch.try(&.in_loop?) && assign.branch != declared_in
end

View file

@ -0,0 +1,70 @@
module Ameba::Rule::Lint
# Checks if specs are focused.
#
# In specs `focus: true` is mainly used to focus on a spec
# item locally during development. However, if such change
# is committed, it silently runs only focused spec on all
# other enviroment, which is undesired.
#
# This is considered bad:
#
# ```
# describe MyClass, focus: true do
# end
#
# describe ".new", focus: true do
# end
#
# context "my context", focus: true do
# end
#
# it "works", focus: true do
# end
# ```
#
# And it should be written as the following:
#
# ```
# describe MyClass do
# end
#
# describe ".new" do
# end
#
# context "my context" do
# end
#
# it "works" do
# end
# ```
#
# YAML configuration example:
#
# ```
# Lint/SpecFocus:
# Enabled: true
# ```
class SpecFocus < Base
properties do
description "Reports focused spec items"
end
MSG = "Focused spec item detected"
SPEC_ITEM_NAMES = %w(describe context it pending)
def test(source)
return unless source.spec?
AST::NodeVisitor.new self, source
end
def test(source, node : Crystal::Call)
return unless node.name.in?(SPEC_ITEM_NAMES)
return unless node.block
arg = node.named_args.try &.find(&.name.== "focus")
issue_for arg, MSG if arg
end
end
end

View file

@ -18,8 +18,7 @@ module Ameba::Rule::Lint
# rescue e : Exception
# end
# ```
#
struct Syntax < Base
class Syntax < Base
properties do
description "Reports invalid Crystal syntax"
severity Ameba::Severity::Error

View file

@ -24,8 +24,7 @@ module Ameba::Rule::Lint
# Lint/UnneededDisableDirective
# Enabled: true
# ```
#
struct UnneededDisableDirective < Base
class UnneededDisableDirective < Base
properties do
description "Reports unneeded disable directives in comments"
end
@ -37,7 +36,7 @@ module Ameba::Rule::Lint
next unless token.type == :COMMENT
next unless directive = source.parse_inline_directive(token.value.to_s)
next unless names = unneeded_disables(source, directive, token.location)
next unless names.any?
next if names.empty?
issue_for token, MSG % names.join(", ")
end
@ -47,11 +46,12 @@ module Ameba::Rule::Lint
return unless directive[:action] == "disable"
directive[:rules].reject do |rule_name|
next if rule_name == self.name
source.issues.any? do |issue|
issue.rule.name == rule_name &&
issue.disabled? &&
issue_at_location?(source, issue, location)
end && rule_name != self.name
end
end
end

View file

@ -41,8 +41,7 @@ module Ameba::Rule::Lint
# Lint/UnreachableCode:
# Enabled: true
# ```
#
struct UnreachableCode < Base
class UnreachableCode < Base
include AST::Util
properties do

View file

@ -24,8 +24,7 @@ module Ameba::Rule::Lint
# IgnoreBlocks: false
# IgnoreProcs: false
# ```
#
struct UnusedArgument < Base
class UnusedArgument < Base
properties do
description "Disallows unused arguments"

View file

@ -25,8 +25,7 @@ module Ameba::Rule::Lint
# Lint/UselessAssign:
# Enabled: true
# ```
#
struct UselessAssign < Base
class UselessAssign < Base
properties do
description "Disallows useless variable assignments"
end

View file

@ -30,8 +30,7 @@ module Ameba::Rule::Lint
# Lint/UselessConditionInWhen:
# Enabled: true
# ```
#
struct UselessConditionInWhen < Base
class UselessConditionInWhen < Base
properties do
description "Disallows useless conditions in when"
end
@ -43,10 +42,7 @@ module Ameba::Rule::Lint
# simple implementation in future.
protected def check_node(source, when_node, cond)
cond_s = cond.to_s
return if when_node
.conds
.map(&.to_s)
.none? { |c| c == cond_s }
return if when_node.conds.none?(&.to_s.==(cond_s))
issue_for cond, MSG
end

View file

@ -8,8 +8,7 @@ module Ameba::Rule::Metrics
# Enabled: true
# MaxComplexity: 10
# ```
#
struct CyclomaticComplexity < Base
class CyclomaticComplexity < Base
properties do
description "Disallows methods with a cyclomatic complexity higher than `MaxComplexity`"
max_complexity 10
@ -21,17 +20,17 @@ module Ameba::Rule::Metrics
complexity = AST::CountingVisitor.new(node).count
if complexity > max_complexity && (location = node.name_location)
issue_for(
location,
def_name_end_location(node),
issue_for location, def_name_end_location(node),
MSG % {complexity, max_complexity}
)
end
end
private def def_name_end_location(node)
return unless location = node.name_location
line_number, column_number = location.line_number, location.column_number
line_number, column_number =
location.line_number, location.column_number
Crystal::Location.new(location.filename, line_number, column_number + node.name.size)
end
end

View file

@ -24,23 +24,21 @@ module Ameba::Rule::Performance
# - select
# - reject
# ```
#
struct AnyAfterFilter < Base
ANY_NAME = "any?"
MSG = "Use `#{ANY_NAME} {...}` instead of `%s {...}.#{ANY_NAME}`"
class AnyAfterFilter < Base
properties do
filter_names : Array(String) = %w(select reject)
description "Identifies usage of `any?` calls that follow filters."
filter_names : Array(String) = %w(select reject)
end
ANY_NAME = "any?"
MSG = "Use `any? {...}` instead of `%s {...}.any?`"
def test(source, node : Crystal::Call)
return unless node.name == ANY_NAME && (obj = node.obj)
return unless obj.is_a?(Crystal::Call) && obj.block && node.block.nil?
return unless obj.name.in?(filter_names)
if node.block.nil? && obj.is_a?(Crystal::Call) &&
filter_names.includes?(obj.name) && !obj.block.nil?
issue_for obj.name_location, node.name_end_location, MSG % obj.name
end
issue_for obj.name_location, node.name_end_location, MSG % obj.name
end
end
end

View file

@ -0,0 +1,44 @@
module Ameba::Rule::Performance
# This rule is used to identify usage of arg-less `Enumerable#any?` calls.
#
# Using `Enumerable#any?` instead of `Enumerable#empty?` might lead to an
# unexpected results (like `[nil, false].any? # => false`). In some cases
# it also might be less efficient, since it iterates until the block will
# return a _truthy_ value, instead of just checking if there's at least
# one value present.
#
# For example, this is considered invalid:
#
# ```
# [1, 2, 3].any?
# ```
#
# And it should be written as this:
#
# ```
# ![1, 2, 3].empty?
# ```
#
# YAML configuration example:
#
# ```
# Performance/AnyInsteadOfEmpty:
# Enabled: true
# ```
class AnyInsteadOfEmpty < Base
properties do
description "Identifies usage of arg-less `any?` calls."
end
ANY_NAME = "any?"
MSG = "Use `!{...}.empty?` instead of `{...}.any?`"
def test(source, node : Crystal::Call)
return unless node.name == ANY_NAME
return unless node.block.nil? && node.args.empty?
return unless node.obj
issue_for node.name_location, node.name_end_location, MSG
end
end
end

View file

@ -0,0 +1,75 @@
module Ameba::Rule::Performance
# This rule is used to identify usage of chained calls not utilizing
# the bang method variants.
#
# For example, this is considered inefficient:
#
# ```
# names = %w[Alice Bob]
# chars = names
# .flat_map(&.chars)
# .uniq
# .sort
# ```
#
# And can be written as this:
#
# ```
# names = %w[Alice Bob]
# chars = names
# .flat_map(&.chars)
# .uniq!
# .sort!
# ```
#
# YAML configuration example:
#
# ```
# Performance/ChainedCallWithNoBang:
# Enabled: true
# CallNames:
# - uniq
# - sort
# - sort_by
# - shuffle
# - reverse
# ```
class ChainedCallWithNoBang < Base
properties do
description "Identifies usage of chained calls not utilizing the bang method variants."
# All of those have bang method variants returning `self`
# and are not modifying the receiver type (like `compact` does),
# thus are safe to switch to the bang variant.
call_names : Array(String) = %w(uniq sort sort_by shuffle reverse)
end
# All these methods are allocating a new object
ALLOCATING_METHOD_NAMES = %w(
keys values values_at map map_with_index flat_map compact_map
flatten compact select reject sample group_by chunks tally merge
combinations repeated_combinations permutations repeated_permutations
transpose invert chars captures named_captures clone
)
MSG = "Use bang method variant `%s!` after chained `%s` call"
def test(source)
AST::NodeVisitor.new self, source, skip: [
Crystal::Macro,
Crystal::MacroExpression,
Crystal::MacroIf,
Crystal::MacroFor,
]
end
def test(source, node : Crystal::Call)
return unless (obj = node.obj).is_a?(Crystal::Call)
return unless node.name.in?(call_names)
return unless obj.name.in?(call_names) || obj.name.in?(ALLOCATING_METHOD_NAMES)
issue_for node.name_location, node.name_end_location,
MSG % {node.name, obj.name}
end
end
end

View file

@ -0,0 +1,48 @@
module Ameba::Rule::Performance
# This rule is used to identify usage of `compact` calls that follow `map`.
#
# For example, this is considered inefficient:
#
# ```
# %w[Alice Bob].map(&.match(/^A./)).compact
# ```
#
# And can be written as this:
#
# ```
# %w[Alice Bob].compact_map(&.match(/^A./))
# ```
#
# YAML configuration example:
#
# ```
# Performance/CompactAfterMap:
# Enabled: true
# ```
class CompactAfterMap < Base
properties do
description "Identifies usage of `compact` calls that follow `map`."
end
COMPACT_NAME = "compact"
MAP_NAME = "map"
MSG = "Use `compact_map {...}` instead of `map {...}.compact`"
def test(source)
AST::NodeVisitor.new self, source, skip: [
Crystal::Macro,
Crystal::MacroExpression,
Crystal::MacroIf,
Crystal::MacroFor,
]
end
def test(source, node : Crystal::Call)
return unless node.name == COMPACT_NAME && (obj = node.obj)
return unless obj.is_a?(Crystal::Call) && obj.block
return unless obj.name == MAP_NAME
issue_for obj.name_location, node.name_end_location, MSG
end
end
end

View file

@ -23,17 +23,16 @@ module Ameba::Rule::Performance
# FilterNames:
# - select
# ```
#
struct FirstLastAfterFilter < Base
class FirstLastAfterFilter < Base
properties do
description "Identifies usage of `first/last/first?/last?` calls that follow filters."
filter_names : Array(String) = %w(select)
end
CALL_NAMES = %w(first last first? last?)
MSG = "Use `find {...}` instead of `%s {...}.%s`"
MSG_REVERSE = "Use `reverse_each.find {...}` instead of `%s {...}.%s`"
properties do
filter_names : Array(String) = %w(select)
description "Identifies usage of `first/last/first?/last?` calls that follow filters."
end
def test(source)
AST::NodeVisitor.new self, source, skip: [
Crystal::Macro,
@ -44,14 +43,13 @@ module Ameba::Rule::Performance
end
def test(source, node : Crystal::Call)
return unless CALL_NAMES.includes?(node.name) && (obj = node.obj)
return if node.args.any?
return unless node.name.in?(CALL_NAMES) && (obj = node.obj)
return unless obj.is_a?(Crystal::Call) && obj.block
return unless node.block.nil? && node.args.empty?
return unless obj.name.in?(filter_names)
if node.block.nil? && obj.is_a?(Crystal::Call) &&
filter_names.includes?(obj.name) && !obj.block.nil?
message = node.name.includes?(CALL_NAMES.first) ? MSG : MSG_REVERSE
issue_for obj.name_location, node.name_end_location, message % {obj.name, node.name}
end
message = node.name.includes?(CALL_NAMES.first) ? MSG : MSG_REVERSE
issue_for obj.name_location, node.name_end_location, message % {obj.name, node.name}
end
end
end

View file

@ -0,0 +1,48 @@
module Ameba::Rule::Performance
# This rule is used to identify usage of `flatten` calls that follow `map`.
#
# For example, this is considered inefficient:
#
# ```
# %w[Alice Bob].map(&.chars).flatten
# ```
#
# And can be written as this:
#
# ```
# %w[Alice Bob].flat_map(&.chars)
# ```
#
# YAML configuration example:
#
# ```
# Performance/FlattenAfterMap:
# Enabled: true
# ```
class FlattenAfterMap < Base
properties do
description "Identifies usage of `flatten` calls that follow `map`."
end
FLATTEN_NAME = "flatten"
MAP_NAME = "map"
MSG = "Use `flat_map {...}` instead of `map {...}.flatten`"
def test(source)
AST::NodeVisitor.new self, source, skip: [
Crystal::Macro,
Crystal::MacroExpression,
Crystal::MacroIf,
Crystal::MacroFor,
]
end
def test(source, node : Crystal::Call)
return unless node.name == FLATTEN_NAME && (obj = node.obj)
return unless obj.is_a?(Crystal::Call) && obj.block
return unless obj.name == MAP_NAME
issue_for obj.name_location, node.name_end_location, MSG
end
end
end

View file

@ -0,0 +1,52 @@
module Ameba::Rule::Performance
# This rule is used to identify usage of `join/sum/product` calls
# that follow `map`.
#
# For example, this is considered inefficient:
#
# ```
# (1..3).map(&.to_s).join('.')
# (1..3).map(&.*(2)).sum
# ```
#
# And can be written as this:
#
# ```
# (1..3).join('.', &.to_s)
# (1..3).sum(&.*(2))
# ```
#
# YAML configuration example:
#
# ```
# Performance/MapInsteadOfBlock:
# Enabled: true
# ```
class MapInsteadOfBlock < Base
properties do
description "Identifies usage of `join/sum/product` calls that follow `map`."
end
CALL_NAMES = %w(join sum product)
MAP_NAME = "map"
MSG = "Use `%s {...}` instead of `map {...}.%s`"
def test(source)
AST::NodeVisitor.new self, source, skip: [
Crystal::Macro,
Crystal::MacroExpression,
Crystal::MacroIf,
Crystal::MacroFor,
]
end
def test(source, node : Crystal::Call)
return unless node.name.in?(CALL_NAMES) && (obj = node.obj)
return unless obj.is_a?(Crystal::Call) && obj.block
return unless obj.name == MAP_NAME
issue_for obj.name_location, node.name_end_location,
MSG % {node.name, node.name}
end
end
end

View file

@ -30,16 +30,15 @@ module Ameba::Rule::Performance
# - select
# - reject
# ```
#
struct SizeAfterFilter < Base
SIZE_NAME = "size"
MSG = "Use `count {...}` instead of `%s {...}.#{SIZE_NAME}`."
class SizeAfterFilter < Base
properties do
filter_names : Array(String) = %w(select reject)
description "Identifies usage of `size` calls that follow filter"
filter_names : Array(String) = %w(select reject)
end
SIZE_NAME = "size"
MSG = "Use `count {...}` instead of `%s {...}.size`."
def test(source)
AST::NodeVisitor.new self, source, skip: [
Crystal::Macro,
@ -51,11 +50,10 @@ module Ameba::Rule::Performance
def test(source, node : Crystal::Call)
return unless node.name == SIZE_NAME && (obj = node.obj)
return unless obj.is_a?(Crystal::Call) && obj.block
return unless obj.name.in?(filter_names)
if obj.is_a?(Crystal::Call) &&
filter_names.includes?(obj.name) && !obj.block.nil?
issue_for obj.name_location, node.name_end_location, MSG % obj.name
end
issue_for obj.name_location, node.name_end_location, MSG % obj.name
end
end
end

View file

@ -21,8 +21,7 @@ module Ameba::Rule::Style
# Style/ConstantNames:
# Enabled: true
# ```
#
struct ConstantNames < Base
class ConstantNames < Base
properties do
description "Enforces constant names to be in screaming case"
end
@ -30,11 +29,11 @@ module Ameba::Rule::Style
MSG = "Constant name should be screaming-cased: %s, not %s"
def test(source, node : Crystal::Assign)
if (target = node.target).is_a? Crystal::Path
if (target = node.target).is_a?(Crystal::Path)
name = target.names.first
expected = name.upcase
return if expected == name || name.camelcase == name
return if name.in?(expected, name.camelcase)
issue_for target, MSG % {expected, name}
end

View file

@ -0,0 +1,74 @@
module Ameba::Rule::Style
# This rule is used to identify usage of `is_a?/nil?` calls within filters.
#
# For example, this is considered invalid:
#
# ```
# matches = %w[Alice Bob].map(&.match(/^A./))
#
# matches.any?(&.is_a?(Regex::MatchData)) # => true
# matches.one?(&.nil?) # => true
#
# typeof(matches.reject(&.nil?)) # => Array(Regex::MatchData | Nil)
# typeof(matches.select(&.is_a?(Regex::MatchData))) # => Array(Regex::MatchData | Nil)
# ```
#
# And it should be written as this:
#
# ```
# matches = %w[Alice Bob].map(&.match(/^A./))
#
# matches.any?(Regex::MatchData) # => true
# matches.one?(Nil) # => true
#
# typeof(matches.reject(Nil)) # => Array(Regex::MatchData)
# typeof(matches.select(Regex::MatchData)) # => Array(Regex::MatchData)
# ```
#
# YAML configuration example:
#
# ```
# Style/IsAFilter:
# Enabled: true
# FilterNames:
# - select
# - reject
# - any?
# - all?
# - none?
# - one?
# ```
class IsAFilter < Base
properties do
description "Identifies usage of `is_a?/nil?` calls within filters."
filter_names : Array(String) = %w(select reject any? all? none? one?)
end
MSG = "Use `%s(%s)` instead of `%s {...}`"
def test(source, node : Crystal::Call)
return unless node.name.in?(filter_names)
return unless (block = node.block)
return unless (body = block.body).is_a?(Crystal::IsA)
return unless (path = body.const).is_a?(Crystal::Path)
return unless body.obj.is_a?(Crystal::Var)
name = path.names.join("::")
name = "::#{name}" if path.global? && !body.nil_check?
end_location = node.end_location
if !end_location || end_location.try(&.column_number.zero?)
if end_location = path.end_location
end_location = Crystal::Location.new(
end_location.filename,
end_location.line_number,
end_location.column_number + 1
)
end
end
issue_for node.name_location, end_location,
MSG % {node.name, name, node.name}
end
end
end

View file

@ -4,7 +4,7 @@ module Ameba::Rule::Style
# This is considered bad:
#
# ```
# var.is_a? Nil
# var.is_a?(Nil)
# ```
#
# And needs to be written as:
@ -19,8 +19,7 @@ module Ameba::Rule::Style
# Style/IsANil:
# Enabled: true
# ```
#
struct IsANil < Base
class IsANil < Base
properties do
description "Disallows calls to `is_a?(Nil)` in favor of `nil?`"
end
@ -32,7 +31,10 @@ module Ameba::Rule::Style
return if node.nil_check?
const = node.const
issue_for const, MSG if const.is_a?(Crystal::Path) && const.names == PATH_NIL_NAMES
return unless const.is_a?(Crystal::Path)
return unless const.names == PATH_NIL_NAMES
issue_for const, MSG
end
end
end

View file

@ -26,12 +26,11 @@ module Ameba::Rule::Style
# Enabled: true
# IntMinDigits: 5 # i.e. integers higher than 9999
# ```
#
struct LargeNumbers < Base
class LargeNumbers < Base
properties do
enabled false
description "Disallows usage of large numbers without underscore"
int_min_digits 5
enabled false
end
MSG = "Large numbers should be written with underscores: %s"
@ -53,7 +52,7 @@ module Ameba::Rule::Style
end
private def allowed?(_sign, value, fraction, _suffix)
return true if !fraction.nil? && fraction.size > 3
return true if fraction && fraction.size > 3
digits = value.chars.select &.to_s.=~ /[0-9]/
digits.size >= int_min_digits
@ -71,7 +70,7 @@ module Ameba::Rule::Style
value.chars.reject(&.== '_').each_slice(by) do |slice|
slices << (yield slice).join
end
end.join("_")
end.join('_')
end
private def parse_number(value)
@ -83,7 +82,7 @@ module Ameba::Rule::Style
end
private def parse_sign(value)
if "+-".includes?(value[0])
if value[0].in?('+', '-')
sign = value[0]
value = value[1..-1]
end
@ -91,7 +90,7 @@ module Ameba::Rule::Style
end
private def parse_suffix(value)
if pos = (value =~ /e/ || value =~ /_?(i|u|f)/)
if pos = (value =~ /(e|_?(i|u|f))/)
suffix = value[pos..-1]
value = value[0..pos - 1]
end

View file

@ -37,8 +37,7 @@ module Ameba::Rule::Style
# Style/MethodNames:
# Enabled: true
# ```
#
struct MethodNames < Base
class MethodNames < Base
properties do
description "Enforces method names to be in underscored case"
end
@ -51,7 +50,7 @@ module Ameba::Rule::Style
line_number = node.location.try &.line_number
column_number = node.name_location.try &.column_number
return if line_number.nil? || column_number.nil?
return unless line_number && column_number
issue_for(
{line_number, column_number},

View file

@ -26,8 +26,7 @@ module Ameba::Rule::Style
# Style/NegatedConditionsInUnless:
# Enabled: true
# ```
#
struct NegatedConditionsInUnless < Base
class NegatedConditionsInUnless < Base
properties do
description "Disallows negated conditions in unless"
end
@ -35,8 +34,7 @@ module Ameba::Rule::Style
MSG = "Avoid negated conditions in unless blocks"
def test(source, node : Crystal::Unless)
return unless negated_condition? node.cond
issue_for node, MSG
issue_for node, MSG if negated_condition?(node.cond)
end
private def negated_condition?(node)
@ -44,7 +42,7 @@ module Ameba::Rule::Style
when Crystal::BinaryOp
negated_condition?(node.left) || negated_condition?(node.right)
when Crystal::Expressions
node.expressions.any? { |e| negated_condition? e }
node.expressions.any? { |e| negated_condition?(e) }
when Crystal::Not
true
else

View file

@ -28,11 +28,10 @@ module Ameba::Rule::Style
# Style/PredicateName:
# Enabled: true
# ```
#
struct PredicateName < Base
class PredicateName < Base
properties do
description "Disallows tautological predicate names"
enabled false
description "Disallows tautological predicate names"
end
MSG = "Favour method name '%s?' over '%s'"

View file

@ -55,9 +55,9 @@ module Ameba::Rule::Style
# Style/RedundantBegin:
# Enabled: true
# ```
#
struct RedundantBegin < Base
class RedundantBegin < Base
include AST::Util
properties do
description "Disallows redundant begin blocks"
end
@ -65,9 +65,7 @@ module Ameba::Rule::Style
MSG = "Redundant `begin` block detected"
def test(source, node : Crystal::Def)
return unless redundant_begin?(source, node)
issue_for node, MSG
issue_for node, MSG if redundant_begin?(source, node)
end
private def redundant_begin?(source, node)
@ -76,8 +74,6 @@ module Ameba::Rule::Style
redundant_begin_in_handler?(source, body, node)
when Crystal::Expressions
redundant_begin_in_expressions?(body)
else
# nop
end
end
@ -88,7 +84,7 @@ module Ameba::Rule::Style
private def redundant_begin_in_handler?(source, handler, node)
return false if begin_exprs_in_handler?(handler) || inner_handler?(handler)
code = node_source(node, source.lines).try &.join("\n")
code = node_source(node, source.lines).try &.join('\n')
def_redundant_begin? code if code
rescue
false
@ -107,6 +103,7 @@ module Ameba::Rule::Style
private def def_redundant_begin?(code)
lexer = Crystal::Lexer.new code
in_body = in_argument_list = false
loop do
token = lexer.next_token
@ -122,6 +119,7 @@ module Ameba::Rule::Style
when :NEWLINE
in_body = true unless in_argument_list
when :SPACE
# ignore
else
return false if in_body
end

View file

@ -96,9 +96,10 @@ module Ameba::Rule::Style
# AllowMultiNext: true
# AllowEmptyNext: true
# ```
struct RedundantNext < Base
class RedundantNext < Base
properties do
description "Reports redundant next expressions"
allow_multi_next true
allow_empty_next true
end

View file

@ -93,9 +93,10 @@ module Ameba::Rule::Style
# AllowMutliReturn: true
# AllowEmptyReturn: true
# ```
struct RedundantReturn < Base
class RedundantReturn < Base
properties do
description "Reports redundant return expressions"
allow_multi_return true
allow_empty_return true
end

View file

@ -51,8 +51,7 @@ module Ameba::Rule::Style
# Style/TypeNames:
# Enabled: true
# ```
#
struct TypeNames < Base
class TypeNames < Base
properties do
description "Enforces type names in camelcase manner"
end
@ -62,7 +61,7 @@ module Ameba::Rule::Style
private def check_node(source, node)
name = node.name.to_s
expected = name.camelcase
return if expected == name
return if name == expected
issue_for node, MSG % {expected, name}
end

View file

@ -42,8 +42,7 @@ module Ameba::Rule::Style
# Style/UnlessElse:
# Enabled: true
# ```
#
struct UnlessElse < Base
class UnlessElse < Base
properties do
description "Disallows the use of an `else` block with the `unless`"
end
@ -51,8 +50,7 @@ module Ameba::Rule::Style
MSG = "Favour if over unless with else"
def test(source, node : Crystal::Unless)
return if node.else.nop?
issue_for node, MSG
issue_for node, MSG unless node.else.nop?
end
end
end

View file

@ -22,8 +22,7 @@ module Ameba::Rule::Style
# Style/VariableNames:
# Enabled: true
# ```
#
struct VariableNames < Base
class VariableNames < Base
properties do
description "Enforces variable names to be in underscored case"
end

Some files were not shown because too many files have changed in this diff Show more