Compare commits

...

59 Commits

Author SHA1 Message Date
Sijawusz Pur Rahnama 1fccbfc8b8 Set timezone to `UTC` in CI 2023-11-06 16:59:09 +01:00
Sijawusz Pur Rahnama c2b5e9449c Do not report `TODO` admonitions 2023-11-06 16:59:09 +01:00
Sijawusz Pur Rahnama d5ac394d19 Switch only `FIXME` comment to `TODO` 2023-11-06 16:59:09 +01:00
Sijawusz Pur Rahnama bdbb79f1fa Fix nonexistent method name used in error message 2023-11-06 16:59:09 +01:00
Sijawusz Pur Rahnama 1b342e8257 Make `TODOFormatter`'s configuration file path configurable
Fixes the case where formatter specs were deleting project's `.ameba.yml` file
2023-11-06 16:59:09 +01:00
Sijawusz Pur Rahnama 23c61e04c0 Add `Lint/DocumentationAdmonition` rule 2023-11-06 16:59:09 +01:00
Sijawusz Pur Rahnama ddb6e3c38f
Merge pull request #390 from crystal-ameba/refactor-rules-cli-switch
Refactor `--rules` CLI switch output + add `--describe <rule-name>` CLI switch
2023-11-05 06:44:55 +01:00
Sijawusz Pur Rahnama ef16ad6471 Add presenter specs 2023-11-05 06:39:24 +01:00
Sijawusz Pur Rahnama 1b57e2cad5 Make `Formatter::Util` extend itself for easier access 2023-11-05 06:08:40 +01:00
Sijawusz Pur Rahnama 3d3626accc Introduced new presenter abstraction 2023-11-04 01:44:59 +01:00
Sijawusz Pur Rahnama bede3f97a1 Colorize code in rule descriptions too 2023-11-04 00:49:11 +01:00
Sijawusz Pur Rahnama 8ff621ba66 Add `--describe` CLI switch 2023-11-04 00:49:11 +01:00
Sijawusz Pur Rahnama f1f21ac94d Refactor `--rules` CLI switch output 2023-11-04 00:49:11 +01:00
Sijawusz Pur Rahnama 1718945523 Refactor `ExplainFormatter` a bit 2023-11-04 00:49:11 +01:00
Sijawusz Pur Rahnama c9538220c6
Merge pull request #407 from crystal-ameba/crystal-next-compatibility
fix: crystal next compatibility
2023-10-09 23:47:32 +02:00
Vitalii Elenhaupt 789e1b77e8
fix: crystal next compatibility
refs https://github.com/crystal-lang/crystal/pull/11597
fixes https://github.com/crystal-ameba/ameba/issues/406
2023-10-06 18:57:39 +03:00
Sijawusz Pur Rahnama 7174e81a13
Merge pull request #401 from crystal-ameba/dependabot/github_actions/docker/login-action-3
Bump docker/login-action from 2 to 3
2023-09-12 23:41:13 +02:00
Sijawusz Pur Rahnama 29f84921b5
Merge pull request #402 from crystal-ameba/dependabot/github_actions/docker/build-push-action-5
Bump docker/build-push-action from 4 to 5
2023-09-12 23:41:02 +02:00
Sijawusz Pur Rahnama c7f3fe78aa
Merge pull request #403 from crystal-ameba/dependabot/github_actions/docker/metadata-action-5
Bump docker/metadata-action from 4 to 5
2023-09-12 23:40:53 +02:00
Sijawusz Pur Rahnama 2d9db35ec4
Merge pull request #404 from crystal-ameba/dependabot/github_actions/docker/setup-qemu-action-3
Bump docker/setup-qemu-action from 2 to 3
2023-09-12 23:40:44 +02:00
Sijawusz Pur Rahnama dfda3d7677
Merge pull request #405 from crystal-ameba/dependabot/github_actions/docker/setup-buildx-action-3
Bump docker/setup-buildx-action from 2 to 3
2023-09-12 23:40:34 +02:00
dependabot[bot] 0829f70256
Bump docker/setup-buildx-action from 2 to 3
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2 to 3.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-12 21:20:47 +00:00
dependabot[bot] 53b311c5eb
Bump docker/setup-qemu-action from 2 to 3
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 2 to 3.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-12 21:20:44 +00:00
dependabot[bot] 867ddb4fbd
Bump docker/metadata-action from 4 to 5
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 4 to 5.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Upgrade guide](https://github.com/docker/metadata-action/blob/master/UPGRADE.md)
- [Commits](https://github.com/docker/metadata-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: docker/metadata-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-12 21:20:41 +00:00
dependabot[bot] 6724f9a0e0
Bump docker/build-push-action from 4 to 5
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 4 to 5.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-12 21:20:37 +00:00
dependabot[bot] 6389edc5fa
Bump docker/login-action from 2 to 3
Bumps [docker/login-action](https://github.com/docker/login-action) from 2 to 3.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-12 21:20:34 +00:00
Vitalii Elenhaupt 0ab39a025b
Merge pull request #399 from crystal-ameba/dependabot/github_actions/actions/checkout-4
Bump actions/checkout from 3 to 4
2023-09-05 08:15:00 +03:00
dependabot[bot] 135ff87c7e
Bump actions/checkout from 3 to 4
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-04 21:50:31 +00:00
Vitalii Elenhaupt 18d193bd08
Merge pull request #394 from stufro/388-raise-on-invalid-file-path
Raise error when passed invalid file paths
2023-08-10 13:08:00 +03:00
Stuart Frost f96cb01015 Ensure test cleanup occurs 2023-08-05 19:28:58 +01:00
Stuart Frost 1b85ba6f22 Refactor: use unless instead of if not 2023-08-05 19:28:47 +01:00
Stuart Frost eb60b25c4e Refactor building default globs 2023-08-05 16:15:50 +01:00
Stuart Frost 7690074cab Conditionally add !lib to default globs 2023-08-04 21:48:35 +01:00
Vitalii Elenhaupt 7b8316f061
Bump v1.5.0 2023-07-28 22:40:22 +03:00
Stuart Frost b2069ea4ff Add extra test cases 2023-07-27 09:29:28 +01:00
Stuart Frost e85531df6c Rename test fixture to source.cr 2023-07-27 09:24:26 +01:00
Stuart Frost 07aebfc84a
Merge branch 'master' into 388-raise-on-invalid-file-path 2023-07-26 15:22:04 +01:00
Vitalii Elenhaupt 8ef588dc6d
Merge pull request #393 from stufro/362-raise-on-invalid-config-file-path
Raise error when passed invalid config file path
2023-07-26 17:19:44 +03:00
Stuart Frost 3b9c442e09 Raise error when passed invalid file paths 2023-07-26 15:01:59 +01:00
Stuart Frost 88e0437902 Move fixture file to spec/fixtures directory 2023-07-25 10:00:16 +01:00
Stuart Frost 4741c9f4c4 Reword generic error message on config load 2023-07-25 08:46:13 +01:00
Stuart Frost d9b2d69055 Reword error when file doesn't exist
Applied suggestion from PR

Co-authored-by: Vitalii Elenhaupt <3624712+veelenga@users.noreply.github.com>
2023-07-25 08:43:49 +01:00
Stuart Frost 5f878fb40f Move missing config file check into Ameba::Config 2023-07-24 19:10:52 +01:00
Stuart Frost 01a943d0d6 Raise error when passed invalid config file path 2023-07-24 15:30:38 +01:00
Sijawusz Pur Rahnama 8c9d234d0b
Merge pull request #391 from straight-shoota/feat/portability
Make postinstall portable
2023-07-16 20:42:37 +02:00
Johannes Müller efa9c9dba0
Makefile: Remove `run_file` target 2023-07-15 22:47:14 +02:00
Johannes Müller 15ce5437d1
Make `postinstall` portable
Using `shards build` directly instead of `make build` improves portability a lot.
This should basically "just work" almost anywhere (including Windows). No need for `make` to be available and makefile compatibility doesn't matter either.
2023-07-15 10:13:53 +02:00
Johannes Müller eacb9308a7
Utilize shards' `executables`
There's no need for copying the executables manually (which happens in both makefile targets `bin` and `run_file`).
Shards' `executables` takes care of that.

The makefile targets could potentially be dropped as well, I don't think there would be other uses case for those.
2023-07-15 10:13:53 +02:00
Sijawusz Pur Rahnama a33f98624a
Merge pull request #376 from crystal-ameba/update-to-work-with-crystal-nightly 2023-07-11 23:50:14 +02:00
Sijawusz Pur Rahnama 33c8273866
Merge pull request #387 from crystal-ameba/feature/minmax-after-map-rule
Add `Performance/MinMaxAfterMap` rule
2023-07-10 16:17:53 +02:00
Sijawusz Pur Rahnama 327ed546b9
Apply suggestions from code review
Co-authored-by: Vitalii Elenhaupt <3624712+veelenga@users.noreply.github.com>
2023-07-10 16:09:01 +02:00
Sijawusz Pur Rahnama ddff8d226b Add `Performance/MinMaxAfterMap` rule 2023-07-10 15:46:17 +02:00
Sijawusz Pur Rahnama 5cff76071a No need for such micro-optimizations, LLVM takes care of those 2023-07-02 14:34:25 +02:00
Sijawusz Pur Rahnama 29e29b8e1d Fix `Performance/ExcessiveAllocations` to exclude `each` calls without a block 2023-06-30 21:44:47 +02:00
Sijawusz Pur Rahnama 21051acfff
Merge pull request #386 from crystal-ameba/fix-issue-385 2023-06-30 20:58:56 +02:00
Sijawusz Pur Rahnama abe5237802 Add `Performance/ExcessiveAllocations` rule 2023-06-30 15:17:40 +02:00
Sijawusz Pur Rahnama b7b21ffeb0
Merge pull request #384 from crystal-ameba/fix-issue-383
Fix `Style/VerboseBlock` rule to work with binary operations
2023-06-29 10:20:13 +02:00
Sijawusz Pur Rahnama 4d0125a0f3 Fix `Style/VerboseBlock` rule to work with binary operations 2023-06-29 08:15:39 +02:00
Sijawusz Pur Rahnama db59b23f9b Fix specs against Crystal nightly 2023-06-10 01:11:21 +02:00
48 changed files with 771 additions and 120 deletions

3
.ameba.yml Normal file
View File

@ -0,0 +1,3 @@
Lint/DocumentationAdmonition:
Timezone: UTC
Admonitions: [FIXME, BUG]

View File

@ -24,18 +24,18 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into ${{ env.REGISTRY }} registry
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@ -45,7 +45,7 @@ jobs:
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v4
uses: docker/metadata-action@v5
with:
images: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
@ -61,7 +61,7 @@ jobs:
# https://github.com/docker/build-push-action
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v4
uses: docker/build-push-action@v5
with:
context: .
push: true

View File

@ -18,13 +18,16 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- name: Set timezone to UTC
uses: szenius/set-timezone@v1.2
- name: Install Crystal
uses: crystal-lang/install-crystal@v1
with:
crystal: ${{ matrix.crystal }}
- name: Download source
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install dependencies
run: shards install

View File

@ -19,7 +19,7 @@ jobs:
uses: crystal-lang/install-crystal@v1
- name: Download source
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install dependencies
run: shards install

View File

@ -30,9 +30,5 @@ bin: build
mkdir -p $(SHARD_BIN)
cp ./bin/ameba $(SHARD_BIN)
.PHONY: run_file
run_file:
cp -n ./bin/ameba.cr $(SHARD_BIN) || true
.PHONY: test
test: spec lint

View File

@ -1,5 +1,5 @@
name: ameba
version: 1.4.3
version: 1.5.0
authors:
- Vitalii Elenhaupt <velenhaupt@gmail.com>
@ -10,11 +10,12 @@ targets:
scripts:
# TODO: remove pre-compiled executable in future releases
postinstall: make bin && make run_file
postinstall: shards build -Dpreview_mt
executables:
- ameba
- ameba.cr
crystal: "~> 1.7.0"
crystal: "~> 1.9.0"
license: MIT

View File

@ -85,30 +85,5 @@ module Ameba::AST
assignment.branch.should be_nil
end
end
describe "#transformed?" do
it "returns false if the assignment is not transformed by the compiler" do
nodes = as_nodes <<-CRYSTAL
def method(a)
a = 2
end
CRYSTAL
scope = Scope.new nodes.def_nodes.first
variable = Variable.new(nodes.var_nodes.first, scope)
assignment = Assignment.new(nodes.assign_nodes.first, variable, scope)
assignment.transformed?.should be_false
end
it "returns true if the assignment is transformed by the compiler" do
nodes = as_nodes <<-CRYSTAL
array.each do |(a, b)|
end
CRYSTAL
scope = Scope.new nodes.block_nodes.first
variable = Variable.new(nodes.var_nodes.first, scope)
assignment = Assignment.new(nodes.assign_nodes.first, variable, scope)
assignment.transformed?.should be_true
end
end
end
end

View File

@ -5,7 +5,7 @@ module Ameba::Cli
describe "Cmd" do
describe ".run" do
it "runs ameba" do
r = Cli.run %w(-f silent file.cr)
r = Cli.run %w(-f silent -c spec/fixtures/config.yml spec/fixtures/source.cr)
r.should be_nil
end
end
@ -43,12 +43,12 @@ module Ameba::Cli
end
it "defaults rules? flag to false" do
c = Cli.parse_args %w(file.cr)
c = Cli.parse_args %w(spec/fixtures/source.cr)
c.rules?.should be_false
end
it "defaults skip_reading_config? flag to false" do
c = Cli.parse_args %w(file.cr)
c = Cli.parse_args %w(spec/fixtures/source.cr)
c.skip_reading_config?.should be_false
end
@ -58,7 +58,7 @@ module Ameba::Cli
end
it "defaults all? flag to false" do
c = Cli.parse_args %w(file.cr)
c = Cli.parse_args %w(spec/fixtures/source.cr)
c.all?.should be_false
end
@ -95,35 +95,35 @@ module Ameba::Cli
describe "-e/--explain" do
it "configures file/line/column" do
c = Cli.parse_args %w(--explain src/file.cr:3:5)
c = Cli.parse_args %w(--explain spec/fixtures/source.cr:3:5)
location_to_explain = c.location_to_explain.should_not be_nil
location_to_explain[:file].should eq "src/file.cr"
location_to_explain[:file].should eq "spec/fixtures/source.cr"
location_to_explain[:line].should eq 3
location_to_explain[:column].should eq 5
end
it "raises an error if location is not valid" do
expect_raises(Exception, "location should have PATH:line:column") do
Cli.parse_args %w(--explain src/file.cr:3)
Cli.parse_args %w(--explain spec/fixtures/source.cr:3)
end
end
it "raises an error if line number is not valid" do
expect_raises(Exception, "location should have PATH:line:column") do
Cli.parse_args %w(--explain src/file.cr:a:3)
Cli.parse_args %w(--explain spec/fixtures/source.cr:a:3)
end
end
it "raises an error if column number is not valid" do
expect_raises(Exception, "location should have PATH:line:column") do
Cli.parse_args %w(--explain src/file.cr:3:&)
Cli.parse_args %w(--explain spec/fixtures/source.cr:3:&)
end
end
it "raises an error if line/column are missing" do
expect_raises(Exception, "location should have PATH:line:column") do
Cli.parse_args %w(--explain src/file.cr)
Cli.parse_args %w(--explain spec/fixtures/source.cr)
end
end
end

View File

@ -2,17 +2,28 @@ require "../spec_helper"
module Ameba
describe Config do
config_sample = "config/ameba.yml"
config_sample = "spec/fixtures/config.yml"
it "should have a list of available formatters" do
Config::AVAILABLE_FORMATTERS.should_not be_nil
end
describe ".new" do
it "loads default globs when config is empty" do
yml = YAML.parse "{}"
config = Config.new(yml)
config.globs.should eq Config::DEFAULT_GLOBS
context "when config is empty" do
it "loads default globs" do
yml = YAML.parse "{}"
config = Config.new(yml)
config.globs.should eq ["**/*.cr"]
end
it "sets !lib as a default glob when there are .cr files in lib" do
File.touch "lib/shard.cr"
yml = YAML.parse "{}"
config = Config.new(yml)
config.globs.should eq ["**/*.cr", "!lib"]
ensure
File.delete "lib/shard.cr"
end
end
it "initializes globs as string" do
@ -84,6 +95,12 @@ module Ameba
config.formatter.should_not be_nil
end
it "raises when custom config file doesn't exist" do
expect_raises(Exception, "Unable to load config file: Config file does not exist foo.yml") do
Config.load "foo.yml"
end
end
it "loads default config" do
config = Config.load
config.should_not be_nil
@ -96,7 +113,7 @@ module Ameba
config = Config.load config_sample
it "holds source globs" do
config.globs.should eq Config::DEFAULT_GLOBS
config.globs.should eq ["**/*.cr"]
end
it "allows to set globs" do

View File

@ -1,10 +1,12 @@
require "../../spec_helper"
require "file_utils"
CONFIG_PATH = Path[Dir.tempdir] / Ameba::Config::FILENAME
module Ameba
private def with_formatter(&)
io = IO::Memory.new
formatter = Formatter::TODOFormatter.new(io)
formatter = Formatter::TODOFormatter.new(io, CONFIG_PATH)
yield formatter, io
end
@ -20,7 +22,7 @@ module Ameba
describe Formatter::TODOFormatter do
::Spec.after_each do
FileUtils.rm_rf(Ameba::Config::DEFAULT_PATH)
FileUtils.rm_rf(CONFIG_PATH)
end
context "problems not found" do
@ -45,7 +47,7 @@ module Ameba
s = Source.new "a = 1", "source.cr"
s.add_issue DummyRule.new, {1, 2}, "message"
formatter.finished([s])
io.to_s.should contain "Created #{Config::DEFAULT_PATH}"
io.to_s.should contain "Created #{CONFIG_PATH}"
end
end

View File

@ -45,6 +45,30 @@ module Ameba
subject.expand(["**/#{current_file_basename}", "**/#{current_file_basename}"])
.should eq [current_file_path]
end
it "raises an ArgumentError when the glob doesn't match any files" do
expect_raises(ArgumentError, "No files found matching foo/*") do
subject.expand(["foo/*"])
end
end
it "raises an ArgumentError when given a missing file" do
expect_raises(ArgumentError, "No files found matching foo.cr") do
subject.expand(["foo.cr"])
end
end
it "raises an ArgumentError when given a missing directory" do
expect_raises(ArgumentError, "No files found matching foo/") do
subject.expand(["foo/"])
end
end
it "raises an ArgumentError when given multiple arguments, one of which is missing" do
expect_raises(ArgumentError, "No files found matching foo.cr") do
subject.expand(["**/#{current_file_basename}", "foo.cr"])
end
end
end
end
end

View File

@ -0,0 +1,32 @@
require "../../spec_helper"
module Ameba
private def with_rule_collection_presenter(&)
with_presenter(Presenter::RuleCollectionPresenter) do |presenter, io|
rules = Config.load.rules
presenter.run(rules)
output = io.to_s
output = Formatter::Util.deansify(output).to_s
yield rules, output, presenter
end
end
describe Presenter::RuleCollectionPresenter do
it "outputs rule collection details" do
with_rule_collection_presenter do |rules, output|
rules.each do |rule|
output.should contain rule.name
output.should contain rule.severity.symbol
if description = rule.description
output.should contain description
end
end
output.should contain "Total rules: #{rules.size}"
output.should match /\d+ enabled/
end
end
end
end

View File

@ -0,0 +1,30 @@
require "../../spec_helper"
module Ameba
private def rule_presenter_each_rule(&)
with_presenter(Presenter::RulePresenter) do |presenter, io|
rules = Config.load.rules
rules.each do |rule|
presenter.run(rule)
output = io.to_s
output = Formatter::Util.deansify(output).to_s
yield rule, output, presenter
end
end
end
describe Presenter::RulePresenter do
it "outputs rule details" do
rule_presenter_each_rule do |rule, output|
output.should contain rule.name
output.should contain rule.severity.to_s
if description = rule.description
output.should contain description
end
end
end
end
end

View File

@ -0,0 +1,113 @@
require "../../../spec_helper"
module Ameba::Rule::Lint
subject = DocumentationAdmonition.new
describe DocumentationAdmonition do
it "passes for comments with admonition mid-word/sentence" do
subject.admonitions.each do |admonition|
expect_no_issues subject, <<-CRYSTAL
# Mentioning #{admonition} mid-sentence
# x#{admonition}x
# x#{admonition}
# #{admonition}x
CRYSTAL
end
end
it "fails for comments with admonition" do
subject.admonitions.each do |admonition|
expect_issue subject, <<-CRYSTAL
# #{admonition}: Single-line comment
# ^{} error: Found a #{admonition} admonition in a comment
CRYSTAL
expect_issue subject, <<-CRYSTAL
# Text before ...
# #{admonition}(some context): Part of multi-line comment
# ^{} error: Found a #{admonition} admonition in a comment
# Text after ...
CRYSTAL
expect_issue subject, <<-CRYSTAL
# #{admonition}
# ^{} error: Found a #{admonition} admonition in a comment
if rand > 0.5
end
CRYSTAL
end
end
context "with date" do
it "passes for admonitions with future date" do
subject.admonitions.each do |admonition|
future_date = (Time.utc + 21.days).to_s(format: "%F")
expect_no_issues subject, <<-CRYSTAL
# #{admonition}(#{future_date}): sth in the future
CRYSTAL
end
end
it "fails for admonitions with past date" do
subject.admonitions.each do |admonition|
past_date = (Time.utc - 21.days).to_s(format: "%F")
expect_issue subject, <<-CRYSTAL
# #{admonition}(#{past_date}): sth in the past
# ^{} error: Found a #{admonition} admonition in a comment (21 days past)
CRYSTAL
end
end
it "fails for admonitions with yesterday's date" do
subject.admonitions.each do |admonition|
yesterday_date = (Time.utc - 1.day).to_s(format: "%F")
expect_issue subject, <<-CRYSTAL
# #{admonition}(#{yesterday_date}): sth in the past
# ^{} error: Found a #{admonition} admonition in a comment (1 day past)
CRYSTAL
end
end
it "fails for admonitions with today's date" do
subject.admonitions.each do |admonition|
today_date = Time.utc.to_s(format: "%F")
expect_issue subject, <<-CRYSTAL
# #{admonition}(#{today_date}): sth in the past
# ^{} error: Found a #{admonition} admonition in a comment (today is the day!)
CRYSTAL
end
end
it "fails for admonitions with invalid date" do
subject.admonitions.each do |admonition|
expect_issue subject, <<-CRYSTAL
# #{admonition}(0000-00-00): sth wrong
# ^{} error: #{admonition} admonition error: Invalid time: "0000-00-00"
CRYSTAL
end
end
end
context "properties" do
describe "#admonitions" do
it "lets setting custom admonitions" do
rule = DocumentationAdmonition.new
rule.admonitions = %w[FOO BAR]
rule.admonitions.each do |admonition|
expect_issue rule, <<-CRYSTAL
# #{admonition}
# ^{} error: Found a #{admonition} admonition in a comment
CRYSTAL
end
subject.admonitions.each do |admonition|
expect_no_issues rule, <<-CRYSTAL
# #{admonition}
CRYSTAL
end
end
end
end
end
end

View File

@ -44,7 +44,7 @@ module Ameba::Rule::Lint
foo = 1
-> (foo : Int32) {}
# ^ error: Shadowing outer local variable `foo`
# ^^^^^^^^^^^ error: Shadowing outer local variable `foo`
end
CRYSTAL
end
@ -69,7 +69,7 @@ module Ameba::Rule::Lint
3.times do |foo|
# ^ error: Shadowing outer local variable `foo`
-> (foo : Int32) { foo + 1 }
# ^ error: Shadowing outer local variable `foo`
# ^^^^^^^^^^^ error: Shadowing outer local variable `foo`
end
CRYSTAL
end

View File

@ -115,12 +115,12 @@ module Ameba::Rule::Lint
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.end_location.to_s.should eq "source_spec.cr:1:21"
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.end_location.to_s.should eq "source_spec.cr:2:23"
second.message.should eq "Focused spec item detected"
end
end

View File

@ -52,7 +52,7 @@ module Ameba::Rule::Lint
it "reports if proc argument is unused" do
source = expect_issue subject, <<-CRYSTAL
-> (a : Int32, b : String) do
# ^ error: Unused argument `b`. If it's necessary, use `_b` as an argument name to indicate that it won't be used.
# ^^^^^^^^^^ error: Unused argument `b`. If it's necessary, use `_b` as an argument name to indicate that it won't be used.
a = a + 1
end
CRYSTAL
@ -306,7 +306,7 @@ module Ameba::Rule::Lint
expect_issue rule, <<-CRYSTAL
->(a : Int32) {}
# ^ error: Unused argument `a`. If it's necessary, use `_a` as an argument name to indicate that it won't be used.
# ^^^^^^^^^ error: Unused argument `a`. If it's necessary, use `_a` as an argument name to indicate that it won't be used.
CRYSTAL
end
end

View File

@ -233,7 +233,7 @@ module Ameba::Rule::Lint
subject.catch(s).should be_valid
end
context "when transformed" do
context "block unpacking" do
it "does not report if the first arg is transformed and not used" do
s = Source.new %(
collection.each do |(a, b)|

View File

@ -0,0 +1,58 @@
require "../../../spec_helper"
module Ameba::Rule::Performance
subject = ExcessiveAllocations.new
describe ExcessiveAllocations do
it "passes if there is no potential performance improvements" do
expect_no_issues subject, <<-CRYSTAL
"Alice".chars.each
"Alice".chars.each(arg) { |c| puts c }
"Alice".chars(arg).each { |c| puts c }
"Alice\nBob".lines.each(arg) { |l| puts l }
"Alice\nBob".lines(arg).each { |l| puts l }
CRYSTAL
end
it "reports if there is a collection method followed by each" do
source = expect_issue subject, <<-CRYSTAL
"Alice".chars.each { |c| puts c }
# ^^^^^^^^^^ error: Use `each_char {...}` instead of `chars.each {...}` to avoid excessive allocation
"Alice\nBob".lines.each { |l| puts l }
# ^^^^^^^^^^ error: Use `each_line {...}` instead of `lines.each {...}` to avoid excessive allocation
CRYSTAL
expect_correction source, <<-CRYSTAL
"Alice".each_char { |c| puts c }
"Alice\nBob".each_line { |l| puts l }
CRYSTAL
end
it "does not report if source is a spec" do
expect_no_issues subject, <<-CRYSTAL, "source_spec.cr"
"Alice".chars.each { |c| puts c }
CRYSTAL
end
context "properties" do
it "#call_names" do
rule = ExcessiveAllocations.new
rule.call_names = {
"children" => "each_child",
}
expect_no_issues rule, <<-CRYSTAL
"Alice".chars.each { |c| puts c }
CRYSTAL
end
end
context "macro" do
it "doesn't report in macro scope" do
expect_no_issues subject, <<-CRYSTAL
{{ "Alice".chars.each { |c| puts c } }}
CRYSTAL
end
end
end
end

View File

@ -0,0 +1,45 @@
require "../../../spec_helper"
module Ameba::Rule::Performance
subject = MinMaxAfterMap.new
describe MinMaxAfterMap do
it "passes if there are no potential performance improvements" do
expect_no_issues subject, <<-CRYSTAL
%w[Alice Bob].map { |name| name.size }.min(2)
%w[Alice Bob].map { |name| name.size }.max(2)
CRYSTAL
end
it "reports if there is a `min/max/minmax` call followed by `map`" do
source = expect_issue subject, <<-CRYSTAL
%w[Alice Bob].map { |name| name.size }.min
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: Use `min_of {...}` instead of `map {...}.min`.
%w[Alice Bob].map(&.size).max.zero?
# ^^^^^^^^^^^^^^^ error: Use `max_of {...}` instead of `map {...}.max`.
%w[Alice Bob].map(&.size).minmax?
# ^^^^^^^^^^^^^^^^^^^ error: Use `minmax_of? {...}` instead of `map {...}.minmax?`.
CRYSTAL
expect_correction source, <<-CRYSTAL
%w[Alice Bob].min_of { |name| name.size }
%w[Alice Bob].max_of(&.size).zero?
%w[Alice Bob].minmax_of?(&.size)
CRYSTAL
end
it "does not report if source is a spec" do
expect_no_issues subject, path: "source_spec.cr", code: <<-CRYSTAL
%w[Alice Bob].map(&.size).min
CRYSTAL
end
context "macro" do
it "doesn't report in macro scope" do
expect_no_issues subject, <<-CRYSTAL
{{ %w[Alice Bob].map(&.size).min }}
CRYSTAL
end
end
end
end

View File

@ -25,6 +25,7 @@ module Ameba::Rule::Style
(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 }
(1..3).map { |o| o.foo = foos[o.abs]? || 0 }
CRYSTAL
end

4
spec/fixtures/config.yml vendored Normal file
View File

@ -0,0 +1,4 @@
Ameba/PerfRule:
Enabled: false
Ameba/ErrorRule:
Enabled: false

0
spec/fixtures/source.cr vendored Normal file
View File

View File

@ -282,6 +282,13 @@ module Ameba
end
end
def with_presenter(klass, &)
io = IO::Memory.new
presenter = klass.new(io)
yield presenter, io
end
def as_node(source)
Crystal::Parser.new(source).parse
end

View File

@ -3,6 +3,7 @@ require "./ameba/ast/**"
require "./ameba/ext/**"
require "./ameba/rule/**"
require "./ameba/formatter/*"
require "./ameba/presenter/*"
require "./ameba/source/**"
# Ameba's entry module.

View File

@ -76,6 +76,9 @@ module Ameba::AST
end
end
# TODO: Remove in a next release. BC for crystal <= 1.9.
# refs https://github.com/crystal-ameba/ameba/pull/407
#
# Indicates whether the node is a transformed assignment by the compiler.
# i.e.
#

View File

@ -21,7 +21,6 @@ module Ameba::AST
}
SPECIAL_NODE_NAMES = %w[super previous_def]
RECORD_NODE_NAME = "record"
@scope_queue = [] of Scope
@current_scope : Scope
@ -190,7 +189,7 @@ module Ameba::AST
end
private def record_macro?(node)
node.name == RECORD_NODE_NAME && node.args.first?.is_a?(Crystal::Path)
node.name == "record" && node.args.first?.is_a?(Crystal::Path)
end
private def skip?(node)

View File

@ -28,7 +28,14 @@ module Ameba::Cli
configure_rules(config, opts)
if opts.rules?
print_rules(config)
print_rules(config.rules)
end
if describe_rule_name = opts.describe_rule
unless rule = config.rules.find(&.name.== describe_rule_name)
raise "Unknown rule"
end
describe_rule(rule)
end
runner = Ameba.run(config)
@ -49,6 +56,7 @@ module Ameba::Cli
property globs : Array(String)?
property only : Array(String)?
property except : Array(String)?
property describe_rule : String?
property location_to_explain : NamedTuple(file: String, line: Int32, column: Int32)?
property fail_level : Severity?
property? skip_reading_config = false
@ -119,6 +127,11 @@ module Ameba::Cli
configure_explain_opts(loc, opts)
end
parser.on("-d", "--describe Category/Rule",
"Describe a rule with specified name") do |rule_name|
configure_describe_opts(rule_name, opts)
end
parser.on("--without-affected-code",
"Stop showing affected code while using a default formatter") do
opts.without_affected_code = true
@ -152,6 +165,11 @@ module Ameba::Cli
opts.without_affected_code?
end
private def configure_describe_opts(rule_name, opts)
opts.describe_rule = rule_name.presence
opts.formatter = :silent
end
private def configure_explain_opts(loc, opts)
location_to_explain = parse_explain_location(loc)
opts.location_to_explain = location_to_explain
@ -183,14 +201,13 @@ module Ameba::Cli
exit 0
end
private def print_rules(config)
config.rules.each do |rule|
puts "%s [%s] - %s" % {
rule.name.colorize(:white),
rule.severity.symbol.to_s.colorize(:green),
rule.description.colorize(:dark_gray),
}
end
private def describe_rule(rule)
Presenter::RulePresenter.new.run(rule)
exit 0
end
private def print_rules(rules)
Presenter::RuleCollectionPresenter.new.run(rules)
exit 0
end
end

View File

@ -55,10 +55,7 @@ class Ameba::Config
Path[XDG_CONFIG_HOME] / "ameba/config.yml",
}
DEFAULT_GLOBS = %w(
**/*.cr
!lib
)
SOURCES_GLOB = "**/*.cr"
getter rules : Array(Rule::Base)
property severity = Severity::Convention
@ -95,7 +92,7 @@ class Ameba::Config
@rules = Rule.rules.map &.new(config).as(Rule::Base)
@rule_groups = @rules.group_by &.group
@excluded = load_array_section(config, "Excluded")
@globs = load_array_section(config, "Globs", DEFAULT_GLOBS)
@globs = load_array_section(config, "Globs", default_globs)
return unless formatter_name = load_formatter_name(config)
self.formatter = formatter_name
@ -115,12 +112,13 @@ class Ameba::Config
end
Config.new YAML.parse(content)
rescue e
raise "Config file is invalid: #{e.message}"
raise "Unable to load config file: #{e.message}"
end
protected def self.read_config(path = nil)
if path
return File.exists?(path) ? File.read(path) : nil
raise ArgumentError.new("Config file does not exist #{path}") unless File.exists?(path)
return File.read(path)
end
each_config_path do |config_path|
return File.read(config_path) if File.exists?(config_path)
@ -238,6 +236,12 @@ class Ameba::Config
end
end
private def default_globs
[SOURCES_GLOB].tap do |globs|
globs.push("!lib") unless Dir["lib/**/*.cr"].empty?
end
end
# :nodoc:
module RuleConfig
# Define rule properties

View File

@ -4,8 +4,6 @@ module Ameba::Formatter
# A formatter that shows the detailed explanation of the issue at
# a specific location.
class ExplainFormatter
HEADING_MARKER = "## "
include Util
getter output : IO::FileDescriptor | IO::Memory
@ -64,9 +62,8 @@ module Ameba::Formatter
rule.name.colorize(:magenta),
rule.severity.to_s.colorize(rule.severity.color),
}
if rule.responds_to?(:description)
output_paragraph rule.description
if rule_description = colorize_code_fences(rule.description)
output_paragraph rule_description
end
rule_doc = colorize_code_fences(rule.class.parsed_doc)
@ -84,7 +81,7 @@ module Ameba::Formatter
end
private def output_title(title)
output << HEADING_MARKER.colorize(:yellow)
output << "### ".colorize(:yellow)
output << title.upcase.colorize(:yellow)
output << "\n\n"
end
@ -95,7 +92,7 @@ module Ameba::Formatter
private def output_paragraph(paragraph : Array)
paragraph.each do |line|
output << ' ' << line << '\n'
output << " " << line << '\n'
end
output << '\n'
end

View File

@ -3,7 +3,7 @@ module Ameba::Formatter
# Basically, it takes all issues reported and disables corresponding rules
# or excludes failed sources from these rules.
class TODOFormatter < DotFormatter
def initialize(@output = STDOUT)
def initialize(@output = STDOUT, @config_path : Path = Config::DEFAULT_PATH)
end
def finished(sources)
@ -26,7 +26,7 @@ module Ameba::Formatter
end
private def generate_todo_config(issues)
file = File.new(Config::DEFAULT_PATH, mode: "w")
file = File.new(@config_path, mode: "w")
file << header
rule_issues_map(issues).each do |rule, rule_issues|
file << "\n# Problems found: #{rule_issues.size}"

View File

@ -1,5 +1,7 @@
module Ameba::Formatter
module Util
extend self
def deansify(message : String?) : String?
message.try &.gsub(/\x1b[^m]*m/, "").presence
end

View File

@ -22,6 +22,7 @@ module Ameba
def expand(globs)
globs.flat_map do |glob|
glob += "/**/*.cr" if File.directory?(glob)
raise ArgumentError.new("No files found matching #{glob}") if Dir[glob].empty?
Dir[glob]
end.uniq!
end

View File

@ -0,0 +1,12 @@
module Ameba::Presenter
private ENABLED_MARK = "".colorize(:green)
private DISABLED_MARK = "x".colorize(:red)
class BasePresenter
# TODO: allow other IOs
getter output : IO::FileDescriptor | IO::Memory
def initialize(@output = STDOUT)
end
end
end

View File

@ -0,0 +1,34 @@
module Ameba::Presenter
class RuleCollectionPresenter < BasePresenter
def run(rules)
rules = rules.to_h do |rule|
name = rule.name.split('/')
name = "%s/%s" % {
name[0...-1].join('/').colorize(:light_gray),
name.last.colorize(:white),
}
{name, rule}
end
longest_name = rules.max_of(&.first.size)
rules.group_by(&.last.group).each do |group, group_rules|
output.puts "— %s" % group.colorize(:light_blue).underline
output.puts
group_rules.each do |name, rule|
output.puts " %s [%s] %s %s" % {
rule.enabled? ? ENABLED_MARK : DISABLED_MARK,
rule.severity.symbol.to_s.colorize(:green),
name.ljust(longest_name),
rule.description.colorize(:dark_gray),
}
end
output.puts
end
output.puts "Total rules: %s / %s enabled" % {
rules.size.to_s.colorize(:light_blue),
rules.count(&.last.enabled?).to_s.colorize(:light_blue),
}
end
end
end

View File

@ -0,0 +1,43 @@
module Ameba::Presenter
class RulePresenter < BasePresenter
def run(rule)
output.puts
output_title "Rule info"
output_paragraph "%s of a %s severity [enabled: %s]" % {
rule.name.colorize(:magenta),
rule.severity.to_s.colorize(rule.severity.color),
rule.enabled? ? ENABLED_MARK : DISABLED_MARK,
}
if rule_description = colorize_code_fences(rule.description)
output_paragraph rule_description
end
if rule_doc = colorize_code_fences(rule.class.parsed_doc)
output_title "Detailed description"
output_paragraph rule_doc
end
end
private def output_title(title)
output.print "### %s\n\n" % title.upcase.colorize(:yellow)
end
private def output_paragraph(paragraph : String)
output_paragraph(paragraph.lines)
end
private def output_paragraph(paragraph : Array)
paragraph.each do |line|
output.puts " #{line}"
end
output.puts
end
private def colorize_code_fences(string)
return unless string
string
.gsub(/```(.+?)```/m, &.colorize(:dark_gray))
.gsub(/`(?!`)(.+?)`/, &.colorize(:dark_gray))
end
end
end

View File

@ -0,0 +1,96 @@
module Ameba::Rule::Lint
# A rule that reports documentation admonitions.
#
# Optionally, these can fail at an appropriate time.
#
# ```
# def get_user(id)
# # TODO(2024-04-24) Fix this hack when the database migration is complete
# if id < 1_000_000
# v1_api_call(id)
# else
# v2_api_call(id)
# end
# end
# ```
#
# `TODO` comments are used to remind yourself of source code related things.
#
# The premise here is that `TODO` should be dealt with in the near future
# and are therefore reported by Ameba.
#
# `FIXME` comments are used to indicate places where source code needs fixing.
#
# The premise here is that `FIXME` should indeed be fixed as soon as possible
# and are therefore reported by Ameba.
#
# YAML configuration example:
#
# ```
# Lint/DocumentationAdmonition:
# Enabled: true
# Admonitions: [TODO, FIXME, BUG]
# Timezone: UTC
# ```
class DocumentationAdmonition < Base
properties do
description "Reports documentation admonitions"
admonitions %w[TODO FIXME BUG]
timezone "UTC"
end
MSG = "Found a %s admonition in a comment"
MSG_LATE = "Found a %s admonition in a comment (%s)"
MSG_ERR = "%s admonition error: %s"
@[YAML::Field(ignore: true)]
private getter location : Time::Location {
Time::Location.load(self.timezone)
}
def test(source)
Tokenizer.new(source).run do |token|
next unless token.type.comment?
next unless doc = token.value.to_s
pattern =
/^#\s*(?<admonition>#{Regex.union(admonitions)})(?:\((?<context>.+?)\))?(?:\W+|$)/m
matches = doc.scan(pattern)
matches.each do |match|
admonition = match["admonition"]
begin
case expr = match["context"]?.presence
when /\A\d{4}-\d{2}-\d{2}\Z/ # date
# ameba:disable Lint/NotNil
date = Time.parse(expr.not_nil!, "%F", location)
issue_for_date source, token, admonition, date
when /\A\d{4}-\d{2}-\d{2} \d{2}:\d{2}(:\d{2})?\Z/ # date + time (no tz)
# ameba:disable Lint/NotNil
date = Time.parse(expr.not_nil!, "%F #{$1?.presence ? "%T" : "%R"}", location)
issue_for_date source, token, admonition, date
else
issue_for token, MSG % admonition
end
rescue ex
issue_for token, MSG_ERR % {admonition, "#{ex}: #{expr.inspect}"}
end
end
end
end
private def issue_for_date(source, node, admonition, date)
diff = Time.utc - date.to_utc
return if diff.negative?
past = case diff
when 0.seconds..1.day then "today is the day!"
when 1.day..2.days then "1 day past"
else "#{diff.total_days.to_i} days past"
end
issue_for node, MSG_LATE % {admonition, past}
end
end
end

View File

@ -32,15 +32,14 @@ module Ameba::Rule::Lint
description "Identifies usage of `not_nil!` calls"
end
NOT_NIL_NAME = "not_nil!"
MSG = "Avoid using `not_nil!`"
MSG = "Avoid using `not_nil!`"
def test(source)
AST::NodeVisitor.new self, source, skip: :macro
end
def test(source, node : Crystal::Call)
return unless node.name == NOT_NIL_NAME
return unless node.name == "not_nil!"
return unless node.obj && node.args.empty?
return unless name_location = node.name_location

View File

@ -30,15 +30,14 @@ module Ameba::Rule::Lint
BLOCK_CALL_NAMES = %w(index rindex find)
CALL_NAMES = %w(index rindex)
NOT_NIL_NAME = "not_nil!"
MSG = "Use `%s! {...}` instead of `%s {...}.not_nil!`"
MSG = "Use `%s! {...}` instead of `%s {...}.not_nil!`"
def test(source)
AST::NodeVisitor.new self, source, skip: :macro
end
def test(source, node : Crystal::Call)
return unless node.name == NOT_NIL_NAME && node.args.empty?
return unless node.name == "not_nil!" && node.args.empty?
return unless (obj = node.obj).is_a?(Crystal::Call)
return unless obj.name.in?(obj.block ? BLOCK_CALL_NAMES : CALL_NAMES)

View File

@ -32,11 +32,10 @@ module Ameba::Rule::Performance
filter_names %w(select reject)
end
ANY_NAME = "any?"
MSG = "Use `any? {...}` instead of `%s {...}.any?`"
MSG = "Use `any? {...}` instead of `%s {...}.any?`"
def test(source, node : Crystal::Call)
return unless node.name == ANY_NAME && (obj = node.obj)
return unless node.name == "any?" && (obj = node.obj)
return unless obj.is_a?(Crystal::Call) && obj.block && node.block.nil?
return unless obj.name.in?(filter_names)

View File

@ -34,11 +34,10 @@ module Ameba::Rule::Performance
description "Identifies usage of arg-less `any?` calls"
end
ANY_NAME = "any?"
MSG = "Use `!{...}.empty?` instead of `{...}.any?`"
MSG = "Use `!{...}.empty?` instead of `{...}.any?`"
def test(source, node : Crystal::Call)
return unless node.name == ANY_NAME
return unless node.name == "any?"
return unless node.block.nil? && node.args.empty?
return unless node.obj

View File

@ -26,18 +26,16 @@ module Ameba::Rule::Performance
description "Identifies usage of `compact` calls that follow `map`"
end
COMPACT_NAME = "compact"
MAP_NAME = "map"
MSG = "Use `compact_map {...}` instead of `map {...}.compact`"
MSG = "Use `compact_map {...}` instead of `map {...}.compact`"
def test(source)
AST::NodeVisitor.new self, source, skip: :macro
end
def test(source, node : Crystal::Call)
return unless node.name == COMPACT_NAME && (obj = node.obj)
return unless node.name == "compact" && (obj = node.obj)
return unless obj.is_a?(Crystal::Call) && obj.block
return unless obj.name == MAP_NAME
return unless obj.name == "map"
issue_for obj.name_location, node.name_end_location, MSG
end

View File

@ -0,0 +1,70 @@
require "./base"
module Ameba::Rule::Performance
# This rule is used to identify excessive collection allocations,
# that can be avoided by using `each_<member>` instead of `<collection>.each`.
#
# For example, this is considered inefficient:
#
# ```
# "Alice".chars.each { |c| puts c }
# "Alice\nBob".lines.each { |l| puts l }
# ```
#
# And can be written as this:
#
# ```
# "Alice".each_char { |c| puts c }
# "Alice\nBob".each_line { |l| puts l }
# ```
#
# YAML configuration example:
#
# ```
# Performance/ExcessiveAllocations:
# Enabled: true
# CallNames:
# codepoints: each_codepoint
# graphemes: each_grapheme
# chars: each_char
# lines: each_line
# ```
class ExcessiveAllocations < Base
include AST::Util
properties do
description "Identifies usage of excessive collection allocations"
call_names({
"codepoints" => "each_codepoint",
"graphemes" => "each_grapheme",
"chars" => "each_char",
"lines" => "each_line",
# "keys" => "each_key",
# "values" => "each_value",
# "children" => "each_child",
})
end
MSG = "Use `%s {...}` instead of `%s.each {...}` to avoid excessive allocation"
def test(source)
AST::NodeVisitor.new self, source, skip: :macro
end
def test(source, node : Crystal::Call)
return unless node.name == "each" && node.args.empty? && node.block
return unless (obj = node.obj).is_a?(Crystal::Call)
return unless obj.args.empty? && obj.block.nil?
return unless method = call_names[obj.name]?
return unless name_location = obj.name_location
return unless end_location = name_end_location(node)
msg = MSG % {method, obj.name}
issue_for name_location, end_location, msg do |corrector|
corrector.replace(name_location, end_location, method)
end
end
end
end

View File

@ -26,18 +26,16 @@ module Ameba::Rule::Performance
description "Identifies usage of `flatten` calls that follow `map`"
end
FLATTEN_NAME = "flatten"
MAP_NAME = "map"
MSG = "Use `flat_map {...}` instead of `map {...}.flatten`"
MSG = "Use `flat_map {...}` instead of `map {...}.flatten`"
def test(source)
AST::NodeVisitor.new self, source, skip: :macro
end
def test(source, node : Crystal::Call)
return unless node.name == FLATTEN_NAME && (obj = node.obj)
return unless node.name == "flatten" && (obj = node.obj)
return unless obj.is_a?(Crystal::Call) && obj.block
return unless obj.name == MAP_NAME
return unless obj.name == "map"
issue_for obj.name_location, node.name_end_location, MSG
end

View File

@ -0,0 +1,67 @@
require "./base"
module Ameba::Rule::Performance
# This rule is used to identify usage of `min/max/minmax` calls that follow `map`.
#
# For example, this is considered invalid:
#
# ```
# %w[Alice Bob].map(&.size).min
# %w[Alice Bob].map(&.size).max
# %w[Alice Bob].map(&.size).minmax
# ```
#
# And it should be written as this:
#
# ```
# %w[Alice Bob].min_of(&.size)
# %w[Alice Bob].max_of(&.size)
# %w[Alice Bob].minmax_of(&.size)
# ```
#
# YAML configuration example:
#
# ```
# Performance/MinMaxAfterMap:
# Enabled: true
# ```
class MinMaxAfterMap < Base
properties do
description "Identifies usage of `min/max/minmax` calls that follow `map`"
end
MSG = "Use `%s {...}` instead of `map {...}.%s`."
CALL_NAMES = %w[min min? max max? minmax minmax?]
def test(source)
AST::NodeVisitor.new self, source, skip: :macro
end
def test(source, node : Crystal::Call)
return unless node.name.in?(CALL_NAMES) && node.block.nil? && node.args.empty?
return unless (obj = node.obj) && obj.is_a?(Crystal::Call)
return unless obj.name == "map" && obj.block && obj.args.empty?
return unless name_location = obj.name_location
return unless end_location = node.name_end_location
of_name = node.name.sub(/(.+?)(\?)?$/, "\\1_of\\2")
message = MSG % {of_name, node.name}
issue_for name_location, end_location, message do |corrector|
next unless node_name_location = node.name_location
# TODO: switching the order of the below calls breaks the corrector
corrector.replace(
name_location,
name_location.adjust(column_number: {{ "map".size - 1 }}),
of_name
)
corrector.remove(
node_name_location.adjust(column_number: -1),
end_location
)
end
end
end
end

View File

@ -38,15 +38,14 @@ module Ameba::Rule::Performance
filter_names %w(select reject)
end
SIZE_NAME = "size"
MSG = "Use `count {...}` instead of `%s {...}.size`."
MSG = "Use `count {...}` instead of `%s {...}.size`."
def test(source)
AST::NodeVisitor.new self, source, skip: :macro
end
def test(source, node : Crystal::Call)
return unless node.name == SIZE_NAME && (obj = node.obj)
return unless node.name == "size" && (obj = node.obj)
return unless obj.is_a?(Crystal::Call) && obj.block
return unless obj.name.in?(filter_names)

View File

@ -99,6 +99,9 @@ module Ameba::Rule::Style
node.named_args.try &.each do |arg|
i += reference_count(arg.value, obj)
end
when Crystal::BinaryOp
i += reference_count(node.left, obj)
i += reference_count(node.right, obj)
when Crystal::Block
i += reference_count(node.body, obj)
when Crystal::Var
@ -230,7 +233,7 @@ module Ameba::Rule::Style
# we filter out the blocks that are of call type - `i.to_i64.odd?`
return unless (body = block.body).is_a?(Crystal::Call)
# we need to "unwind" the chain challs, so the final receiver object
# we need to "unwind" the chain calls, so the final receiver object
# ends up being a variable - `i`
obj = body.obj
while obj.is_a?(Crystal::Call)

View File

@ -109,7 +109,7 @@ module Ameba::Spec::ExpectIssue
code = lines.join('\n')
if code == annotated_code
raise "Use `report_no_issues` to assert that no issues are found"
raise "Use `expect_no_issues` to assert that no issues are found"
end
source, actual_annotations = actual_annotations(rules, code, path, lines)