mirror of
https://gitea.invidious.io/iv-org/shard-ameba.git
synced 2024-08-15 00:53:29 +00:00
Merge pull request #207 from crystal-ameba/release/0.14.0
Release 0.14.0
This commit is contained in:
commit
d8c32f0045
106 changed files with 2045 additions and 448 deletions
|
@ -35,7 +35,7 @@
|
||||||
## About
|
## About
|
||||||
|
|
||||||
Ameba is a static code analysis tool for the Crystal language.
|
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.
|
also catches code smells and wrong code constructions.
|
||||||
|
|
||||||
See also [Roadmap](https://github.com/crystal-ameba/ameba/wiki).
|
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
|
```sh
|
||||||
$ ameba
|
$ ameba
|
||||||
Inspecting 107 files.
|
Inspecting 107 files
|
||||||
|
|
||||||
...............F.....................F....................................................................
|
...............F.....................F....................................................................
|
||||||
|
|
||||||
|
@ -61,9 +61,7 @@ src/ameba/formatter/base_formatter.cr:12:7
|
||||||
^
|
^
|
||||||
|
|
||||||
Finished in 542.64 milliseconds
|
Finished in 542.64 milliseconds
|
||||||
|
129 inspected, 2 failures
|
||||||
129 inspected, 2 failures.
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Run in parallel
|
### Run in parallel
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
name: ameba
|
name: ameba
|
||||||
version: 0.13.4
|
version: 0.14.0
|
||||||
|
|
||||||
authors:
|
authors:
|
||||||
- Vitalii Elenhaupt <velenhaupt@gmail.com>
|
- Vitalii Elenhaupt <velenhaupt@gmail.com>
|
||||||
|
|
|
@ -113,7 +113,7 @@ module Ameba::AST
|
||||||
it "adds a new variable to the scope" do
|
it "adds a new variable to the scope" do
|
||||||
scope = Scope.new as_node("")
|
scope = Scope.new as_node("")
|
||||||
scope.add_variable(Crystal::Var.new "foo")
|
scope.add_variable(Crystal::Var.new "foo")
|
||||||
scope.variables.any?.should be_true
|
scope.variables.empty?.should be_false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -48,7 +48,7 @@ module Ameba::AST
|
||||||
it "assigns the variable (creates a new assignment)" do
|
it "assigns the variable (creates a new assignment)" do
|
||||||
variable = Variable.new(var_node, scope)
|
variable = Variable.new(var_node, scope)
|
||||||
variable.assign(assign_node, scope)
|
variable.assign(assign_node, scope)
|
||||||
variable.assignments.any?.should be_true
|
variable.assignments.empty?.should be_false
|
||||||
end
|
end
|
||||||
|
|
||||||
it "can create multiple assignments" do
|
it "can create multiple assignments" do
|
||||||
|
@ -64,7 +64,7 @@ module Ameba::AST
|
||||||
variable = Variable.new(var_node, scope)
|
variable = Variable.new(var_node, scope)
|
||||||
variable.assign(as_node("foo=1"), scope)
|
variable.assign(as_node("foo=1"), scope)
|
||||||
variable.reference(var_node, scope)
|
variable.reference(var_node, scope)
|
||||||
variable.references.any?.should be_true
|
variable.references.empty?.should be_false
|
||||||
end
|
end
|
||||||
|
|
||||||
it "adds a reference to the scope" do
|
it "adds a reference to the scope" do
|
||||||
|
|
17
spec/ameba/ast/visitors/top_level_nodes_visitor_spec.cr
Normal file
17
spec/ameba/ast/visitors/top_level_nodes_visitor_spec.cr
Normal 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
|
|
@ -42,6 +42,16 @@ module Ameba::Cli
|
||||||
c.except.should eq %w(RULE1 RULE2)
|
c.except.should eq %w(RULE1 RULE2)
|
||||||
end
|
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
|
it "defaults all? flag to false" do
|
||||||
c = Cli.parse_args %w(file.cr)
|
c = Cli.parse_args %w(file.cr)
|
||||||
c.all?.should eq false
|
c.all?.should eq false
|
||||||
|
|
|
@ -120,7 +120,7 @@ module Ameba
|
||||||
it "returns list of sources" do
|
it "returns list of sources" do
|
||||||
config.sources.size.should be > 0
|
config.sources.size.should be > 0
|
||||||
config.sources.first.should be_a Source
|
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
|
end
|
||||||
|
|
||||||
it "returns a list of sources mathing globs" do
|
it "returns a list of sources mathing globs" do
|
||||||
|
@ -130,7 +130,7 @@ module Ameba
|
||||||
|
|
||||||
it "returns a lisf of sources excluding 'Excluded'" do
|
it "returns a lisf of sources excluding 'Excluded'" do
|
||||||
config.excluded = %w(**/config_spec.cr)
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ module Ameba::Formatter
|
||||||
describe "#started" do
|
describe "#started" do
|
||||||
it "writes started message" do
|
it "writes started message" do
|
||||||
subject.started [Source.new ""]
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ module Ameba::Formatter
|
||||||
describe "#finished" do
|
describe "#finished" do
|
||||||
it "writes a final message" do
|
it "writes a final message" do
|
||||||
subject.finished [Source.new ""]
|
subject.finished [Source.new ""]
|
||||||
output.to_s.should contain "1 inspected, 0 failures."
|
output.to_s.should contain "1 inspected, 0 failures"
|
||||||
end
|
end
|
||||||
|
|
||||||
it "writes the elapsed time" do
|
it "writes the elapsed time" do
|
||||||
|
@ -45,7 +45,7 @@ module Ameba::Formatter
|
||||||
end
|
end
|
||||||
subject.finished [s]
|
subject.finished [s]
|
||||||
log = output.to_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 "DummyRuleError"
|
||||||
log.should contain "NamedRuleError"
|
log.should contain "NamedRuleError"
|
||||||
end
|
end
|
||||||
|
@ -60,7 +60,7 @@ module Ameba::Formatter
|
||||||
end
|
end
|
||||||
subject.finished [s]
|
subject.finished [s]
|
||||||
log = output.to_s
|
log = output.to_s
|
||||||
log.should contain "> a = 22"
|
log.should contain "> \e[97ma = 22"
|
||||||
log.should contain " \e[33m^\e[0m"
|
log.should contain " \e[33m^\e[0m"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -99,7 +99,7 @@ module Ameba::Formatter
|
||||||
s.add_issue(DummyRule.new, location: {1, 1},
|
s.add_issue(DummyRule.new, location: {1, 1},
|
||||||
message: "DummyRuleError", status: :disabled)
|
message: "DummyRuleError", status: :disabled)
|
||||||
subject.finished [s]
|
subject.finished [s]
|
||||||
output.to_s.should contain "1 inspected, 0 failures."
|
output.to_s.should contain "1 inspected, 0 failures"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,6 +8,65 @@ module Ameba::Formatter
|
||||||
subject = Subject.new
|
subject = Subject.new
|
||||||
|
|
||||||
describe Util do
|
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
|
describe "#affected_code" do
|
||||||
it "returns nil if there is no such a line number" do
|
it "returns nil if there is no such a line number" do
|
||||||
source = Source.new %(
|
source = Source.new %(
|
||||||
|
@ -22,7 +81,38 @@ module Ameba::Formatter
|
||||||
a = 1
|
a = 1
|
||||||
)
|
)
|
||||||
location = Crystal::Location.new("filename", 1, 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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -42,9 +42,22 @@ module Ameba
|
||||||
location: nil,
|
location: nil,
|
||||||
end_location: nil,
|
end_location: nil,
|
||||||
message: "",
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
48
spec/ameba/rule/lint/duplicated_require_spec.cr
Normal file
48
spec/ameba/rule/lint/duplicated_require_spec.cr
Normal 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
|
|
@ -77,7 +77,7 @@ module Ameba::Rule::Lint
|
||||||
issue.rule.should_not be_nil
|
issue.rule.should_not be_nil
|
||||||
issue.location.to_s.should eq "source.cr:2:14"
|
issue.location.to_s.should eq "source.cr:2:14"
|
||||||
issue.end_location.to_s.should eq "source.cr:2:30"
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
127
spec/ameba/rule/lint/spec_focus_spec.cr
Normal file
127
spec/ameba/rule/lint/spec_focus_spec.cr
Normal 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
|
46
spec/ameba/rule/performance/any_instead_of_empty_spec.cr
Normal file
46
spec/ameba/rule/performance/any_instead_of_empty_spec.cr
Normal 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
|
|
@ -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
|
50
spec/ameba/rule/performance/compact_after_map_spec.cr
Normal file
50
spec/ameba/rule/performance/compact_after_map_spec.cr
Normal 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
|
43
spec/ameba/rule/performance/flatten_after_map_spec.cr
Normal file
43
spec/ameba/rule/performance/flatten_after_map_spec.cr
Normal 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
|
59
spec/ameba/rule/performance/map_instead_of_block_spec.cr
Normal file
59
spec/ameba/rule/performance/map_instead_of_block_spec.cr
Normal 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
|
64
spec/ameba/rule/style/is_a_filter_spec.cr
Normal file
64
spec/ameba/rule/style/is_a_filter_spec.cr
Normal 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
|
214
spec/ameba/rule/style/verbose_block_spec.cr
Normal file
214
spec/ameba/rule/style/verbose_block_spec.cr
Normal 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
|
|
@ -4,9 +4,7 @@ module Ameba
|
||||||
describe Severity do
|
describe Severity do
|
||||||
describe "#symbol" do
|
describe "#symbol" do
|
||||||
it "returns the symbol for each severity in the enum" do
|
it "returns the symbol for each severity in the enum" do
|
||||||
Severity.values.each do |severity|
|
Severity.values.each(&.symbol.should_not(be_nil))
|
||||||
severity.symbol.should_not be_nil
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns symbol for Error" do
|
it "returns symbol for Error" do
|
||||||
|
|
|
@ -23,6 +23,18 @@ module Ameba
|
||||||
end
|
end
|
||||||
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
|
describe "#matches_path?" do
|
||||||
it "returns true if source's path is matched" do
|
it "returns true if source's path is matched" do
|
||||||
s = Source.new "", "source.cr"
|
s = Source.new "", "source.cr"
|
||||||
|
|
|
@ -3,7 +3,7 @@ require "../src/ameba"
|
||||||
|
|
||||||
module Ameba
|
module Ameba
|
||||||
# Dummy Rule which does nothing.
|
# Dummy Rule which does nothing.
|
||||||
struct DummyRule < Rule::Base
|
class DummyRule < Rule::Base
|
||||||
properties do
|
properties do
|
||||||
description : String = "Dummy rule that does nothing."
|
description : String = "Dummy rule that does nothing."
|
||||||
end
|
end
|
||||||
|
@ -35,7 +35,7 @@ module Ameba
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
struct NamedRule < Rule::Base
|
class NamedRule < Rule::Base
|
||||||
properties do
|
properties do
|
||||||
description "A rule with a custom name."
|
description "A rule with a custom name."
|
||||||
end
|
end
|
||||||
|
@ -45,7 +45,7 @@ module Ameba
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
struct ErrorRule < Rule::Base
|
class ErrorRule < Rule::Base
|
||||||
properties do
|
properties do
|
||||||
description "Always adds an error at 1:1"
|
description "Always adds an error at 1:1"
|
||||||
end
|
end
|
||||||
|
@ -55,7 +55,7 @@ module Ameba
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
struct ScopeRule < Rule::Base
|
class ScopeRule < Rule::Base
|
||||||
@[YAML::Field(ignore: true)]
|
@[YAML::Field(ignore: true)]
|
||||||
getter scopes = [] of AST::Scope
|
getter scopes = [] of AST::Scope
|
||||||
|
|
||||||
|
@ -68,7 +68,7 @@ module Ameba
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
struct FlowExpressionRule < Rule::Base
|
class FlowExpressionRule < Rule::Base
|
||||||
@[YAML::Field(ignore: true)]
|
@[YAML::Field(ignore: true)]
|
||||||
getter expressions = [] of AST::FlowExpression
|
getter expressions = [] of AST::FlowExpression
|
||||||
|
|
||||||
|
@ -81,7 +81,7 @@ module Ameba
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
struct RedundantControlExpressionRule < Rule::Base
|
class RedundantControlExpressionRule < Rule::Base
|
||||||
@[YAML::Field(ignore: true)]
|
@[YAML::Field(ignore: true)]
|
||||||
getter nodes = [] of Crystal::ASTNode
|
getter nodes = [] of Crystal::ASTNode
|
||||||
|
|
||||||
|
@ -95,7 +95,7 @@ module Ameba
|
||||||
end
|
end
|
||||||
|
|
||||||
# A rule that always raises an error
|
# A rule that always raises an error
|
||||||
struct RaiseRule < Rule::Base
|
class RaiseRule < Rule::Base
|
||||||
property should_raise = false
|
property should_raise = false
|
||||||
|
|
||||||
properties do
|
properties do
|
||||||
|
|
|
@ -20,7 +20,6 @@ require "./ameba/formatter/*"
|
||||||
#
|
#
|
||||||
# Ameba.run config
|
# Ameba.run config
|
||||||
# ```
|
# ```
|
||||||
#
|
|
||||||
module Ameba
|
module Ameba
|
||||||
extend self
|
extend self
|
||||||
|
|
||||||
|
@ -35,7 +34,6 @@ module Ameba
|
||||||
# Ameba.run
|
# Ameba.run
|
||||||
# Ameba.run config
|
# Ameba.run config
|
||||||
# ```
|
# ```
|
||||||
#
|
|
||||||
def run(config = Config.load)
|
def run(config = Config.load)
|
||||||
Runner.new(config).run
|
Runner.new(config).run
|
||||||
end
|
end
|
||||||
|
|
|
@ -44,7 +44,6 @@ module Ameba::AST
|
||||||
# end
|
# end
|
||||||
# end
|
# end
|
||||||
# ```
|
# ```
|
||||||
#
|
|
||||||
def in_loop?
|
def in_loop?
|
||||||
@parent.loop?
|
@parent.loop?
|
||||||
end
|
end
|
||||||
|
|
|
@ -37,8 +37,7 @@ module Ameba::AST
|
||||||
|
|
||||||
# Returns true if this node or one of the parent branchables is a loop, false otherwise.
|
# Returns true if this node or one of the parent branchables is a loop, false otherwise.
|
||||||
def loop?
|
def loop?
|
||||||
return true if loop?(node)
|
loop?(node) || parent.try(&.loop?) || false
|
||||||
parent.try(&.loop?) || false
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -59,8 +59,6 @@ module Ameba::AST
|
||||||
end
|
end
|
||||||
when Crystal::BinaryOp
|
when Crystal::BinaryOp
|
||||||
unreachable_nodes << current_node.right if flow_expression?(current_node.left, in_loop?)
|
unreachable_nodes << current_node.right if flow_expression?(current_node.left, in_loop?)
|
||||||
else
|
|
||||||
# nop
|
|
||||||
end
|
end
|
||||||
|
|
||||||
unreachable_nodes
|
unreachable_nodes
|
||||||
|
|
|
@ -78,7 +78,7 @@ module Ameba::AST
|
||||||
# scope.find_variable("foo")
|
# scope.find_variable("foo")
|
||||||
# ```
|
# ```
|
||||||
def find_variable(name : String)
|
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
|
end
|
||||||
|
|
||||||
# Creates a new assignment for the variable.
|
# Creates a new assignment for the variable.
|
||||||
|
@ -117,8 +117,8 @@ module Ameba::AST
|
||||||
|
|
||||||
# Returns true instance variable assinged in this scope.
|
# Returns true instance variable assinged in this scope.
|
||||||
def assigns_ivar?(name)
|
def assigns_ivar?(name)
|
||||||
arguments.find { |arg| arg.name == name } &&
|
arguments.find(&.name.== name) &&
|
||||||
ivariables.find { |var| var.name == "@#{name}" }
|
ivariables.find(&.name.== "@#{name}")
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns true if and only if current scope represents some
|
# 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.
|
# Returns true if current scope is a def, false if not.
|
||||||
def def?
|
def def?
|
||||||
node.is_a? Crystal::Def
|
node.is_a?(Crystal::Def)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns true if this scope is a top level scope, false if not.
|
# 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`.
|
# the same Crystal node as `@node`.
|
||||||
def eql?(node)
|
def eql?(node)
|
||||||
node == @node &&
|
node == @node &&
|
||||||
!node.location.nil? &&
|
node.location &&
|
||||||
node.location == @node.location
|
node.location == @node.location
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -97,7 +97,6 @@ module Ameba::AST::Util
|
||||||
# ```
|
# ```
|
||||||
#
|
#
|
||||||
# That's because not all branches return(i.e. `else` is missing).
|
# That's because not all branches return(i.e. `else` is missing).
|
||||||
#
|
|
||||||
def flow_expression?(node, in_loop = false)
|
def flow_expression?(node, in_loop = false)
|
||||||
return true if flow_command? node, in_loop
|
return true if flow_command? node, in_loop
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,6 @@ module Ameba::AST
|
||||||
# ```
|
# ```
|
||||||
# Assignment.new(node, variable, scope)
|
# Assignment.new(node, variable, scope)
|
||||||
# ```
|
# ```
|
||||||
#
|
|
||||||
def initialize(@node, @variable, @scope)
|
def initialize(@node, @variable, @scope)
|
||||||
if scope = @variable.scope
|
if scope = @variable.scope
|
||||||
@branch = Branch.of(@node, scope)
|
@branch = Branch.of(@node, scope)
|
||||||
|
@ -49,7 +48,7 @@ module Ameba::AST
|
||||||
# a ||= 1
|
# a ||= 1
|
||||||
# ```
|
# ```
|
||||||
def op_assign?
|
def op_assign?
|
||||||
node.is_a? Crystal::OpAssign
|
node.is_a?(Crystal::OpAssign)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns true if this assignment is in a branch, false if not.
|
# Returns true if this assignment is in a branch, false if not.
|
||||||
|
@ -95,7 +94,6 @@ module Ameba::AST
|
||||||
# puts(b)
|
# puts(b)
|
||||||
# end
|
# end
|
||||||
# ```
|
# ```
|
||||||
#
|
|
||||||
def transformed?
|
def transformed?
|
||||||
return false unless (assign = node).is_a?(Crystal::Assign)
|
return false unless (assign = node).is_a?(Crystal::Assign)
|
||||||
return false unless (value = assign.value).is_a?(Crystal::Call)
|
return false unless (value = assign.value).is_a?(Crystal::Call)
|
||||||
|
|
|
@ -27,7 +27,6 @@ module Ameba::AST
|
||||||
# ```
|
# ```
|
||||||
# Variable.new(node, scope)
|
# Variable.new(node, scope)
|
||||||
# ```
|
# ```
|
||||||
#
|
|
||||||
def initialize(@node, @scope)
|
def initialize(@node, @scope)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -45,7 +44,6 @@ module Ameba::AST
|
||||||
# variable.assign(node2)
|
# variable.assign(node2)
|
||||||
# variable.assignment.size # => 2
|
# variable.assignment.size # => 2
|
||||||
# ```
|
# ```
|
||||||
#
|
|
||||||
def assign(node, scope)
|
def assign(node, scope)
|
||||||
assignments << Assignment.new(node, self, scope)
|
assignments << Assignment.new(node, self, scope)
|
||||||
|
|
||||||
|
@ -60,7 +58,7 @@ module Ameba::AST
|
||||||
# variable.referenced? # => true
|
# variable.referenced? # => true
|
||||||
# ```
|
# ```
|
||||||
def referenced?
|
def referenced?
|
||||||
references.any?
|
!references.empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
# Creates a reference to this variable in some scope.
|
# Creates a reference to this variable in some scope.
|
||||||
|
@ -69,7 +67,6 @@ module Ameba::AST
|
||||||
# variable = Variable.new(node, scope)
|
# variable = Variable.new(node, scope)
|
||||||
# variable.reference(var_node, some_scope)
|
# variable.reference(var_node, some_scope)
|
||||||
# ```
|
# ```
|
||||||
#
|
|
||||||
def reference(node : Crystal::Var, scope : Scope)
|
def reference(node : Crystal::Var, scope : Scope)
|
||||||
Reference.new(node, scope).tap do |reference|
|
Reference.new(node, scope).tap do |reference|
|
||||||
references << reference
|
references << reference
|
||||||
|
@ -91,8 +88,8 @@ module Ameba::AST
|
||||||
next if consumed_branches.includes?(assignment.branch)
|
next if consumed_branches.includes?(assignment.branch)
|
||||||
assignment.referenced = true
|
assignment.referenced = true
|
||||||
|
|
||||||
break unless assignment.branch
|
break unless branch = assignment.branch
|
||||||
consumed_branches << assignment.branch.not_nil!
|
consumed_branches << branch
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -175,6 +172,10 @@ module Ameba::AST
|
||||||
node.accept self
|
node.accept self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def references?(node : Crystal::Var)
|
||||||
|
@macro_literals.any?(&.value.includes?(node.name))
|
||||||
|
end
|
||||||
|
|
||||||
def visit(node : Crystal::ASTNode)
|
def visit(node : Crystal::ASTNode)
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
@ -203,7 +204,7 @@ module Ameba::AST
|
||||||
private def update_assign_reference!
|
private def update_assign_reference!
|
||||||
if @assign_before_reference.nil? &&
|
if @assign_before_reference.nil? &&
|
||||||
references.size <= assignments.size &&
|
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
|
@assign_before_reference = assignments.find { |ass| !ass.in_branch? }.try &.node
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -15,7 +15,6 @@ module Ameba::AST
|
||||||
# ```
|
# ```
|
||||||
# visitor = Ameba::AST::NodeVisitor.new(rule, source)
|
# visitor = Ameba::AST::NodeVisitor.new(rule, source)
|
||||||
# ```
|
# ```
|
||||||
#
|
|
||||||
def initialize(@rule, @source)
|
def initialize(@rule, @source)
|
||||||
@source.ast.accept self
|
@source.ast.accept self
|
||||||
end
|
end
|
||||||
|
|
|
@ -18,7 +18,6 @@ module Ameba::AST
|
||||||
if flow_expression?(node, in_loop?)
|
if flow_expression?(node, in_loop?)
|
||||||
@rule.test @source, node, FlowExpression.new(node, in_loop?)
|
@rule.test @source, node, FlowExpression.new(node, in_loop?)
|
||||||
end
|
end
|
||||||
|
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -61,7 +60,7 @@ module Ameba::AST
|
||||||
end
|
end
|
||||||
|
|
||||||
private def in_loop?
|
private def in_loop?
|
||||||
@loop_stack.any?
|
!@loop_stack.empty?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -40,7 +40,7 @@ module Ameba::AST
|
||||||
@skip : Array(Crystal::ASTNode.class)?
|
@skip : Array(Crystal::ASTNode.class)?
|
||||||
|
|
||||||
def initialize(@rule, @source, skip = nil)
|
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
|
super @rule, @source
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ module Ameba::AST
|
||||||
{% end %}
|
{% end %}
|
||||||
|
|
||||||
def visit(node)
|
def visit(node)
|
||||||
return true unless (skip = @skip)
|
return true unless skip = @skip
|
||||||
!skip.includes?(node.class)
|
!skip.includes?(node.class)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -28,8 +28,6 @@ module Ameba::AST
|
||||||
when Crystal::Case then traverse_case node
|
when Crystal::Case then traverse_case node
|
||||||
when Crystal::BinaryOp then traverse_binary_op node
|
when Crystal::BinaryOp then traverse_binary_op node
|
||||||
when Crystal::ExceptionHandler then traverse_exception_handler node
|
when Crystal::ExceptionHandler then traverse_exception_handler node
|
||||||
else
|
|
||||||
# ok
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,6 @@ module Ameba::AST
|
||||||
RECORD_NODE_NAME = "record"
|
RECORD_NODE_NAME = "record"
|
||||||
|
|
||||||
@scope_queue = [] of Scope
|
@scope_queue = [] of Scope
|
||||||
|
|
||||||
@current_scope : Scope
|
@current_scope : Scope
|
||||||
|
|
||||||
def initialize(@rule, @source)
|
def initialize(@rule, @source)
|
||||||
|
@ -169,7 +168,6 @@ module Ameba::AST
|
||||||
variable.reference(variable.node, @current_scope).explicit = false
|
variable.reference(variable.node, @current_scope).explicit = false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
true
|
true
|
||||||
when @current_scope.top_level? && record_macro?(node)
|
when @current_scope.top_level? && record_macro?(node)
|
||||||
false
|
false
|
||||||
|
|
28
src/ameba/ast/visitors/top_level_nodes_visitor.cr
Normal file
28
src/ameba/ast/visitors/top_level_nodes_visitor.cr
Normal 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
|
|
@ -8,12 +8,21 @@ module Ameba::Cli
|
||||||
def run(args = ARGV)
|
def run(args = ARGV)
|
||||||
opts = parse_args args
|
opts = parse_args args
|
||||||
config = Config.load opts.config, opts.colors?
|
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_formatter(config, opts)
|
||||||
configure_rules(config, opts)
|
configure_rules(config, opts)
|
||||||
|
|
||||||
|
if opts.rules?
|
||||||
|
print_rules(config)
|
||||||
|
end
|
||||||
|
|
||||||
runner = Ameba.run(config)
|
runner = Ameba.run(config)
|
||||||
|
|
||||||
if location = opts.location_to_explain
|
if location = opts.location_to_explain
|
||||||
|
@ -34,6 +43,7 @@ module Ameba::Cli
|
||||||
property except : Array(String)?
|
property except : Array(String)?
|
||||||
property location_to_explain : NamedTuple(file: String, line: Int32, column: Int32)?
|
property location_to_explain : NamedTuple(file: String, line: Int32, column: Int32)?
|
||||||
property fail_level : Severity?
|
property fail_level : Severity?
|
||||||
|
property? rules = false
|
||||||
property? all = false
|
property? all = false
|
||||||
property? colors = true
|
property? colors = true
|
||||||
property? without_affected_code = false
|
property? without_affected_code = false
|
||||||
|
@ -44,13 +54,14 @@ module Ameba::Cli
|
||||||
parser.banner = "Usage: ameba [options] [file1 file2 ...]"
|
parser.banner = "Usage: ameba [options] [file1 file2 ...]"
|
||||||
|
|
||||||
parser.on("-v", "--version", "Print version") { print_version }
|
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.on("-s", "--silent", "Disable output") { opts.formatter = :silent }
|
||||||
parser.unknown_args do |f|
|
parser.unknown_args do |f|
|
||||||
if f.size == 1 && f.first =~ /.+:\d+:\d+/
|
if f.size == 1 && f.first =~ /.+:\d+:\d+/
|
||||||
configure_explain_opts(f.first, opts)
|
configure_explain_opts(f.first, opts)
|
||||||
else
|
else
|
||||||
opts.globs = f if f.any?
|
opts.globs = f unless f.empty?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -66,12 +77,12 @@ module Ameba::Cli
|
||||||
|
|
||||||
parser.on("--only RULE1,RULE2,...",
|
parser.on("--only RULE1,RULE2,...",
|
||||||
"Run only given rules (or groups)") do |rules|
|
"Run only given rules (or groups)") do |rules|
|
||||||
opts.only = rules.split ","
|
opts.only = rules.split(',')
|
||||||
end
|
end
|
||||||
|
|
||||||
parser.on("--except RULE1,RULE2,...",
|
parser.on("--except RULE1,RULE2,...",
|
||||||
"Disable the given rules (or groups)") do |rules|
|
"Disable the given rules (or groups)") do |rules|
|
||||||
opts.except = rules.split ","
|
opts.except = rules.split(',')
|
||||||
end
|
end
|
||||||
|
|
||||||
parser.on("--all", "Enables all available rules") do
|
parser.on("--all", "Enables all available rules") do
|
||||||
|
@ -84,7 +95,8 @@ module Ameba::Cli
|
||||||
opts.config = ""
|
opts.config = ""
|
||||||
end
|
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)
|
opts.fail_level = Severity.parse(level)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -107,13 +119,13 @@ module Ameba::Cli
|
||||||
end
|
end
|
||||||
|
|
||||||
private def configure_rules(config, opts)
|
private def configure_rules(config, opts)
|
||||||
if only = opts.only
|
case
|
||||||
config.rules.map! { |r| r.enabled = false; r }
|
when only = opts.only
|
||||||
|
config.rules.each(&.enabled = false)
|
||||||
config.update_rules(only, enabled: true)
|
config.update_rules(only, enabled: true)
|
||||||
elsif opts.all?
|
when opts.all?
|
||||||
config.rules.map! { |r| r.enabled = true; r }
|
config.rules.each(&.enabled = true)
|
||||||
end
|
end
|
||||||
|
|
||||||
config.update_rules(opts.except, enabled: false)
|
config.update_rules(opts.except, enabled: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -121,7 +133,8 @@ module Ameba::Cli
|
||||||
if name = opts.formatter
|
if name = opts.formatter
|
||||||
config.formatter = name
|
config.formatter = name
|
||||||
end
|
end
|
||||||
config.formatter.config[:without_affected_code] = opts.without_affected_code?
|
config.formatter.config[:without_affected_code] =
|
||||||
|
opts.without_affected_code?
|
||||||
end
|
end
|
||||||
|
|
||||||
private def configure_explain_opts(loc, opts)
|
private def configure_explain_opts(loc, opts)
|
||||||
|
@ -132,10 +145,15 @@ module Ameba::Cli
|
||||||
end
|
end
|
||||||
|
|
||||||
private def parse_explain_location(arg)
|
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
|
raise ArgumentError.new unless location.size === 3
|
||||||
|
|
||||||
file, line, column = location
|
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
|
rescue
|
||||||
raise "location should have PATH:line:column format"
|
raise "location should have PATH:line:column format"
|
||||||
end
|
end
|
||||||
|
@ -145,8 +163,18 @@ module Ameba::Cli
|
||||||
exit 0
|
exit 0
|
||||||
end
|
end
|
||||||
|
|
||||||
private def show_help(parser)
|
private def print_help(parser)
|
||||||
puts parser
|
puts parser
|
||||||
exit 0
|
exit 0
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -31,7 +31,6 @@ class Ameba::Config
|
||||||
!lib
|
!lib
|
||||||
)
|
)
|
||||||
|
|
||||||
setter formatter : Formatter::BaseFormatter?
|
|
||||||
getter rules : Array(Rule::Base)
|
getter rules : Array(Rule::Base)
|
||||||
property severity = Severity::Convention
|
property severity = Severity::Convention
|
||||||
|
|
||||||
|
@ -66,7 +65,9 @@ class Ameba::Config
|
||||||
@excluded = load_array_section(config, "Excluded")
|
@excluded = load_array_section(config, "Excluded")
|
||||||
@globs = load_array_section(config, "Globs", DEFAULT_GLOBS)
|
@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
|
end
|
||||||
|
|
||||||
# Loads YAML configuration file by `path`.
|
# Loads YAML configuration file by `path`.
|
||||||
|
@ -74,7 +75,6 @@ class Ameba::Config
|
||||||
# ```
|
# ```
|
||||||
# config = Ameba::Config.load
|
# config = Ameba::Config.load
|
||||||
# ```
|
# ```
|
||||||
#
|
|
||||||
def self.load(path = PATH, colors = true)
|
def self.load(path = PATH, colors = true)
|
||||||
Colorize.enabled = colors
|
Colorize.enabled = colors
|
||||||
content = File.exists?(path) ? File.read path : "{}"
|
content = File.exists?(path) ? File.read path : "{}"
|
||||||
|
@ -84,7 +84,7 @@ class Ameba::Config
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.formatter_names
|
def self.formatter_names
|
||||||
AVAILABLE_FORMATTERS.keys.join("|")
|
AVAILABLE_FORMATTERS.keys.join('|')
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns a list of sources matching globs and excluded sections.
|
# Returns a list of sources matching globs and excluded sections.
|
||||||
|
@ -96,7 +96,6 @@ class Ameba::Config
|
||||||
# config.excluded = ["spec"]
|
# config.excluded = ["spec"]
|
||||||
# config.sources # => list of sources pointing to files found by the wildcards
|
# config.sources # => list of sources pointing to files found by the wildcards
|
||||||
# ```
|
# ```
|
||||||
#
|
|
||||||
def sources
|
def sources
|
||||||
(find_files_by_globs(globs) - find_files_by_globs(excluded))
|
(find_files_by_globs(globs) - find_files_by_globs(excluded))
|
||||||
.map { |path| Source.new File.read(path), path }
|
.map { |path| Source.new File.read(path), path }
|
||||||
|
@ -111,8 +110,8 @@ class Ameba::Config
|
||||||
# config.formatter
|
# config.formatter
|
||||||
# ```
|
# ```
|
||||||
#
|
#
|
||||||
def formatter
|
property formatter : Formatter::BaseFormatter do
|
||||||
@formatter ||= Formatter::DotFormatter.new
|
Formatter::DotFormatter.new
|
||||||
end
|
end
|
||||||
|
|
||||||
# Sets formatter by name.
|
# Sets formatter by name.
|
||||||
|
@ -121,7 +120,6 @@ class Ameba::Config
|
||||||
# config = Ameba::Config.load
|
# config = Ameba::Config.load
|
||||||
# config.formatter = :progress
|
# config.formatter = :progress
|
||||||
# ```
|
# ```
|
||||||
#
|
|
||||||
def formatter=(name : String | Symbol)
|
def formatter=(name : String | Symbol)
|
||||||
if f = AVAILABLE_FORMATTERS[name]?
|
if f = AVAILABLE_FORMATTERS[name]?
|
||||||
@formatter = f.new
|
@formatter = f.new
|
||||||
|
@ -136,15 +134,13 @@ class Ameba::Config
|
||||||
# config = Ameba::Config.load
|
# config = Ameba::Config.load
|
||||||
# config.update_rule "MyRuleName", enabled: false
|
# config.update_rule "MyRuleName", enabled: false
|
||||||
# ```
|
# ```
|
||||||
#
|
|
||||||
def update_rule(name, enabled = true, excluded = nil)
|
def update_rule(name, enabled = true, excluded = nil)
|
||||||
index = @rules.index { |r| r.name == name }
|
rule = @rules.find(&.name.==(name))
|
||||||
raise ArgumentError.new("Rule `#{name}` does not exist") unless index
|
raise ArgumentError.new("Rule `#{name}` does not exist") unless rule
|
||||||
|
|
||||||
rule = @rules[index]
|
rule
|
||||||
rule.enabled = enabled
|
.tap(&.enabled = enabled)
|
||||||
rule.excluded = excluded
|
.tap(&.excluded = excluded)
|
||||||
@rules[index] = rule
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Updates rules properties.
|
# Updates rules properties.
|
||||||
|
@ -159,20 +155,22 @@ class Ameba::Config
|
||||||
# ```
|
# ```
|
||||||
# config.update_rules %w(Group1 Group2), enabled: true
|
# config.update_rules %w(Group1 Group2), enabled: true
|
||||||
# ```
|
# ```
|
||||||
#
|
def update_rules(names, enabled = true, excluded = nil)
|
||||||
def update_rules(names, **args)
|
|
||||||
names.try &.each do |name|
|
names.try &.each do |name|
|
||||||
if group = @rule_groups[name]?
|
if rules = @rule_groups[name]?
|
||||||
group.each { |rule| update_rule(rule.name, **args) }
|
rules.each do |rule|
|
||||||
|
rule.enabled = enabled
|
||||||
|
rule.excluded = excluded
|
||||||
|
end
|
||||||
else
|
else
|
||||||
update_rule name, **args
|
update_rule name, enabled, excluded
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private def load_formatter_name(config)
|
private def load_formatter_name(config)
|
||||||
name = config["Formatter"]?.try &.["Name"]?
|
name = config["Formatter"]?.try &.["Name"]?
|
||||||
name ? name.to_s : nil
|
name.try(&.to_s)
|
||||||
end
|
end
|
||||||
|
|
||||||
private def load_array_section(config, section_name, default = [] of String)
|
private def load_array_section(config, section_name, default = [] of String)
|
||||||
|
@ -269,7 +267,7 @@ class Ameba::Config
|
||||||
include YAML::Serializable::Strict
|
include YAML::Serializable::Strict
|
||||||
|
|
||||||
def self.new(config = nil)
|
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
|
yaml = raw[rule_name]?.try &.to_yaml
|
||||||
end
|
end
|
||||||
from_yaml yaml || "{}"
|
from_yaml yaml || "{}"
|
||||||
|
|
|
@ -6,14 +6,15 @@ module Ameba::Formatter
|
||||||
class DotFormatter < BaseFormatter
|
class DotFormatter < BaseFormatter
|
||||||
include Util
|
include Util
|
||||||
|
|
||||||
@started_at : Time?
|
@started_at : Time::Span?
|
||||||
@mutex = Thread::Mutex.new
|
@mutex = Thread::Mutex.new
|
||||||
|
|
||||||
# Reports a message when inspection is started.
|
# Reports a message when inspection is started.
|
||||||
def started(sources)
|
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
|
end
|
||||||
|
|
||||||
# Reports a result of the inspection of a corresponding source.
|
# Reports a result of the inspection of a corresponding source.
|
||||||
|
@ -35,32 +36,35 @@ module Ameba::Formatter
|
||||||
next if issue.disabled?
|
next if issue.disabled?
|
||||||
next if (location = issue.location).nil?
|
next if (location = issue.location).nil?
|
||||||
|
|
||||||
output << "#{location}\n".colorize(:cyan)
|
output.puts location.colorize(:cyan)
|
||||||
output << "[#{issue.rule.severity.symbol}] #{issue.rule.name}: #{issue.message}\n".colorize(:red)
|
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)
|
output << code.colorize(:default)
|
||||||
end
|
end
|
||||||
|
|
||||||
output << "\n"
|
output.puts
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
output << finished_in_message(@started_at, Time.utc) # Time.monotonic
|
output.puts finished_in_message(@started_at, Time.monotonic)
|
||||||
output << final_message(sources, failed_sources)
|
output.puts final_message(sources, failed_sources)
|
||||||
end
|
end
|
||||||
|
|
||||||
private def started_message(size)
|
private def started_message(size)
|
||||||
if size == 1
|
if size == 1
|
||||||
"Inspecting 1 file.\n\n".colorize(:default)
|
"Inspecting 1 file".colorize(:default)
|
||||||
else
|
else
|
||||||
"Inspecting #{size} files.\n\n".colorize(:default)
|
"Inspecting #{size} files".colorize(:default)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private def finished_in_message(started, finished)
|
private def finished_in_message(started, finished)
|
||||||
if 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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -86,11 +90,11 @@ module Ameba::Formatter
|
||||||
|
|
||||||
private def final_message(sources, failed_sources)
|
private def final_message(sources, failed_sources)
|
||||||
total = sources.size
|
total = sources.size
|
||||||
failures = failed_sources.map { |f| f.issues.size }.sum
|
failures = failed_sources.sum(&.issues.size)
|
||||||
color = failures == 0 ? :green : :red
|
color = failures == 0 ? :green : :red
|
||||||
s = failures != 1 ? "s" : ""
|
s = failures != 1 ? "s" : ""
|
||||||
|
|
||||||
"#{total} inspected, #{failures} failure#{s}.\n".colorize color
|
"#{total} inspected, #{failures} failure#{s}".colorize(color)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,7 +4,6 @@ module Ameba::Formatter
|
||||||
# A formatter that shows the detailed explanation of the issue at
|
# A formatter that shows the detailed explanation of the issue at
|
||||||
# a specific location.
|
# a specific location.
|
||||||
class ExplainFormatter
|
class ExplainFormatter
|
||||||
LINE_BREAK = "\n"
|
|
||||||
HEADING = "## "
|
HEADING = "## "
|
||||||
PREFIX = " "
|
PREFIX = " "
|
||||||
|
|
||||||
|
@ -21,36 +20,36 @@ module Ameba::Formatter
|
||||||
# ExplainFormatter.new output,
|
# ExplainFormatter.new output,
|
||||||
# {file: path, line: line_number, column: column_number}
|
# {file: path, line: line_number, column: column_number}
|
||||||
# ```
|
# ```
|
||||||
#
|
def initialize(@output, location)
|
||||||
def initialize(@output, loc)
|
@location = Crystal::Location.new(location[:file], location[:line], location[:column])
|
||||||
@location = Crystal::Location.new(loc[:file], loc[:line], loc[:column])
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Reports the explainations at the *@location*.
|
# Reports the explainations at the *@location*.
|
||||||
def finished(sources)
|
def finished(sources)
|
||||||
source = sources.find { |s| s.path == @location.filename }
|
source = sources.find(&.path.==(@location.filename))
|
||||||
|
|
||||||
return unless source
|
return unless source
|
||||||
|
|
||||||
source.issues.each do |issue|
|
issue = source.issues.find(&.location.==(@location))
|
||||||
if (location = issue.location) &&
|
return unless issue
|
||||||
location.line_number == @location.line_number &&
|
|
||||||
location.column_number == @location.column_number
|
|
||||||
explain(source, issue)
|
explain(source, issue)
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private def explain(source, issue)
|
private def explain(source, issue)
|
||||||
rule = issue.rule
|
rule = issue.rule
|
||||||
|
|
||||||
|
location, end_location =
|
||||||
|
issue.location, issue.end_location
|
||||||
|
|
||||||
|
return unless location
|
||||||
|
|
||||||
output_title "ISSUE INFO"
|
output_title "ISSUE INFO"
|
||||||
output_paragraph [
|
output_paragraph [
|
||||||
issue.message.colorize(:red).to_s,
|
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_title "AFFECTED CODE"
|
||||||
output_paragraph affected_code
|
output_paragraph affected_code
|
||||||
end
|
end
|
||||||
|
@ -65,19 +64,19 @@ module Ameba::Formatter
|
||||||
end
|
end
|
||||||
|
|
||||||
private def output_title(title)
|
private def output_title(title)
|
||||||
output << HEADING.colorize(:yellow) << title.colorize(:yellow) << LINE_BREAK
|
output << HEADING.colorize(:yellow) << title.colorize(:yellow) << '\n'
|
||||||
output << LINE_BREAK
|
output << '\n'
|
||||||
end
|
end
|
||||||
|
|
||||||
private def output_paragraph(paragraph : String)
|
private def output_paragraph(paragraph : String)
|
||||||
output_paragraph(paragraph.split(LINE_BREAK))
|
output_paragraph(paragraph.lines)
|
||||||
end
|
end
|
||||||
|
|
||||||
private def output_paragraph(paragraph : Array(String))
|
private def output_paragraph(paragraph : Array(String))
|
||||||
paragraph.each do |line|
|
paragraph.each do |line|
|
||||||
output << PREFIX << line << LINE_BREAK
|
output << PREFIX << line << '\n'
|
||||||
end
|
end
|
||||||
output << LINE_BREAK
|
output << '\n'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,7 +9,7 @@ module Ameba::Formatter
|
||||||
@mutex.synchronize do
|
@mutex.synchronize do
|
||||||
output.printf "%s:%d:%d: %s: [%s] %s\n",
|
output.printf "%s:%d:%d: %s: [%s] %s\n",
|
||||||
source.path, loc.line_number, loc.column_number, e.rule.severity.symbol,
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,19 +8,20 @@ module Ameba::Formatter
|
||||||
|
|
||||||
def finished(sources)
|
def finished(sources)
|
||||||
super
|
super
|
||||||
issues = sources.map(&.issues).flatten
|
|
||||||
|
issues = sources.flat_map(&.issues)
|
||||||
unless issues.any? { |issue| !issue.disabled? }
|
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
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
if issues.any? { |issue| issue.syntax? }
|
if issues.any?(&.syntax?)
|
||||||
@output << "Unable to generate TODO file. Please fix syntax issues.\n"
|
@output.puts "Unable to generate TODO file. Please fix syntax issues."
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
file = generate_todo_config issues
|
file = generate_todo_config issues
|
||||||
@output << "Created #{file.path}\n"
|
@output.puts "Created #{file.path}"
|
||||||
file
|
file
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -40,7 +41,7 @@ module Ameba::Formatter
|
||||||
private def rule_issues_map(issues)
|
private def rule_issues_map(issues)
|
||||||
Hash(Rule::Base, Array(Issue)).new.tap do |h|
|
Hash(Rule::Base, Array(Issue)).new.tap do |h|
|
||||||
issues.each do |issue|
|
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
|
(h[issue.rule] ||= Array(Issue).new) << issue
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -57,9 +58,8 @@ module Ameba::Formatter
|
||||||
end
|
end
|
||||||
|
|
||||||
private def rule_todo(rule, issues)
|
private def rule_todo(rule, issues)
|
||||||
rule.excluded =
|
rule.excluded = issues
|
||||||
issues.map(&.location.try &.filename.try &.to_s)
|
.compact_map(&.location.try &.filename.try &.to_s)
|
||||||
.compact
|
|
||||||
.uniq!
|
.uniq!
|
||||||
|
|
||||||
{rule.name => rule}.to_yaml
|
{rule.name => rule}.to_yaml
|
||||||
|
|
|
@ -1,23 +1,110 @@
|
||||||
module Ameba::Formatter
|
module Ameba::Formatter
|
||||||
module Util
|
module Util
|
||||||
def affected_code(source, location, max_length = 100, placeholder = " ...", prompt = "> ")
|
def deansify(message : String?) : String?
|
||||||
line, column = location.line_number, location.column_number
|
message.try &.gsub(/\x1b[^m]*m/, "").presence
|
||||||
affected_line = source.lines[line - 1]?
|
|
||||||
|
|
||||||
return if affected_line.nil? || affected_line.strip.empty?
|
|
||||||
|
|
||||||
if affected_line.size > max_length && column < max_length
|
|
||||||
affected_line = affected_line[0, max_length - placeholder.size - 1] + placeholder
|
|
||||||
end
|
end
|
||||||
|
|
||||||
stripped = affected_line.lstrip
|
def trim(str, max_length = 120, ellipsis = " ...")
|
||||||
position = column - (affected_line.size - stripped.size) + prompt.size
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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|
|
String.build do |str|
|
||||||
str << prompt << stripped << "\n"
|
if show_context
|
||||||
str << " " * (position - 1)
|
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)
|
str << "^".colorize(:yellow)
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,12 +7,11 @@ module Ameba
|
||||||
# ```
|
# ```
|
||||||
# find_files_by_globs(["**/*.cr", "!lib"])
|
# find_files_by_globs(["**/*.cr", "!lib"])
|
||||||
# ```
|
# ```
|
||||||
#
|
|
||||||
def find_files_by_globs(globs)
|
def find_files_by_globs(globs)
|
||||||
rejected = rejected_globs(globs)
|
rejected = rejected_globs(globs)
|
||||||
selected = globs - rejected
|
selected = globs - rejected
|
||||||
|
|
||||||
expand(selected) - expand(rejected.map! { |p| p[1..-1] })
|
expand(selected) - expand(rejected.map!(&.[1..-1]))
|
||||||
end
|
end
|
||||||
|
|
||||||
# Expands globs. Globs can point to files or even directories.
|
# 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
|
# expand(["spec/*.cr", "src"]) # => all files in src folder + first level specs
|
||||||
# ```
|
# ```
|
||||||
#
|
|
||||||
def expand(globs)
|
def expand(globs)
|
||||||
globs.map do |glob|
|
globs.flat_map do |glob|
|
||||||
glob += "/**/*.cr" if File.directory?(glob)
|
glob += "/**/*.cr" if File.directory?(glob)
|
||||||
Dir[glob]
|
Dir[glob]
|
||||||
end.flatten.uniq!
|
end.uniq!
|
||||||
end
|
end
|
||||||
|
|
||||||
private def rejected_globs(globs)
|
private def rejected_globs(globs)
|
||||||
|
|
|
@ -36,9 +36,8 @@ module Ameba
|
||||||
# Time.epoch(1483859302)
|
# Time.epoch(1483859302)
|
||||||
# end
|
# end
|
||||||
# ```
|
# ```
|
||||||
#
|
|
||||||
def location_disabled?(location, rule)
|
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_number = location.try &.line_number.try &.- 1
|
||||||
return false unless line = lines[line_number]?
|
return false unless line = lines[line_number]?
|
||||||
|
|
||||||
|
@ -65,7 +64,6 @@ module Ameba
|
||||||
# line = "# # ameba:disable Rule1, Rule2"
|
# line = "# # ameba:disable Rule1, Rule2"
|
||||||
# parse_inline_directive(line) # => nil
|
# parse_inline_directive(line) # => nil
|
||||||
# ```
|
# ```
|
||||||
#
|
|
||||||
def parse_inline_directive(line)
|
def parse_inline_directive(line)
|
||||||
if directive = COMMENT_DIRECTIVE_REGEX.match(line)
|
if directive = COMMENT_DIRECTIVE_REGEX.match(line)
|
||||||
return if commented_out?(line.gsub(directive[0], ""))
|
return if commented_out?(line.gsub(directive[0], ""))
|
||||||
|
@ -89,8 +87,10 @@ module Ameba
|
||||||
|
|
||||||
private def line_disabled?(line, rule)
|
private def line_disabled?(line, rule)
|
||||||
return false unless directive = parse_inline_directive(line)
|
return false unless directive = parse_inline_directive(line)
|
||||||
Action.parse?(directive[:action]).try(&.disable?) &&
|
return false unless Action.parse?(directive[:action]).try(&.disable?)
|
||||||
(directive[:rules].includes?(rule.name) || directive[:rules].includes?(rule.group))
|
|
||||||
|
directive[:rules].includes?(rule.name) ||
|
||||||
|
directive[:rules].includes?(rule.group)
|
||||||
end
|
end
|
||||||
|
|
||||||
private def commented_out?(line)
|
private def commented_out?(line)
|
||||||
|
|
|
@ -1,22 +1,31 @@
|
||||||
module Ameba
|
module Ameba
|
||||||
# Represents an issue reported by Ameba.
|
# Represents an issue reported by Ameba.
|
||||||
record Issue,
|
struct Issue
|
||||||
|
enum Status
|
||||||
|
Enabled
|
||||||
|
Disabled
|
||||||
|
end
|
||||||
|
|
||||||
# A rule that triggers this issue.
|
# A rule that triggers this issue.
|
||||||
rule : Rule::Base,
|
getter rule : Rule::Base
|
||||||
|
|
||||||
# Location of the issue.
|
# Location of the issue.
|
||||||
location : Crystal::Location?,
|
getter location : Crystal::Location?
|
||||||
|
|
||||||
# End location of the issue.
|
# End location of the issue.
|
||||||
end_location : Crystal::Location?,
|
getter end_location : Crystal::Location?
|
||||||
|
|
||||||
# Issue message.
|
# Issue message.
|
||||||
message : String,
|
getter message : String
|
||||||
|
|
||||||
# Issue status.
|
# Issue status.
|
||||||
status : Symbol? do
|
getter status : Status
|
||||||
def disabled?
|
|
||||||
status == :disabled
|
delegate :enabled?, :disabled?,
|
||||||
|
to: status
|
||||||
|
|
||||||
|
def initialize(@rule, @location, @end_location, @message, status : Status? = nil)
|
||||||
|
@status = status || Status::Enabled
|
||||||
end
|
end
|
||||||
|
|
||||||
def syntax?
|
def syntax?
|
||||||
|
|
|
@ -5,37 +5,46 @@ module Ameba
|
||||||
getter issues = [] of Issue
|
getter issues = [] of Issue
|
||||||
|
|
||||||
# Adds a new issue to the list of issues.
|
# Adds a new issue to the list of issues.
|
||||||
def add_issue(rule, location : Crystal::Location?, end_location : Crystal::Location?, message, status = nil)
|
def add_issue(rule, location, end_location, message, status : Issue::Status? = nil) : Issue
|
||||||
status ||= :disabled if location_disabled?(location, rule)
|
status ||=
|
||||||
issues << Issue.new rule, location, end_location, message, status
|
Issue::Status::Disabled if location_disabled?(location, rule)
|
||||||
|
|
||||||
|
Issue.new(rule, location, end_location, message, status).tap do |issue|
|
||||||
|
issues << issue
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Adds a new issue for AST *node*.
|
# Adds a new issue for Crystal AST *node*.
|
||||||
def add_issue(rule, node : Crystal::ASTNode, message, **args)
|
def add_issue(rule, node : Crystal::ASTNode, message, status : Issue::Status? = nil) : Issue
|
||||||
add_issue rule, node.location, node.end_location, message, **args
|
add_issue rule, node.location, node.end_location, message, status
|
||||||
end
|
end
|
||||||
|
|
||||||
# Adds a new issue for Crystal *token*.
|
# Adds a new issue for Crystal *token*.
|
||||||
def add_issue(rule, token : Crystal::Token, message, **args)
|
def add_issue(rule, token : Crystal::Token, message, status : Issue::Status? = nil) : Issue
|
||||||
add_issue rule, token.location, nil, message, **args
|
add_issue rule, token.location, nil, message, status
|
||||||
end
|
end
|
||||||
|
|
||||||
# Adds a new issue for *location* defined by line and column numbers.
|
# Adds a new issue for *location* defined by line and column numbers.
|
||||||
def add_issue(rule, location : Tuple(Int32, Int32), message, **args)
|
def add_issue(rule, location : {Int32, Int32}, message, status : Issue::Status? = nil) : Issue
|
||||||
location = Crystal::Location.new path, *location
|
location =
|
||||||
add_issue rule, location, nil, message, **args
|
Crystal::Location.new(path, *location)
|
||||||
|
|
||||||
|
add_issue rule, location, nil, message, status
|
||||||
end
|
end
|
||||||
|
|
||||||
# Adds a new issue for *location* and *end_location* defined by line and column numbers.
|
# 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)
|
def add_issue(rule, location : {Int32, Int32}, end_location : {Int32, Int32}, message, status : Issue::Status? = nil) : Issue
|
||||||
location = Crystal::Location.new path, *location
|
location =
|
||||||
end_location = Crystal::Location.new path, *end_location
|
Crystal::Location.new(path, *location)
|
||||||
add_issue rule, location, end_location, message, **args
|
end_location =
|
||||||
|
Crystal::Location.new(path, *end_location)
|
||||||
|
|
||||||
|
add_issue rule, location, end_location, message, status
|
||||||
end
|
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?
|
def valid?
|
||||||
issues.reject(&.disabled?).empty?
|
issues.none?(&.enabled?)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,7 +10,7 @@ module Ameba::Rule
|
||||||
# inherits from this struct:
|
# inherits from this struct:
|
||||||
#
|
#
|
||||||
# ```
|
# ```
|
||||||
# struct MyRule < Ameba::Rule::Base
|
# class MyRule < Ameba::Rule::Base
|
||||||
# def test(source)
|
# def test(source)
|
||||||
# if invalid?(source)
|
# if invalid?(source)
|
||||||
# issue_for line, column, "Something wrong."
|
# issue_for line, column, "Something wrong."
|
||||||
|
@ -26,8 +26,7 @@ module Ameba::Rule
|
||||||
# Enforces rules to implement an abstract `#test` method which
|
# Enforces rules to implement an abstract `#test` method which
|
||||||
# is designed to test the source passed in. If source has issues
|
# is designed to test the source passed in. If source has issues
|
||||||
# that are tested by this rule, it should add an issue.
|
# that are tested by this rule, it should add an issue.
|
||||||
#
|
abstract class Base
|
||||||
abstract struct Base
|
|
||||||
include Config::RuleConfig
|
include Config::RuleConfig
|
||||||
|
|
||||||
# This method is designed to test the source passed in. If source has issues
|
# 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 = MyRule.new.catch(source)
|
||||||
# source.valid?
|
# source.valid?
|
||||||
# ```
|
# ```
|
||||||
#
|
|
||||||
def catch(source : Source)
|
def catch(source : Source)
|
||||||
source.tap { |s| test s }
|
source.tap { test source }
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns a name of this rule, which is basically a class name.
|
# 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)
|
# def test(source)
|
||||||
# end
|
# end
|
||||||
# end
|
# end
|
||||||
#
|
#
|
||||||
# MyRule.new.name # => "MyRule"
|
# MyRule.new.name # => "MyRule"
|
||||||
# ```
|
# ```
|
||||||
#
|
|
||||||
def name
|
def name
|
||||||
{{@type}}.rule_name
|
{{@type}}.rule_name
|
||||||
end
|
end
|
||||||
|
@ -73,13 +70,12 @@ module Ameba::Rule
|
||||||
# Returns a group this rule belong to.
|
# Returns a group this rule belong to.
|
||||||
#
|
#
|
||||||
# ```
|
# ```
|
||||||
# struct MyGroup::MyRule < Ameba::Rule::Base
|
# class MyGroup::MyRule < Ameba::Rule::Base
|
||||||
# # ...
|
# # ...
|
||||||
# end
|
# end
|
||||||
#
|
#
|
||||||
# MyGroup::MyRule.new.group # => "MyGroup"
|
# MyGroup::MyRule.new.group # => "MyGroup"
|
||||||
# ```
|
# ```
|
||||||
#
|
|
||||||
def group
|
def group
|
||||||
{{@type}}.group_name
|
{{@type}}.group_name
|
||||||
end
|
end
|
||||||
|
@ -91,11 +87,10 @@ module Ameba::Rule
|
||||||
# ```
|
# ```
|
||||||
# my_rule.excluded?(source) # => true or false
|
# my_rule.excluded?(source) # => true or false
|
||||||
# ```
|
# ```
|
||||||
#
|
|
||||||
def excluded?(source)
|
def excluded?(source)
|
||||||
excluded.try &.any? do |path|
|
excluded.try &.any? do |path|
|
||||||
source.matches_path?(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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -105,13 +100,12 @@ module Ameba::Rule
|
||||||
# ```
|
# ```
|
||||||
# my_rule.special? # => true or false
|
# my_rule.special? # => true or false
|
||||||
# ```
|
# ```
|
||||||
#
|
|
||||||
def special?
|
def special?
|
||||||
SPECIAL.includes? name
|
name.in?(SPECIAL)
|
||||||
end
|
end
|
||||||
|
|
||||||
def ==(other)
|
def ==(other)
|
||||||
name == other.try &.name
|
name == other.try(&.name)
|
||||||
end
|
end
|
||||||
|
|
||||||
def hash
|
def hash
|
||||||
|
@ -123,11 +117,11 @@ module Ameba::Rule
|
||||||
end
|
end
|
||||||
|
|
||||||
protected def self.rule_name
|
protected def self.rule_name
|
||||||
name.gsub("Ameba::Rule::", "").gsub("::", "/")
|
name.gsub("Ameba::Rule::", "").gsub("::", '/')
|
||||||
end
|
end
|
||||||
|
|
||||||
protected def self.group_name
|
protected def self.group_name
|
||||||
rule_name.split("/")[0...-1].join("/")
|
rule_name.split('/')[0...-1].join('/')
|
||||||
end
|
end
|
||||||
|
|
||||||
protected def self.subclasses
|
protected def self.subclasses
|
||||||
|
@ -146,7 +140,7 @@ module Ameba::Rule
|
||||||
# module Ameba
|
# module Ameba
|
||||||
# # This is a test rule.
|
# # This is a test rule.
|
||||||
# # Does nothing.
|
# # Does nothing.
|
||||||
# struct MyRule < Ameba::Rule::Base
|
# class MyRule < Ameba::Rule::Base
|
||||||
# def test(source)
|
# def test(source)
|
||||||
# end
|
# end
|
||||||
# end
|
# end
|
||||||
|
@ -156,8 +150,11 @@ module Ameba::Rule
|
||||||
# ```
|
# ```
|
||||||
def self.parsed_doc
|
def self.parsed_doc
|
||||||
source = File.read(path_to_source_file)
|
source = File.read(path_to_source_file)
|
||||||
nodes = Crystal::Parser.new(source).tap(&.wants_doc = true).parse
|
nodes = Crystal::Parser.new(source)
|
||||||
type_name = rule_name.split("/").last?
|
.tap(&.wants_doc = true)
|
||||||
|
.parse
|
||||||
|
type_name = rule_name.split('/').last?
|
||||||
|
|
||||||
DocFinder.new(nodes, type_name).doc
|
DocFinder.new(nodes, type_name).doc
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -190,7 +187,6 @@ module Ameba::Rule
|
||||||
# ```
|
# ```
|
||||||
# Ameba::Rule.rules # => [Rule1, Rule2, ....]
|
# Ameba::Rule.rules # => [Rule1, Rule2, ....]
|
||||||
# ```
|
# ```
|
||||||
#
|
|
||||||
def self.rules
|
def self.rules
|
||||||
Base.subclasses
|
Base.subclasses
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,8 +8,7 @@ module Ameba::Rule::Layout
|
||||||
# Enabled: true
|
# Enabled: true
|
||||||
# MaxLength: 100
|
# MaxLength: 100
|
||||||
# ```
|
# ```
|
||||||
#
|
class LineLength < Base
|
||||||
struct LineLength < Base
|
|
||||||
properties do
|
properties do
|
||||||
enabled false
|
enabled false
|
||||||
description "Disallows lines longer than `MaxLength` number of symbols"
|
description "Disallows lines longer than `MaxLength` number of symbols"
|
||||||
|
@ -20,9 +19,7 @@ module Ameba::Rule::Layout
|
||||||
|
|
||||||
def test(source)
|
def test(source)
|
||||||
source.lines.each_with_index do |line, index|
|
source.lines.each_with_index do |line, index|
|
||||||
next unless line.size > max_length
|
issue_for({index + 1, max_length + 1}, MSG) if line.size > max_length
|
||||||
|
|
||||||
issue_for({index + 1, max_length + 1}, MSG)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,8 +7,7 @@ module Ameba::Rule::Layout
|
||||||
# Layout/TrailingBlankLines:
|
# Layout/TrailingBlankLines:
|
||||||
# Enabled: true
|
# Enabled: true
|
||||||
# ```
|
# ```
|
||||||
#
|
class TrailingBlankLines < Base
|
||||||
struct TrailingBlankLines < Base
|
|
||||||
properties do
|
properties do
|
||||||
description "Disallows trailing blank lines"
|
description "Disallows trailing blank lines"
|
||||||
end
|
end
|
||||||
|
@ -24,9 +23,9 @@ module Ameba::Rule::Layout
|
||||||
source_lines_size = source_lines.size
|
source_lines_size = source_lines.size
|
||||||
return if source_lines_size == 1 && last_source_line.empty?
|
return if source_lines_size == 1 && last_source_line.empty?
|
||||||
|
|
||||||
last_line_not_empty = !last_source_line.empty?
|
last_line_empty = last_source_line.empty?
|
||||||
if source_lines_size >= 1 && (source_lines.last(2).join.strip.empty? || last_line_not_empty)
|
if source_lines_size >= 1 && (source_lines.last(2).join.blank? || !last_line_empty)
|
||||||
issue_for({source_lines_size - 1, 1}, last_line_not_empty ? MSG_FINAL_NEWLINE : MSG)
|
issue_for({source_lines_size - 1, 1}, last_line_empty ? MSG : MSG_FINAL_NEWLINE)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,8 +7,7 @@ module Ameba::Rule::Layout
|
||||||
# Layout/TrailingWhitespace:
|
# Layout/TrailingWhitespace:
|
||||||
# Enabled: true
|
# Enabled: true
|
||||||
# ```
|
# ```
|
||||||
#
|
class TrailingWhitespace < Base
|
||||||
struct TrailingWhitespace < Base
|
|
||||||
properties do
|
properties do
|
||||||
description "Disallows trailing whitespaces"
|
description "Disallows trailing whitespaces"
|
||||||
end
|
end
|
||||||
|
@ -17,8 +16,7 @@ module Ameba::Rule::Layout
|
||||||
|
|
||||||
def test(source)
|
def test(source)
|
||||||
source.lines.each_with_index do |line, index|
|
source.lines.each_with_index do |line, index|
|
||||||
next unless line =~ /\s$/
|
issue_for({index + 1, line.size}, MSG) if line =~ /\s$/
|
||||||
issue_for({index + 1, line.size}, MSG)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -17,8 +17,7 @@ module Ameba::Rule::Lint
|
||||||
# Lint/BadDirective:
|
# Lint/BadDirective:
|
||||||
# Enabled: true
|
# Enabled: true
|
||||||
# ```
|
# ```
|
||||||
#
|
class BadDirective < Base
|
||||||
struct BadDirective < Base
|
|
||||||
properties do
|
properties do
|
||||||
description "Reports bad comment directives"
|
description "Reports bad comment directives"
|
||||||
end
|
end
|
||||||
|
@ -48,7 +47,9 @@ module Ameba::Rule::Lint
|
||||||
|
|
||||||
private def check_rules(source, token, rules)
|
private def check_rules(source, token, rules)
|
||||||
bad_names = rules - ALL_RULE_NAMES - ALL_GROUP_NAMES
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -19,23 +19,21 @@ module Ameba::Rule::Lint
|
||||||
# Lint/ComparisonToBoolean:
|
# Lint/ComparisonToBoolean:
|
||||||
# Enabled: true
|
# Enabled: true
|
||||||
# ```
|
# ```
|
||||||
#
|
class ComparisonToBoolean < Base
|
||||||
struct ComparisonToBoolean < Base
|
|
||||||
properties do
|
properties do
|
||||||
enabled false
|
enabled false
|
||||||
description "Disallows comparison to booleans"
|
description "Disallows comparison to booleans"
|
||||||
end
|
end
|
||||||
|
|
||||||
MSG = "Comparison to a boolean is pointless"
|
MSG = "Comparison to a boolean is pointless"
|
||||||
|
OP_NAMES = %w(== != ===)
|
||||||
|
|
||||||
def test(source, node : Crystal::Call)
|
def test(source, node : Crystal::Call)
|
||||||
comparison = %w(== != ===).includes?(node.name)
|
comparison = node.name.in?(OP_NAMES)
|
||||||
to_boolean = node.args.first?.try &.is_a?(Crystal::BoolLiteral) ||
|
to_boolean = node.args.first?.try(&.is_a?(Crystal::BoolLiteral)) ||
|
||||||
node.obj.is_a?(Crystal::BoolLiteral)
|
node.obj.is_a?(Crystal::BoolLiteral)
|
||||||
|
|
||||||
return unless comparison && to_boolean
|
issue_for node, MSG if comparison && to_boolean
|
||||||
|
|
||||||
issue_for node, MSG
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,8 +10,7 @@ module Ameba::Rule::Lint
|
||||||
# Lint/DebuggerStatement:
|
# Lint/DebuggerStatement:
|
||||||
# Enabled: true
|
# Enabled: true
|
||||||
# ```
|
# ```
|
||||||
#
|
class DebuggerStatement < Base
|
||||||
struct DebuggerStatement < Base
|
|
||||||
properties do
|
properties do
|
||||||
description "Disallows calls to debugger"
|
description "Disallows calls to debugger"
|
||||||
end
|
end
|
||||||
|
|
31
src/ameba/rule/lint/duplicated_require.cr
Normal file
31
src/ameba/rule/lint/duplicated_require.cr
Normal 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
|
|
@ -38,8 +38,7 @@ module Ameba::Rule::Lint
|
||||||
# Lint/EmptyEnsure
|
# Lint/EmptyEnsure
|
||||||
# Enabled: true
|
# Enabled: true
|
||||||
# ```
|
# ```
|
||||||
#
|
class EmptyEnsure < Base
|
||||||
struct EmptyEnsure < Base
|
|
||||||
properties do
|
properties do
|
||||||
description "Disallows empty ensure statement"
|
description "Disallows empty ensure statement"
|
||||||
end
|
end
|
||||||
|
|
|
@ -27,13 +27,12 @@ module Ameba::Rule::Lint
|
||||||
# Lint/EmptyExpression:
|
# Lint/EmptyExpression:
|
||||||
# Enabled: true
|
# Enabled: true
|
||||||
# ```
|
# ```
|
||||||
#
|
class EmptyExpression < Base
|
||||||
struct EmptyExpression < Base
|
|
||||||
include AST::Util
|
include AST::Util
|
||||||
|
|
||||||
properties do
|
properties do
|
||||||
description "Disallows empty expressions"
|
|
||||||
enabled false
|
enabled false
|
||||||
|
description "Disallows empty expressions"
|
||||||
end
|
end
|
||||||
|
|
||||||
MSG = "Avoid empty expression %s"
|
MSG = "Avoid empty expression %s"
|
||||||
|
@ -41,8 +40,7 @@ module Ameba::Rule::Lint
|
||||||
|
|
||||||
def test(source, node : Crystal::NilLiteral)
|
def test(source, node : Crystal::NilLiteral)
|
||||||
exp = node_source(node, source.lines).try &.join
|
exp = node_source(node, source.lines).try &.join
|
||||||
|
return if exp.in?(nil, "nil")
|
||||||
return if exp.nil? || exp == "nil"
|
|
||||||
|
|
||||||
issue_for node, MSG % exp
|
issue_for node, MSG % exp
|
||||||
end
|
end
|
||||||
|
|
|
@ -37,7 +37,7 @@ module Ameba::Rule::Lint
|
||||||
# Lint/EmptyLoop:
|
# Lint/EmptyLoop:
|
||||||
# Enabled: true
|
# Enabled: true
|
||||||
# ```
|
# ```
|
||||||
struct EmptyLoop < Base
|
class EmptyLoop < Base
|
||||||
include AST::Util
|
include AST::Util
|
||||||
|
|
||||||
properties do
|
properties do
|
||||||
|
|
|
@ -19,8 +19,7 @@ module Ameba::Rule::Lint
|
||||||
# Lint/HashDuplicatedKey:
|
# Lint/HashDuplicatedKey:
|
||||||
# Enabled: true
|
# Enabled: true
|
||||||
# ```
|
# ```
|
||||||
#
|
class HashDuplicatedKey < Base
|
||||||
struct HashDuplicatedKey < Base
|
|
||||||
properties do
|
properties do
|
||||||
description "Disallows duplicated keys in hash literals"
|
description "Disallows duplicated keys in hash literals"
|
||||||
end
|
end
|
||||||
|
@ -28,7 +27,7 @@ module Ameba::Rule::Lint
|
||||||
MSG = "Duplicated keys in hash literal: %s"
|
MSG = "Duplicated keys in hash literal: %s"
|
||||||
|
|
||||||
def test(source, node : Crystal::HashLiteral)
|
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(", ")
|
issue_for node, MSG % keys.join(", ")
|
||||||
end
|
end
|
||||||
|
@ -36,7 +35,7 @@ module Ameba::Rule::Lint
|
||||||
private def duplicated_keys(entries)
|
private def duplicated_keys(entries)
|
||||||
entries.map(&.key)
|
entries.map(&.key)
|
||||||
.group_by(&.itself)
|
.group_by(&.itself)
|
||||||
.select { |_, v| v.size > 1 }
|
.tap(&.select! { |_, v| v.size > 1 })
|
||||||
.map { |k, _| k }
|
.map { |k, _| k }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -19,8 +19,7 @@ module Ameba::Rule::Lint
|
||||||
# Lint/LiteralInCondition:
|
# Lint/LiteralInCondition:
|
||||||
# Enabled: true
|
# Enabled: true
|
||||||
# ```
|
# ```
|
||||||
#
|
class LiteralInCondition < Base
|
||||||
struct LiteralInCondition < Base
|
|
||||||
include AST::Util
|
include AST::Util
|
||||||
|
|
||||||
properties do
|
properties do
|
||||||
|
@ -31,8 +30,7 @@ module Ameba::Rule::Lint
|
||||||
MSG = "Literal value found in conditional"
|
MSG = "Literal value found in conditional"
|
||||||
|
|
||||||
def check_node(source, node)
|
def check_node(source, node)
|
||||||
return unless literal?(node.cond)
|
issue_for node, MSG if literal?(node.cond)
|
||||||
issue_for node, MSG
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def test(source, node : Crystal::If)
|
def test(source, node : Crystal::If)
|
||||||
|
|
|
@ -15,8 +15,7 @@ module Ameba::Rule::Lint
|
||||||
# Lint/LiteralInInterpolation
|
# Lint/LiteralInInterpolation
|
||||||
# Enabled: true
|
# Enabled: true
|
||||||
# ```
|
# ```
|
||||||
#
|
class LiteralInInterpolation < Base
|
||||||
struct LiteralInInterpolation < Base
|
|
||||||
include AST::Util
|
include AST::Util
|
||||||
|
|
||||||
properties do
|
properties do
|
||||||
|
|
|
@ -23,12 +23,12 @@ module Ameba::Rule::Lint
|
||||||
# StringArrayUnwantedSymbols: ',"'
|
# StringArrayUnwantedSymbols: ',"'
|
||||||
# SymbolArrayUnwantedSymbols: ',:'
|
# SymbolArrayUnwantedSymbols: ',:'
|
||||||
# ```
|
# ```
|
||||||
#
|
class PercentArrays < Base
|
||||||
struct PercentArrays < Base
|
|
||||||
properties do
|
properties do
|
||||||
description "Disallows some unwanted symbols in percent array literals"
|
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
|
end
|
||||||
|
|
||||||
MSG = "Symbols `%s` may be unwanted in %s array literals"
|
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!
|
issue_for start_token.not_nil!, issue.not_nil!
|
||||||
end
|
end
|
||||||
issue = start_token = nil
|
issue = start_token = nil
|
||||||
else
|
|
||||||
# nop
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -61,14 +59,11 @@ module Ameba::Rule::Lint
|
||||||
check_array_entry entry, string_array_unwanted_symbols, "%w"
|
check_array_entry entry, string_array_unwanted_symbols, "%w"
|
||||||
when .starts_with? "%i"
|
when .starts_with? "%i"
|
||||||
check_array_entry entry, symbol_array_unwanted_symbols, "%i"
|
check_array_entry entry, symbol_array_unwanted_symbols, "%i"
|
||||||
else
|
|
||||||
# nop
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private def check_array_entry(entry, symbols, literal)
|
private def check_array_entry(entry, symbols, literal)
|
||||||
return unless entry =~ /[#{symbols}]/
|
MSG % {symbols, literal} if entry =~ /[#{symbols}]/
|
||||||
MSG % {symbols, literal}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -22,8 +22,7 @@ module Ameba::Rule::Lint
|
||||||
# Lint/RandZero:
|
# Lint/RandZero:
|
||||||
# Enabled: true
|
# Enabled: true
|
||||||
# ```
|
# ```
|
||||||
#
|
class RandZero < Base
|
||||||
struct RandZero < Base
|
|
||||||
properties do
|
properties do
|
||||||
description "Disallows rand zero calls"
|
description "Disallows rand zero calls"
|
||||||
end
|
end
|
||||||
|
@ -34,9 +33,9 @@ module Ameba::Rule::Lint
|
||||||
return unless node.name == "rand" &&
|
return unless node.name == "rand" &&
|
||||||
node.args.size == 1 &&
|
node.args.size == 1 &&
|
||||||
(arg = node.args.first) &&
|
(arg = node.args.first) &&
|
||||||
(arg.is_a? Crystal::NumberLiteral) &&
|
arg.is_a?(Crystal::NumberLiteral) &&
|
||||||
(value = arg.value) &&
|
(value = arg.value) &&
|
||||||
(value == "0" || value == "1")
|
value.in?("0", "1")
|
||||||
|
|
||||||
issue_for node, MSG % node
|
issue_for node, MSG % node
|
||||||
end
|
end
|
||||||
|
|
|
@ -20,8 +20,7 @@ module Ameba::Rule::Lint
|
||||||
# Lint/RedundantStringCoersion
|
# Lint/RedundantStringCoersion
|
||||||
# Enabled: true
|
# Enabled: true
|
||||||
# ```
|
# ```
|
||||||
#
|
class RedundantStringCoercion < Base
|
||||||
struct RedundantStringCoercion < Base
|
|
||||||
include AST::Util
|
include AST::Util
|
||||||
|
|
||||||
properties do
|
properties do
|
||||||
|
@ -31,7 +30,9 @@ module Ameba::Rule::Lint
|
||||||
MSG = "Redundant use of `Object#to_s` in interpolation"
|
MSG = "Redundant use of `Object#to_s` in interpolation"
|
||||||
|
|
||||||
def test(source, node : Crystal::StringInterpolation)
|
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
|
end
|
||||||
|
|
||||||
private def string_coercion_nodes(node)
|
private def string_coercion_nodes(node)
|
||||||
|
|
|
@ -26,8 +26,7 @@ module Ameba::Rule::Lint
|
||||||
# Lint/RedundantWithIndex:
|
# Lint/RedundantWithIndex:
|
||||||
# Enabled: true
|
# Enabled: true
|
||||||
# ```
|
# ```
|
||||||
#
|
class RedundantWithIndex < Base
|
||||||
struct RedundantWithIndex < Base
|
|
||||||
properties do
|
properties do
|
||||||
description "Disallows redundant `with_index` calls"
|
description "Disallows redundant `with_index` calls"
|
||||||
end
|
end
|
||||||
|
@ -35,15 +34,14 @@ module Ameba::Rule::Lint
|
||||||
def test(source, node : Crystal::Call)
|
def test(source, node : Crystal::Call)
|
||||||
args, block = node.args, node.block
|
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
|
case node.name
|
||||||
when "with_index"
|
when "with_index"
|
||||||
report source, node, "Remove redundant with_index"
|
report source, node, "Remove redundant with_index"
|
||||||
when "each_with_index"
|
when "each_with_index"
|
||||||
report source, node, "Use each instead of each_with_index"
|
report source, node, "Use each instead of each_with_index"
|
||||||
else
|
|
||||||
# nop
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -27,21 +27,20 @@ module Ameba::Rule::Lint
|
||||||
# Lint/RedundantWithObject:
|
# Lint/RedundantWithObject:
|
||||||
# Enabled: true
|
# Enabled: true
|
||||||
# ```
|
# ```
|
||||||
#
|
class RedundantWithObject < Base
|
||||||
struct RedundantWithObject < Base
|
|
||||||
properties do
|
properties do
|
||||||
description "Disallows redundant `with_object` calls"
|
description "Disallows redundant `with_object` calls"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
MSG = "Use `each` instead of `each_with_object`"
|
||||||
|
|
||||||
def test(source, node : Crystal::Call)
|
def test(source, node : Crystal::Call)
|
||||||
return if node.name != "each_with_object" ||
|
return if node.name != "each_with_object" ||
|
||||||
node.args.size != 1 ||
|
node.args.size != 1 ||
|
||||||
node.block.nil? ||
|
node.block.nil? ||
|
||||||
with_index_arg?(node.block.not_nil!)
|
with_index_arg?(node.block.not_nil!)
|
||||||
|
|
||||||
issue_for node.name_location,
|
issue_for node.name_location, node.name_end_location, MSG
|
||||||
node.name_end_location,
|
|
||||||
"Use each instead of each_with_object"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private def with_index_arg?(block : Crystal::Block)
|
private def with_index_arg?(block : Crystal::Block)
|
||||||
|
|
|
@ -35,8 +35,7 @@ module Ameba::Rule::Lint
|
||||||
# Lint/ShadowedArgument:
|
# Lint/ShadowedArgument:
|
||||||
# Enabled: true
|
# Enabled: true
|
||||||
# ```
|
# ```
|
||||||
#
|
class ShadowedArgument < Base
|
||||||
struct ShadowedArgument < Base
|
|
||||||
properties do
|
properties do
|
||||||
description "Disallows shadowed arguments"
|
description "Disallows shadowed arguments"
|
||||||
end
|
end
|
||||||
|
|
|
@ -33,8 +33,7 @@ module Ameba::Rule::Lint
|
||||||
# Lint/ShadowedException:
|
# Lint/ShadowedException:
|
||||||
# Enabled: true
|
# Enabled: true
|
||||||
# ```
|
# ```
|
||||||
#
|
class ShadowedException < Base
|
||||||
struct ShadowedException < Base
|
|
||||||
properties do
|
properties do
|
||||||
description "Disallows rescued exception that get shadowed"
|
description "Disallows rescued exception that get shadowed"
|
||||||
end
|
end
|
||||||
|
@ -42,18 +41,17 @@ module Ameba::Rule::Lint
|
||||||
MSG = "Exception handler has shadowed exceptions: %s"
|
MSG = "Exception handler has shadowed exceptions: %s"
|
||||||
|
|
||||||
def test(source, node : Crystal::ExceptionHandler)
|
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(", ")
|
issue_for node, MSG % excs.join(", ")
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
private def shadowed(exceptions, exception_found = false)
|
private def shadowed(exceptions, exception_found = false)
|
||||||
previous_exceptions = [] of String
|
previous_exceptions = [] of String
|
||||||
|
|
||||||
exceptions.reduce([] of String) do |shadowed, excs|
|
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
|
if exception_found
|
||||||
shadowed.concat excs
|
shadowed.concat excs
|
||||||
|
|
|
@ -30,8 +30,7 @@ module Ameba::Rule::Lint
|
||||||
# Lint/ShadowingOuterLocalVar:
|
# Lint/ShadowingOuterLocalVar:
|
||||||
# Enabled: true
|
# Enabled: true
|
||||||
# ```
|
# ```
|
||||||
#
|
class ShadowingOuterLocalVar < Base
|
||||||
struct ShadowingOuterLocalVar < Base
|
|
||||||
properties do
|
properties do
|
||||||
description "Disallows the usage of the same name as outer local variables" \
|
description "Disallows the usage of the same name as outer local variables" \
|
||||||
" for block or proc arguments."
|
" for block or proc arguments."
|
||||||
|
|
|
@ -49,8 +49,7 @@ module Ameba::Rule::Lint
|
||||||
# Lint/SharedVarInFiber:
|
# Lint/SharedVarInFiber:
|
||||||
# Enabled: true
|
# Enabled: true
|
||||||
# ```
|
# ```
|
||||||
#
|
class SharedVarInFiber < Base
|
||||||
struct SharedVarInFiber < Base
|
|
||||||
properties do
|
properties do
|
||||||
description "Disallows shared variables in fibers."
|
description "Disallows shared variables in fibers."
|
||||||
end
|
end
|
||||||
|
@ -77,7 +76,7 @@ module Ameba::Rule::Lint
|
||||||
declared_in = variable.assignments.first?.try &.branch
|
declared_in = variable.assignments.first?.try &.branch
|
||||||
|
|
||||||
variable.assignments
|
variable.assignments
|
||||||
.reject { |assign| assign.scope.spawn_block? }
|
.reject(&.scope.spawn_block?)
|
||||||
.any? do |assign|
|
.any? do |assign|
|
||||||
assign.branch.try(&.in_loop?) && assign.branch != declared_in
|
assign.branch.try(&.in_loop?) && assign.branch != declared_in
|
||||||
end
|
end
|
||||||
|
|
70
src/ameba/rule/lint/spec_focus.cr
Normal file
70
src/ameba/rule/lint/spec_focus.cr
Normal 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
|
|
@ -18,8 +18,7 @@ module Ameba::Rule::Lint
|
||||||
# rescue e : Exception
|
# rescue e : Exception
|
||||||
# end
|
# end
|
||||||
# ```
|
# ```
|
||||||
#
|
class Syntax < Base
|
||||||
struct Syntax < Base
|
|
||||||
properties do
|
properties do
|
||||||
description "Reports invalid Crystal syntax"
|
description "Reports invalid Crystal syntax"
|
||||||
severity Ameba::Severity::Error
|
severity Ameba::Severity::Error
|
||||||
|
|
|
@ -24,8 +24,7 @@ module Ameba::Rule::Lint
|
||||||
# Lint/UnneededDisableDirective
|
# Lint/UnneededDisableDirective
|
||||||
# Enabled: true
|
# Enabled: true
|
||||||
# ```
|
# ```
|
||||||
#
|
class UnneededDisableDirective < Base
|
||||||
struct UnneededDisableDirective < Base
|
|
||||||
properties do
|
properties do
|
||||||
description "Reports unneeded disable directives in comments"
|
description "Reports unneeded disable directives in comments"
|
||||||
end
|
end
|
||||||
|
@ -37,7 +36,7 @@ module Ameba::Rule::Lint
|
||||||
next unless token.type == :COMMENT
|
next unless token.type == :COMMENT
|
||||||
next unless directive = source.parse_inline_directive(token.value.to_s)
|
next unless directive = source.parse_inline_directive(token.value.to_s)
|
||||||
next unless names = unneeded_disables(source, directive, token.location)
|
next unless names = unneeded_disables(source, directive, token.location)
|
||||||
next unless names.any?
|
next if names.empty?
|
||||||
|
|
||||||
issue_for token, MSG % names.join(", ")
|
issue_for token, MSG % names.join(", ")
|
||||||
end
|
end
|
||||||
|
@ -47,11 +46,12 @@ module Ameba::Rule::Lint
|
||||||
return unless directive[:action] == "disable"
|
return unless directive[:action] == "disable"
|
||||||
|
|
||||||
directive[:rules].reject do |rule_name|
|
directive[:rules].reject do |rule_name|
|
||||||
|
next if rule_name == self.name
|
||||||
source.issues.any? do |issue|
|
source.issues.any? do |issue|
|
||||||
issue.rule.name == rule_name &&
|
issue.rule.name == rule_name &&
|
||||||
issue.disabled? &&
|
issue.disabled? &&
|
||||||
issue_at_location?(source, issue, location)
|
issue_at_location?(source, issue, location)
|
||||||
end && rule_name != self.name
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -41,8 +41,7 @@ module Ameba::Rule::Lint
|
||||||
# Lint/UnreachableCode:
|
# Lint/UnreachableCode:
|
||||||
# Enabled: true
|
# Enabled: true
|
||||||
# ```
|
# ```
|
||||||
#
|
class UnreachableCode < Base
|
||||||
struct UnreachableCode < Base
|
|
||||||
include AST::Util
|
include AST::Util
|
||||||
|
|
||||||
properties do
|
properties do
|
||||||
|
|
|
@ -24,8 +24,7 @@ module Ameba::Rule::Lint
|
||||||
# IgnoreBlocks: false
|
# IgnoreBlocks: false
|
||||||
# IgnoreProcs: false
|
# IgnoreProcs: false
|
||||||
# ```
|
# ```
|
||||||
#
|
class UnusedArgument < Base
|
||||||
struct UnusedArgument < Base
|
|
||||||
properties do
|
properties do
|
||||||
description "Disallows unused arguments"
|
description "Disallows unused arguments"
|
||||||
|
|
||||||
|
|
|
@ -25,8 +25,7 @@ module Ameba::Rule::Lint
|
||||||
# Lint/UselessAssign:
|
# Lint/UselessAssign:
|
||||||
# Enabled: true
|
# Enabled: true
|
||||||
# ```
|
# ```
|
||||||
#
|
class UselessAssign < Base
|
||||||
struct UselessAssign < Base
|
|
||||||
properties do
|
properties do
|
||||||
description "Disallows useless variable assignments"
|
description "Disallows useless variable assignments"
|
||||||
end
|
end
|
||||||
|
|
|
@ -30,8 +30,7 @@ module Ameba::Rule::Lint
|
||||||
# Lint/UselessConditionInWhen:
|
# Lint/UselessConditionInWhen:
|
||||||
# Enabled: true
|
# Enabled: true
|
||||||
# ```
|
# ```
|
||||||
#
|
class UselessConditionInWhen < Base
|
||||||
struct UselessConditionInWhen < Base
|
|
||||||
properties do
|
properties do
|
||||||
description "Disallows useless conditions in when"
|
description "Disallows useless conditions in when"
|
||||||
end
|
end
|
||||||
|
@ -43,10 +42,7 @@ module Ameba::Rule::Lint
|
||||||
# simple implementation in future.
|
# simple implementation in future.
|
||||||
protected def check_node(source, when_node, cond)
|
protected def check_node(source, when_node, cond)
|
||||||
cond_s = cond.to_s
|
cond_s = cond.to_s
|
||||||
return if when_node
|
return if when_node.conds.none?(&.to_s.==(cond_s))
|
||||||
.conds
|
|
||||||
.map(&.to_s)
|
|
||||||
.none? { |c| c == cond_s }
|
|
||||||
|
|
||||||
issue_for cond, MSG
|
issue_for cond, MSG
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,8 +8,7 @@ module Ameba::Rule::Metrics
|
||||||
# Enabled: true
|
# Enabled: true
|
||||||
# MaxComplexity: 10
|
# MaxComplexity: 10
|
||||||
# ```
|
# ```
|
||||||
#
|
class CyclomaticComplexity < Base
|
||||||
struct CyclomaticComplexity < Base
|
|
||||||
properties do
|
properties do
|
||||||
description "Disallows methods with a cyclomatic complexity higher than `MaxComplexity`"
|
description "Disallows methods with a cyclomatic complexity higher than `MaxComplexity`"
|
||||||
max_complexity 10
|
max_complexity 10
|
||||||
|
@ -21,17 +20,17 @@ module Ameba::Rule::Metrics
|
||||||
complexity = AST::CountingVisitor.new(node).count
|
complexity = AST::CountingVisitor.new(node).count
|
||||||
|
|
||||||
if complexity > max_complexity && (location = node.name_location)
|
if complexity > max_complexity && (location = node.name_location)
|
||||||
issue_for(
|
issue_for location, def_name_end_location(node),
|
||||||
location,
|
|
||||||
def_name_end_location(node),
|
|
||||||
MSG % {complexity, max_complexity}
|
MSG % {complexity, max_complexity}
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private def def_name_end_location(node)
|
private def def_name_end_location(node)
|
||||||
return unless location = node.name_location
|
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)
|
Crystal::Location.new(location.filename, line_number, column_number + node.name.size)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -24,23 +24,21 @@ module Ameba::Rule::Performance
|
||||||
# - select
|
# - select
|
||||||
# - reject
|
# - reject
|
||||||
# ```
|
# ```
|
||||||
#
|
class AnyAfterFilter < Base
|
||||||
struct AnyAfterFilter < Base
|
|
||||||
ANY_NAME = "any?"
|
|
||||||
MSG = "Use `#{ANY_NAME} {...}` instead of `%s {...}.#{ANY_NAME}`"
|
|
||||||
|
|
||||||
properties do
|
properties do
|
||||||
filter_names : Array(String) = %w(select reject)
|
|
||||||
description "Identifies usage of `any?` calls that follow filters."
|
description "Identifies usage of `any?` calls that follow filters."
|
||||||
|
filter_names : Array(String) = %w(select reject)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
ANY_NAME = "any?"
|
||||||
|
MSG = "Use `any? {...}` instead of `%s {...}.any?`"
|
||||||
|
|
||||||
def test(source, node : Crystal::Call)
|
def test(source, node : Crystal::Call)
|
||||||
return unless node.name == ANY_NAME && (obj = node.obj)
|
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
|
issue_for obj.name_location, node.name_end_location, MSG % obj.name
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
44
src/ameba/rule/performance/any_instead_of_empty.cr
Normal file
44
src/ameba/rule/performance/any_instead_of_empty.cr
Normal 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
|
75
src/ameba/rule/performance/chained_call_with_no_bang.cr
Normal file
75
src/ameba/rule/performance/chained_call_with_no_bang.cr
Normal 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
|
48
src/ameba/rule/performance/compact_after_map.cr
Normal file
48
src/ameba/rule/performance/compact_after_map.cr
Normal 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
|
|
@ -23,17 +23,16 @@ module Ameba::Rule::Performance
|
||||||
# FilterNames:
|
# FilterNames:
|
||||||
# - select
|
# - select
|
||||||
# ```
|
# ```
|
||||||
#
|
class FirstLastAfterFilter < Base
|
||||||
struct 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?)
|
CALL_NAMES = %w(first last first? last?)
|
||||||
MSG = "Use `find {...}` instead of `%s {...}.%s`"
|
MSG = "Use `find {...}` instead of `%s {...}.%s`"
|
||||||
MSG_REVERSE = "Use `reverse_each.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)
|
def test(source)
|
||||||
AST::NodeVisitor.new self, source, skip: [
|
AST::NodeVisitor.new self, source, skip: [
|
||||||
Crystal::Macro,
|
Crystal::Macro,
|
||||||
|
@ -44,14 +43,13 @@ module Ameba::Rule::Performance
|
||||||
end
|
end
|
||||||
|
|
||||||
def test(source, node : Crystal::Call)
|
def test(source, node : Crystal::Call)
|
||||||
return unless CALL_NAMES.includes?(node.name) && (obj = node.obj)
|
return unless node.name.in?(CALL_NAMES) && (obj = node.obj)
|
||||||
return if node.args.any?
|
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
|
message = node.name.includes?(CALL_NAMES.first) ? MSG : MSG_REVERSE
|
||||||
issue_for obj.name_location, node.name_end_location, message % {obj.name, node.name}
|
issue_for obj.name_location, node.name_end_location, message % {obj.name, node.name}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
48
src/ameba/rule/performance/flatten_after_map.cr
Normal file
48
src/ameba/rule/performance/flatten_after_map.cr
Normal 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
|
52
src/ameba/rule/performance/map_instead_of_block.cr
Normal file
52
src/ameba/rule/performance/map_instead_of_block.cr
Normal 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
|
|
@ -30,16 +30,15 @@ module Ameba::Rule::Performance
|
||||||
# - select
|
# - select
|
||||||
# - reject
|
# - reject
|
||||||
# ```
|
# ```
|
||||||
#
|
class SizeAfterFilter < Base
|
||||||
struct SizeAfterFilter < Base
|
|
||||||
SIZE_NAME = "size"
|
|
||||||
MSG = "Use `count {...}` instead of `%s {...}.#{SIZE_NAME}`."
|
|
||||||
|
|
||||||
properties do
|
properties do
|
||||||
filter_names : Array(String) = %w(select reject)
|
|
||||||
description "Identifies usage of `size` calls that follow filter"
|
description "Identifies usage of `size` calls that follow filter"
|
||||||
|
filter_names : Array(String) = %w(select reject)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
SIZE_NAME = "size"
|
||||||
|
MSG = "Use `count {...}` instead of `%s {...}.size`."
|
||||||
|
|
||||||
def test(source)
|
def test(source)
|
||||||
AST::NodeVisitor.new self, source, skip: [
|
AST::NodeVisitor.new self, source, skip: [
|
||||||
Crystal::Macro,
|
Crystal::Macro,
|
||||||
|
@ -51,11 +50,10 @@ module Ameba::Rule::Performance
|
||||||
|
|
||||||
def test(source, node : Crystal::Call)
|
def test(source, node : Crystal::Call)
|
||||||
return unless node.name == SIZE_NAME && (obj = node.obj)
|
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
|
issue_for obj.name_location, node.name_end_location, MSG % obj.name
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
|
@ -21,8 +21,7 @@ module Ameba::Rule::Style
|
||||||
# Style/ConstantNames:
|
# Style/ConstantNames:
|
||||||
# Enabled: true
|
# Enabled: true
|
||||||
# ```
|
# ```
|
||||||
#
|
class ConstantNames < Base
|
||||||
struct ConstantNames < Base
|
|
||||||
properties do
|
properties do
|
||||||
description "Enforces constant names to be in screaming case"
|
description "Enforces constant names to be in screaming case"
|
||||||
end
|
end
|
||||||
|
@ -30,11 +29,11 @@ module Ameba::Rule::Style
|
||||||
MSG = "Constant name should be screaming-cased: %s, not %s"
|
MSG = "Constant name should be screaming-cased: %s, not %s"
|
||||||
|
|
||||||
def test(source, node : Crystal::Assign)
|
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
|
name = target.names.first
|
||||||
expected = name.upcase
|
expected = name.upcase
|
||||||
|
|
||||||
return if expected == name || name.camelcase == name
|
return if name.in?(expected, name.camelcase)
|
||||||
|
|
||||||
issue_for target, MSG % {expected, name}
|
issue_for target, MSG % {expected, name}
|
||||||
end
|
end
|
||||||
|
|
74
src/ameba/rule/style/is_a_filter.cr
Normal file
74
src/ameba/rule/style/is_a_filter.cr
Normal 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
|
|
@ -4,7 +4,7 @@ module Ameba::Rule::Style
|
||||||
# This is considered bad:
|
# This is considered bad:
|
||||||
#
|
#
|
||||||
# ```
|
# ```
|
||||||
# var.is_a? Nil
|
# var.is_a?(Nil)
|
||||||
# ```
|
# ```
|
||||||
#
|
#
|
||||||
# And needs to be written as:
|
# And needs to be written as:
|
||||||
|
@ -19,8 +19,7 @@ module Ameba::Rule::Style
|
||||||
# Style/IsANil:
|
# Style/IsANil:
|
||||||
# Enabled: true
|
# Enabled: true
|
||||||
# ```
|
# ```
|
||||||
#
|
class IsANil < Base
|
||||||
struct IsANil < Base
|
|
||||||
properties do
|
properties do
|
||||||
description "Disallows calls to `is_a?(Nil)` in favor of `nil?`"
|
description "Disallows calls to `is_a?(Nil)` in favor of `nil?`"
|
||||||
end
|
end
|
||||||
|
@ -32,7 +31,10 @@ module Ameba::Rule::Style
|
||||||
return if node.nil_check?
|
return if node.nil_check?
|
||||||
|
|
||||||
const = node.const
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -26,12 +26,11 @@ module Ameba::Rule::Style
|
||||||
# Enabled: true
|
# Enabled: true
|
||||||
# IntMinDigits: 5 # i.e. integers higher than 9999
|
# IntMinDigits: 5 # i.e. integers higher than 9999
|
||||||
# ```
|
# ```
|
||||||
#
|
class LargeNumbers < Base
|
||||||
struct LargeNumbers < Base
|
|
||||||
properties do
|
properties do
|
||||||
|
enabled false
|
||||||
description "Disallows usage of large numbers without underscore"
|
description "Disallows usage of large numbers without underscore"
|
||||||
int_min_digits 5
|
int_min_digits 5
|
||||||
enabled false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
MSG = "Large numbers should be written with underscores: %s"
|
MSG = "Large numbers should be written with underscores: %s"
|
||||||
|
@ -53,7 +52,7 @@ module Ameba::Rule::Style
|
||||||
end
|
end
|
||||||
|
|
||||||
private def allowed?(_sign, value, fraction, _suffix)
|
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 = value.chars.select &.to_s.=~ /[0-9]/
|
||||||
digits.size >= int_min_digits
|
digits.size >= int_min_digits
|
||||||
|
@ -71,7 +70,7 @@ module Ameba::Rule::Style
|
||||||
value.chars.reject(&.== '_').each_slice(by) do |slice|
|
value.chars.reject(&.== '_').each_slice(by) do |slice|
|
||||||
slices << (yield slice).join
|
slices << (yield slice).join
|
||||||
end
|
end
|
||||||
end.join("_")
|
end.join('_')
|
||||||
end
|
end
|
||||||
|
|
||||||
private def parse_number(value)
|
private def parse_number(value)
|
||||||
|
@ -83,7 +82,7 @@ module Ameba::Rule::Style
|
||||||
end
|
end
|
||||||
|
|
||||||
private def parse_sign(value)
|
private def parse_sign(value)
|
||||||
if "+-".includes?(value[0])
|
if value[0].in?('+', '-')
|
||||||
sign = value[0]
|
sign = value[0]
|
||||||
value = value[1..-1]
|
value = value[1..-1]
|
||||||
end
|
end
|
||||||
|
@ -91,7 +90,7 @@ module Ameba::Rule::Style
|
||||||
end
|
end
|
||||||
|
|
||||||
private def parse_suffix(value)
|
private def parse_suffix(value)
|
||||||
if pos = (value =~ /e/ || value =~ /_?(i|u|f)/)
|
if pos = (value =~ /(e|_?(i|u|f))/)
|
||||||
suffix = value[pos..-1]
|
suffix = value[pos..-1]
|
||||||
value = value[0..pos - 1]
|
value = value[0..pos - 1]
|
||||||
end
|
end
|
||||||
|
|
|
@ -37,8 +37,7 @@ module Ameba::Rule::Style
|
||||||
# Style/MethodNames:
|
# Style/MethodNames:
|
||||||
# Enabled: true
|
# Enabled: true
|
||||||
# ```
|
# ```
|
||||||
#
|
class MethodNames < Base
|
||||||
struct MethodNames < Base
|
|
||||||
properties do
|
properties do
|
||||||
description "Enforces method names to be in underscored case"
|
description "Enforces method names to be in underscored case"
|
||||||
end
|
end
|
||||||
|
@ -51,7 +50,7 @@ module Ameba::Rule::Style
|
||||||
line_number = node.location.try &.line_number
|
line_number = node.location.try &.line_number
|
||||||
column_number = node.name_location.try &.column_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(
|
issue_for(
|
||||||
{line_number, column_number},
|
{line_number, column_number},
|
||||||
|
|
|
@ -26,8 +26,7 @@ module Ameba::Rule::Style
|
||||||
# Style/NegatedConditionsInUnless:
|
# Style/NegatedConditionsInUnless:
|
||||||
# Enabled: true
|
# Enabled: true
|
||||||
# ```
|
# ```
|
||||||
#
|
class NegatedConditionsInUnless < Base
|
||||||
struct NegatedConditionsInUnless < Base
|
|
||||||
properties do
|
properties do
|
||||||
description "Disallows negated conditions in unless"
|
description "Disallows negated conditions in unless"
|
||||||
end
|
end
|
||||||
|
@ -35,8 +34,7 @@ module Ameba::Rule::Style
|
||||||
MSG = "Avoid negated conditions in unless blocks"
|
MSG = "Avoid negated conditions in unless blocks"
|
||||||
|
|
||||||
def test(source, node : Crystal::Unless)
|
def test(source, node : Crystal::Unless)
|
||||||
return unless negated_condition? node.cond
|
issue_for node, MSG if negated_condition?(node.cond)
|
||||||
issue_for node, MSG
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private def negated_condition?(node)
|
private def negated_condition?(node)
|
||||||
|
@ -44,7 +42,7 @@ module Ameba::Rule::Style
|
||||||
when Crystal::BinaryOp
|
when Crystal::BinaryOp
|
||||||
negated_condition?(node.left) || negated_condition?(node.right)
|
negated_condition?(node.left) || negated_condition?(node.right)
|
||||||
when Crystal::Expressions
|
when Crystal::Expressions
|
||||||
node.expressions.any? { |e| negated_condition? e }
|
node.expressions.any? { |e| negated_condition?(e) }
|
||||||
when Crystal::Not
|
when Crystal::Not
|
||||||
true
|
true
|
||||||
else
|
else
|
||||||
|
|
|
@ -28,11 +28,10 @@ module Ameba::Rule::Style
|
||||||
# Style/PredicateName:
|
# Style/PredicateName:
|
||||||
# Enabled: true
|
# Enabled: true
|
||||||
# ```
|
# ```
|
||||||
#
|
class PredicateName < Base
|
||||||
struct PredicateName < Base
|
|
||||||
properties do
|
properties do
|
||||||
description "Disallows tautological predicate names"
|
|
||||||
enabled false
|
enabled false
|
||||||
|
description "Disallows tautological predicate names"
|
||||||
end
|
end
|
||||||
|
|
||||||
MSG = "Favour method name '%s?' over '%s'"
|
MSG = "Favour method name '%s?' over '%s'"
|
||||||
|
|
|
@ -55,9 +55,9 @@ module Ameba::Rule::Style
|
||||||
# Style/RedundantBegin:
|
# Style/RedundantBegin:
|
||||||
# Enabled: true
|
# Enabled: true
|
||||||
# ```
|
# ```
|
||||||
#
|
class RedundantBegin < Base
|
||||||
struct RedundantBegin < Base
|
|
||||||
include AST::Util
|
include AST::Util
|
||||||
|
|
||||||
properties do
|
properties do
|
||||||
description "Disallows redundant begin blocks"
|
description "Disallows redundant begin blocks"
|
||||||
end
|
end
|
||||||
|
@ -65,9 +65,7 @@ module Ameba::Rule::Style
|
||||||
MSG = "Redundant `begin` block detected"
|
MSG = "Redundant `begin` block detected"
|
||||||
|
|
||||||
def test(source, node : Crystal::Def)
|
def test(source, node : Crystal::Def)
|
||||||
return unless redundant_begin?(source, node)
|
issue_for node, MSG if redundant_begin?(source, node)
|
||||||
|
|
||||||
issue_for node, MSG
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private def redundant_begin?(source, node)
|
private def redundant_begin?(source, node)
|
||||||
|
@ -76,8 +74,6 @@ module Ameba::Rule::Style
|
||||||
redundant_begin_in_handler?(source, body, node)
|
redundant_begin_in_handler?(source, body, node)
|
||||||
when Crystal::Expressions
|
when Crystal::Expressions
|
||||||
redundant_begin_in_expressions?(body)
|
redundant_begin_in_expressions?(body)
|
||||||
else
|
|
||||||
# nop
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -88,7 +84,7 @@ module Ameba::Rule::Style
|
||||||
private def redundant_begin_in_handler?(source, handler, node)
|
private def redundant_begin_in_handler?(source, handler, node)
|
||||||
return false if begin_exprs_in_handler?(handler) || inner_handler?(handler)
|
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
|
def_redundant_begin? code if code
|
||||||
rescue
|
rescue
|
||||||
false
|
false
|
||||||
|
@ -107,6 +103,7 @@ module Ameba::Rule::Style
|
||||||
private def def_redundant_begin?(code)
|
private def def_redundant_begin?(code)
|
||||||
lexer = Crystal::Lexer.new code
|
lexer = Crystal::Lexer.new code
|
||||||
in_body = in_argument_list = false
|
in_body = in_argument_list = false
|
||||||
|
|
||||||
loop do
|
loop do
|
||||||
token = lexer.next_token
|
token = lexer.next_token
|
||||||
|
|
||||||
|
@ -122,6 +119,7 @@ module Ameba::Rule::Style
|
||||||
when :NEWLINE
|
when :NEWLINE
|
||||||
in_body = true unless in_argument_list
|
in_body = true unless in_argument_list
|
||||||
when :SPACE
|
when :SPACE
|
||||||
|
# ignore
|
||||||
else
|
else
|
||||||
return false if in_body
|
return false if in_body
|
||||||
end
|
end
|
||||||
|
|
|
@ -96,9 +96,10 @@ module Ameba::Rule::Style
|
||||||
# AllowMultiNext: true
|
# AllowMultiNext: true
|
||||||
# AllowEmptyNext: true
|
# AllowEmptyNext: true
|
||||||
# ```
|
# ```
|
||||||
struct RedundantNext < Base
|
class RedundantNext < Base
|
||||||
properties do
|
properties do
|
||||||
description "Reports redundant next expressions"
|
description "Reports redundant next expressions"
|
||||||
|
|
||||||
allow_multi_next true
|
allow_multi_next true
|
||||||
allow_empty_next true
|
allow_empty_next true
|
||||||
end
|
end
|
||||||
|
|
|
@ -93,9 +93,10 @@ module Ameba::Rule::Style
|
||||||
# AllowMutliReturn: true
|
# AllowMutliReturn: true
|
||||||
# AllowEmptyReturn: true
|
# AllowEmptyReturn: true
|
||||||
# ```
|
# ```
|
||||||
struct RedundantReturn < Base
|
class RedundantReturn < Base
|
||||||
properties do
|
properties do
|
||||||
description "Reports redundant return expressions"
|
description "Reports redundant return expressions"
|
||||||
|
|
||||||
allow_multi_return true
|
allow_multi_return true
|
||||||
allow_empty_return true
|
allow_empty_return true
|
||||||
end
|
end
|
||||||
|
|
|
@ -51,8 +51,7 @@ module Ameba::Rule::Style
|
||||||
# Style/TypeNames:
|
# Style/TypeNames:
|
||||||
# Enabled: true
|
# Enabled: true
|
||||||
# ```
|
# ```
|
||||||
#
|
class TypeNames < Base
|
||||||
struct TypeNames < Base
|
|
||||||
properties do
|
properties do
|
||||||
description "Enforces type names in camelcase manner"
|
description "Enforces type names in camelcase manner"
|
||||||
end
|
end
|
||||||
|
@ -62,7 +61,7 @@ module Ameba::Rule::Style
|
||||||
private def check_node(source, node)
|
private def check_node(source, node)
|
||||||
name = node.name.to_s
|
name = node.name.to_s
|
||||||
expected = name.camelcase
|
expected = name.camelcase
|
||||||
return if expected == name
|
return if name == expected
|
||||||
|
|
||||||
issue_for node, MSG % {expected, name}
|
issue_for node, MSG % {expected, name}
|
||||||
end
|
end
|
||||||
|
|
|
@ -42,8 +42,7 @@ module Ameba::Rule::Style
|
||||||
# Style/UnlessElse:
|
# Style/UnlessElse:
|
||||||
# Enabled: true
|
# Enabled: true
|
||||||
# ```
|
# ```
|
||||||
#
|
class UnlessElse < Base
|
||||||
struct UnlessElse < Base
|
|
||||||
properties do
|
properties do
|
||||||
description "Disallows the use of an `else` block with the `unless`"
|
description "Disallows the use of an `else` block with the `unless`"
|
||||||
end
|
end
|
||||||
|
@ -51,8 +50,7 @@ module Ameba::Rule::Style
|
||||||
MSG = "Favour if over unless with else"
|
MSG = "Favour if over unless with else"
|
||||||
|
|
||||||
def test(source, node : Crystal::Unless)
|
def test(source, node : Crystal::Unless)
|
||||||
return if node.else.nop?
|
issue_for node, MSG unless node.else.nop?
|
||||||
issue_for node, MSG
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -22,8 +22,7 @@ module Ameba::Rule::Style
|
||||||
# Style/VariableNames:
|
# Style/VariableNames:
|
||||||
# Enabled: true
|
# Enabled: true
|
||||||
# ```
|
# ```
|
||||||
#
|
class VariableNames < Base
|
||||||
struct VariableNames < Base
|
|
||||||
properties do
|
properties do
|
||||||
description "Enforces variable names to be in underscored case"
|
description "Enforces variable names to be in underscored case"
|
||||||
end
|
end
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue