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

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

View file

@ -4,7 +4,7 @@
<p align="center">Code style linter for Crystal<p> <p align="center">Code style linter for Crystal<p>
<p align="center"> <p align="center">
<sup> <sup>
<i> (a single-celled animal that catches food and moves about by extending fingerlike projections of protoplasm) </i> <i>(a single-celled animal that catches food and moves about by extending fingerlike projections of protoplasm)</i>
</sup> </sup>
</p> </p>
<p align="center"> <p align="center">
@ -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

View file

@ -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>

View file

@ -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

View file

@ -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

View file

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

View file

@ -42,6 +42,16 @@ module Ameba::Cli
c.except.should eq %w(RULE1 RULE2) 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

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

View file

@ -77,7 +77,7 @@ module Ameba::Rule::Lint
issue.rule.should_not be_nil issue.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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,9 +4,7 @@ module Ameba
describe Severity do describe 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

View file

@ -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"

View file

@ -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

View file

@ -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

View file

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

View file

@ -37,8 +37,7 @@ module Ameba::AST
# Returns true if this node or one of the parent branchables is a loop, false otherwise. # 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

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

View file

@ -8,12 +8,21 @@ module Ameba::Cli
def run(args = ARGV) 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

View file

@ -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 || "{}"

View file

@ -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

View file

@ -4,9 +4,8 @@ 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 = " "
include Util include Util
@ -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

View file

@ -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

View file

@ -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,10 +58,9 @@ 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
end end

View file

@ -1,22 +1,109 @@
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]? end
return if affected_line.nil? || affected_line.strip.empty? def trim(str, max_length = 120, ellipsis = " ...")
if (str.size - ellipsis.size) > max_length
str = str[0, max_length]
if str.size > ellipsis.size
str = str[0...-ellipsis.size] + ellipsis
end
end
str
end
if affected_line.size > max_length && column < max_length def context(lines, lineno, context_lines = 3, remove_empty = true)
affected_line = affected_line[0, max_length - placeholder.size - 1] + placeholder 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 end
stripped = affected_line.lstrip if remove_empty
position = column - (affected_line.size - stripped.size) + prompt.size # 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) str << "^".colorize(:yellow)
if end_location
end_lineno = end_location.line_number
end_column = end_location.column_number
if end_lineno == lineno && end_column > column
end_position = end_column - column
end_position -= 1
str << ("-" * end_position).colorize(:dark_gray)
str << "^".colorize(:yellow)
end
end
str.puts
if show_context
post_context.try &.each do |line|
line = trim(line, max_length, ellipsis)
str << prompt
str.puts(line.colorize(:dark_gray))
end
end
end end
end end
end end

View file

@ -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)

View file

@ -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)

View file

@ -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?

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

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

View file

@ -38,8 +38,7 @@ module Ameba::Rule::Lint
# Lint/EmptyEnsure # 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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."

View file

@ -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

View file

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

View file

@ -18,8 +18,7 @@ module Ameba::Rule::Lint
# rescue e : Exception # 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

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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) && issue_for obj.name_location, node.name_end_location, MSG % obj.name
filter_names.includes?(obj.name) && !obj.block.nil?
issue_for obj.name_location, node.name_end_location, MSG % obj.name
end
end end
end end
end end

View file

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

View file

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

View file

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

View file

@ -23,17 +23,16 @@ module Ameba::Rule::Performance
# FilterNames: # 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) && message = node.name.includes?(CALL_NAMES.first) ? MSG : MSG_REVERSE
filter_names.includes?(obj.name) && !obj.block.nil? issue_for obj.name_location, node.name_end_location, message % {obj.name, node.name}
message = node.name.includes?(CALL_NAMES.first) ? MSG : MSG_REVERSE
issue_for obj.name_location, node.name_end_location, message % {obj.name, node.name}
end
end end
end end
end end

View file

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

View file

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

View file

@ -30,16 +30,15 @@ module Ameba::Rule::Performance
# - select # - 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) && issue_for obj.name_location, node.name_end_location, MSG % obj.name
filter_names.includes?(obj.name) && !obj.block.nil?
issue_for obj.name_location, node.name_end_location, MSG % obj.name
end
end end
end end
end end

View file

@ -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

View file

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

View file

@ -4,7 +4,7 @@ module Ameba::Rule::Style
# This is considered bad: # 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

View file

@ -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

View file

@ -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},

View file

@ -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

View file

@ -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'"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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