Compare commits

...

149 Commits

Author SHA1 Message Date
Sijawusz Pur Rahnama 63be60ce96
Merge pull request #452 from crystal-ameba/reopen-issue-447 2024-01-22 19:30:21 +01:00
Sijawusz Pur Rahnama 17084f4a1d Remove naive solution for #447 2024-01-22 17:15:52 +01:00
Sijawusz Pur Rahnama 590640b559
Merge pull request #451 from crystal-ameba/tweak-useless-assign
Refactor `Lint/UselessAssign` rule a bit
2024-01-20 16:15:56 +01:00
Sijawusz Pur Rahnama 3bea264948 Refactor `Lint/UselessAssign` rule a bit 2024-01-19 20:11:56 +01:00
Sijawusz Pur Rahnama 7f50ff90fd
Merge pull request #450 from crystal-ameba/fix-issue-447
Exclude reporting type declarations passed as call arguments
2024-01-18 07:52:48 +01:00
Sijawusz Pur Rahnama a79e711fae Exclude reporting type declarations passed as call arguments 2024-01-18 00:34:28 +01:00
Sijawusz Pur Rahnama 28fafea19f
Merge pull request #449 from crystal-ameba/fix-issue-446
Do not report type declarations within generic records
2024-01-16 09:20:20 +01:00
Sijawusz Pur Rahnama f2677d68f6 Do not report type declarations within generic records 2024-01-16 02:04:41 +01:00
Vitalii Elenhaupt b56d34715d
Merge pull request #445 from straight-shoota/infra/makefile
Enhance `Makefile`
2024-01-15 19:07:14 +02:00
Johannes Müller 1398c0ee8f
Enhance `Makefile`
* `install` recipe does not rebuild binary
* Add `help` target and documentation
* Add several config variables
* Add sources as dependencies for (in-)validation

Based on template
https://gist.github.com/straight-shoota/275685fcb8187062208c0871318c4a23
2024-01-14 13:29:29 +01:00
Sijawusz Pur Rahnama 734bb2a7f1
Merge pull request #443 from crystal-ameba/fix-issue-442
Do not report type declarations within `lib` definitions
2024-01-10 09:12:37 +01:00
Sijawusz Pur Rahnama 98d5bc720a Skip lib definitions altogether 2024-01-10 01:14:03 +01:00
Sijawusz Pur Rahnama d23ad7f0ab Make `Scope#*_def?` methods accept `check_outer_scopes` parameter 2024-01-10 01:10:36 +01:00
Sijawusz Pur Rahnama b6bd74e02f
Merge pull request #434 from crystal-ameba/misc-refactors
v1.6.1
2024-01-09 21:12:19 +01:00
Sijawusz Pur Rahnama ce3f2b7e4b Add QoL `Variable#reference(scope)` method 2024-01-01 14:49:27 +01:00
Sijawusz Pur Rahnama 444b07c179 Bump version to 1.6.1 2024-01-01 14:46:32 +01:00
Sijawusz Pur Rahnama e99a69765f Few refactors 2024-01-01 14:46:32 +01:00
Sijawusz Pur Rahnama 6d0b12c70f Cleanup docs 2024-01-01 14:46:32 +01:00
Sijawusz Pur Rahnama 65ab317a3b Merge `delegate` calls 2024-01-01 14:46:32 +01:00
Sijawusz Pur Rahnama 452a7a867e
Merge pull request #430 from crystal-ameba/fix-issue-429
Report unused type declarations in `Lint/UselessAssign` rule
2024-01-01 12:22:01 +01:00
Sijawusz Pur Rahnama 5a24f1eba5 Add `UselessAssign#exclude_type_declarations` 2023-12-29 01:50:19 +01:00
Sijawusz Pur Rahnama aeffa6ad00 Add test spec covering accessor macros 2023-12-28 15:46:32 +01:00
Sijawusz Pur Rahnama 4567293add Drop `type_definition?` check from `Scope#top_level?` 2023-12-28 15:43:29 +01:00
Sijawusz Pur Rahnama a49faa33a9 Refactor `Lint/UselessAssign` spec 2023-12-28 15:43:29 +01:00
Sijawusz Pur Rahnama 1dd531740c Fix reported ameba issue 2023-12-28 15:43:29 +01:00
Sijawusz Pur Rahnama 1b661d633d Refactor `ScopeVisitor` to ignore accessor macros 2023-12-28 15:43:29 +01:00
Sijawusz Pur Rahnama 9745637cf9 Update `UselessAssign` rule to report unreferenced type declarations 2023-12-28 15:43:29 +01:00
Sijawusz Pur Rahnama 4ad151e5e0 Do not automatically consider type definitions as referenced 2023-12-28 15:43:29 +01:00
Sijawusz Pur Rahnama c9bc01f88c
Merge pull request #439 from crystal-ameba/fix-issue-353
Make `Lint/SharedVarInFiber` rule account for `loop { ... }`
2023-12-28 15:31:43 +01:00
Sijawusz Pur Rahnama 1feb5c279b Add test spec covering the `loop { … }` block to `Lint/SharedVarInFiber` rule specs 2023-12-28 15:31:11 +01:00
Sijawusz Pur Rahnama 57898fd797 Make `BranchVisitor` treat `loop { … }` calls as branchable 2023-12-28 15:31:11 +01:00
Sijawusz Pur Rahnama 46a42ee9e8
Merge pull request #438 from crystal-ameba/add-error-as-allowed-variable-name
Add `error` to the `RescuedExceptionsVariableName#allowed_names`
2023-12-28 14:32:45 +01:00
Sijawusz Pur Rahnama 61afa5bb2b Add `error` to the `RescuedExceptionsVariableName#allowed_names` 2023-12-28 13:07:14 +01:00
Sijawusz Pur Rahnama 9bb6c9ac75
Merge pull request #436 from crystal-ameba/cleanup-properties-macro
Make `RuleConfig#properties` accept only `Call` nodes
2023-12-28 09:24:09 +01:00
Sijawusz Pur Rahnama 954345d316
Merge pull request #435 from crystal-ameba/revert-pr-394
Revert "Merge pull request #394 from stufro/388-raise-on-invalid-file…
2023-12-28 09:20:27 +01:00
Sijawusz Pur Rahnama 55f3ec53b7 Make `RuleConfig#properties` accept only `Call` nodes
Add optional named argument `as`, in order to specify the property type
2023-12-28 04:48:18 +01:00
Sijawusz Pur Rahnama 26d9bc0bd0 Revert "Merge pull request #394 from stufro/388-raise-on-invalid-file-path"
This reverts commit 18d193bd08, reversing
changes made to 7b8316f061.
2023-12-28 02:03:44 +01:00
Sijawusz Pur Rahnama 47088b10ca Add some more excluded operators to `BinaryOperatorParameterName` rule 2023-11-24 21:13:04 +01:00
Sijawusz Pur Rahnama 9f9d5fae32
Merge pull request #428 from crystal-ameba/revert-incorrect-excessive-allocations-condition
Revert "Fix `Performance/ExcessiveAllocations` to exclude `each` call…
2023-11-18 09:50:45 +01:00
Sijawusz Pur Rahnama 5e70ae4f8c
Merge pull request #427 from crystal-ameba/fix-gha-ci-badge-in-readme
Fix GitHub Actions CI badge in `README.md`
2023-11-18 06:34:34 +01:00
Sijawusz Pur Rahnama 82e0e53080 Revert "Fix `Performance/ExcessiveAllocations` to exclude `each` calls without a block"
This reverts commit 29e29b8e1d.
2023-11-17 19:48:41 +01:00
Sijawusz Pur Rahnama 1b8523def6
Fix GitHub Actions CI badge in `README.md` 2023-11-17 19:23:06 +01:00
Sijawusz Pur Rahnama a88033c8ce
Merge pull request #426 from crystal-ameba/fix-issue-409
Do not report expanded arguments in `ShadowingOuterLocalVar` rule
2023-11-17 19:00:34 +01:00
Sijawusz Pur Rahnama 30e3816ed1 Do not report expanded arguments in `ShadowingOuterLocalVar` rule 2023-11-17 18:36:19 +01:00
Sijawusz Pur Rahnama 5aac63ea74
Merge pull request #425 from crystal-ameba/prepare-release-1.6.0
Prepare release 1.6.0
2023-11-17 18:14:20 +01:00
Sijawusz Pur Rahnama 10b577d23a Use square brackets for `%w` and `%i` array literals 2023-11-17 17:34:39 +01:00
Sijawusz Pur Rahnama 06dc201344 Bump supported crystal version to `1.10` 2023-11-17 16:22:31 +01:00
Sijawusz Pur Rahnama d079f4bae6 Bump version to 1.6.0 2023-11-14 12:34:44 +01:00
Sijawusz Pur Rahnama 0461fff702 Relax `crystal` version in `shard.yml` 2023-11-14 12:34:44 +01:00
Sijawusz Pur Rahnama 22e2d1de00 Cleanup my involvement status 2023-11-14 12:34:44 +01:00
Sijawusz Pur Rahnama 810a3440dd Remove shard `version` specifier from `README.md` 2023-11-14 12:34:44 +01:00
Sijawusz Pur Rahnama f3f1f3a2ab Misc cleanups/refactors 2023-11-14 12:34:44 +01:00
Sijawusz Pur Rahnama 547fec5a94 Refactor `TypeNames` to report the name itself only 2023-11-14 11:22:17 +01:00
Sijawusz Pur Rahnama a8b8c35cc7 Fix usage of deprecated methods 2023-11-14 11:22:17 +01:00
Sijawusz Pur Rahnama 11bf9ffcdc Remove unused `include AST::Util` 2023-11-14 11:22:17 +01:00
Sijawusz Pur Rahnama 52ccf23ef9 Remove deprecated `Assignment#transformed?` method 2023-11-14 11:22:17 +01:00
Sijawusz Pur Rahnama b3f11913ed Reports also methods not ending with `?` suffix in `PredicateName` rule 2023-11-14 11:22:17 +01:00
Sijawusz Pur Rahnama 633ed7538e Use `prefer_name_location: true` in `PredicateName` rule 2023-11-14 11:22:17 +01:00
Sijawusz Pur Rahnama 15d241e138 Add spec for `AST::Util#{static,dynamic}_literal?` 2023-11-14 11:22:17 +01:00
Sijawusz Pur Rahnama 52a3e47a3b
Merge pull request #423 from crystal-ameba/lint-not-nil-after-no-bang-reports-match-calls
Make `Lint/NotNilAfterNoBang` report calls to `#match`
2023-11-14 10:25:44 +01:00
Sijawusz Pur Rahnama 3b87aa6490
Merge pull request #424 from crystal-ameba/report-string-literals-in-ascii-identifiers-rule
Report symbol literals in `Naming/AsciiIdentifiers` rule
2023-11-14 10:24:29 +01:00
Sijawusz Pur Rahnama 018adb54be Add `AsciiIdentifiers#ignore_symbols` property 2023-11-14 05:22:29 +01:00
Sijawusz Pur Rahnama be76b3682a Report string literals in `AsciiIdentifiers` rule 2023-11-14 05:15:38 +01:00
Sijawusz Pur Rahnama 775650c882 Add `MultiAssign` to `NodeVisitor::NODES` 2023-11-14 05:00:49 +01:00
Sijawusz Pur Rahnama 21a406e56d Make `Lint/NotNilAfterNoBang` report calls to `#match` 2023-11-14 03:50:58 +01:00
Sijawusz Pur Rahnama 0b225da9ba
Merge pull request #422 from crystal-ameba/refactor-adding-issues-with-name-location
Make it easier to add issues for nodes with name location preference
2023-11-13 19:16:14 +01:00
Sijawusz Pur Rahnama 0a2609c1b4 Add `ip` to the list of `BlockParameterName#allowed_names` 2023-11-12 11:59:06 +01:00
Sijawusz Pur Rahnama 06952fc7d3 Add `op` to the list of `BlockParameterName#allowed_names` 2023-11-12 11:36:03 +01:00
Sijawusz Pur Rahnama f984d83b05 Use `name(_end)_location` helpers consistently 2023-11-12 10:24:12 +01:00
Sijawusz Pur Rahnama 98cc6fd612 Make it easier to add issues for nodes with name location preference 2023-11-12 10:23:36 +01:00
Sijawusz Pur Rahnama 6caf24ad6d
Merge pull request #421 from crystal-ameba/add-binary-operator-parameter-name-rule
Add `Naming/BinaryOperatorParameterName` rule
2023-11-12 09:56:26 +01:00
Sijawusz Pur Rahnama e62fffae80
Merge pull request #419 from crystal-ameba/add-block-parameter-name-rule
Add `Naming/BlockParameterName` rule
2023-11-12 09:55:50 +01:00
Sijawusz Pur Rahnama 61ccb030bd Fix newly found offenses 2023-11-11 19:08:14 +01:00
Sijawusz Pur Rahnama 971bff6c27 Add `Naming/BlockParameterName` rule 2023-11-11 19:07:46 +01:00
Sijawusz Pur Rahnama bf4219532f Add `Naming/BinaryOperatorParameterName` rule 2023-11-11 18:48:26 +01:00
Sijawusz Pur Rahnama a40f02f77f
Merge pull request #420 from crystal-ameba/add-spec-filename-rule
Add `Lint/SpecFilename` rule
2023-11-11 08:26:33 +01:00
Sijawusz Pur Rahnama bee4472a26
Merge pull request #418 from crystal-ameba/add-rescued-exceptions-variable-name-rule
Add `Naming/RescuedExceptionsVariableName` rule
2023-11-10 15:56:34 +01:00
Sijawusz Pur Rahnama 28014ada67 Add `Lint/SpecFilename` rule 2023-11-10 15:41:54 +01:00
Sijawusz Pur Rahnama 1d76a7c71a Add `Naming/RescuedExceptionsVariableName` rule 2023-11-10 13:26:31 +01:00
Sijawusz Pur Rahnama 0abb73f0b6
Merge pull request #414 from crystal-ameba/add-ascii-identifiers-rule
Add `Naming/AsciiIdentifiers` rule
2023-11-10 01:59:04 +01:00
Sijawusz Pur Rahnama fd44eeba08 Add `Naming/AsciiIdentifiers` rule 2023-11-10 01:55:19 +01:00
Sijawusz Pur Rahnama cc23e7a7e7
Merge pull request #415 from crystal-ameba/add-accessor-method-name-rule
Add `Naming/AccessorMethodName` rule
2023-11-09 10:34:37 +01:00
Sijawusz Pur Rahnama 964d011d53 Add `Naming/AccessorMethodName` rule 2023-11-09 10:30:59 +01:00
Sijawusz Pur Rahnama 3f1e925e07
Merge pull request #417 from crystal-ameba/fix-issue-400
Fix false positive with dynamic literals in `Lint/LiteralsComparison`
2023-11-09 09:09:54 +01:00
Sijawusz Pur Rahnama e84cc05f0f Fix false positive with dynamic literals in `Lint/LiteralsComparison` 2023-11-09 08:20:35 +01:00
Sijawusz Pur Rahnama 7ceb3ffad9
Merge pull request #416 from crystal-ameba/add-filename-rule
Add `Naming/Filename` rule
2023-11-09 07:58:44 +01:00
Sijawusz Pur Rahnama b9ce705a47 Add `Naming/Filename` rule 2023-11-09 07:07:45 +01:00
Sijawusz Pur Rahnama 881209d54e
Merge pull request #412 from crystal-ameba/group-documentation-rules
Move documentation-related rules into its own group
2023-11-09 06:19:36 +01:00
Sijawusz Pur Rahnama bcb72fb3c3
Merge pull request #413 from crystal-ameba/group-naming-rules
Move naming-related rules into its own group
2023-11-09 06:18:48 +01:00
Sijawusz Pur Rahnama b25dc402c8 Group naming-related rules 2023-11-09 00:16:29 +01:00
Sijawusz Pur Rahnama 8569355b5a Move documentation-related rules into its own group 2023-11-08 18:35:32 +01:00
Sijawusz Pur Rahnama 0c6745781e
Merge pull request #381 from crystal-ameba/add-typos-rule
Add `Lint/Typos` rule
2023-11-08 13:01:52 +01:00
Sijawusz Pur Rahnama 891cad2610 Install `typos-cli` on macOS CI 2023-11-08 02:24:35 +01:00
Sijawusz Pur Rahnama 0140fd3573 Add `Lint/Typos` rule 2023-11-08 02:24:35 +01:00
Sijawusz Pur Rahnama 9f6615bdfd
Merge pull request #380 from crystal-ameba/add-documentation-admonition-rule
Add `Lint/DocumentationAdmonition` rule
2023-11-06 17:03:50 +01:00
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 db59b23f9b Fix specs against Crystal nightly 2023-06-10 01:11:21 +02:00
131 changed files with 2631 additions and 857 deletions

7
.ameba.yml Normal file
View File

@ -0,0 +1,7 @@
Documentation/DocumentationAdmonition:
Timezone: UTC
Admonitions: [FIXME, BUG]
Lint/Typos:
Excluded:
- spec/ameba/rule/lint/typos_spec.cr

View File

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

View File

@ -18,17 +18,24 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- name: Set timezone to UTC
uses: szenius/set-timezone@v1.2
- name: Install Crystal - name: Install Crystal
uses: crystal-lang/install-crystal@v1 uses: crystal-lang/install-crystal@v1
with: with:
crystal: ${{ matrix.crystal }} crystal: ${{ matrix.crystal }}
- name: Download source - name: Download source
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Install dependencies - name: Install dependencies
run: shards install run: shards install
- name: Install typos-cli
if: matrix.os == 'macos-latest'
run: brew install typos-cli
- name: Run specs - name: Run specs
run: crystal spec run: crystal spec

View File

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

View File

@ -1,38 +1,92 @@
.POSIX:
all:
# Recipes
## Build ameba
## $ make
## Run tests
## $ make test
## Install ameba
## $ sudo make install
-include Makefile.local # for optional local options
BUILD_TARGET ::= bin/ameba
DESTDIR ?= ## Install destination dir
PREFIX ?= /usr/local## Install path prefix
BINDIR ?= $(DESTDIR)$(PREFIX)/bin
# The crystal command to use
CRYSTAL_BIN ?= crystal CRYSTAL_BIN ?= crystal
# The shards command to use
SHARDS_BIN ?= shards SHARDS_BIN ?= shards
PREFIX ?= /usr/local # The install command to use
INSTALL_BIN ?= /usr/bin/install
SHARD_BIN ?= ../../bin SHARD_BIN ?= ../../bin
CRFLAGS ?= -Dpreview_mt CRFLAGS ?= -Dpreview_mt
SRC_SOURCES ::= $(shell find src -name '*.cr' 2>/dev/null)
DOC_SOURCE ::= src/**
.PHONY: all
all: build
.PHONY: build .PHONY: build
build: build: ## Build the application binary
build: $(BUILD_TARGET)
$(BUILD_TARGET): $(SRC_SOURCES)
$(SHARDS_BIN) build $(CRFLAGS) $(SHARDS_BIN) build $(CRFLAGS)
docs: ## Generate API docs
docs: $(SRC_SOURCES)
$(CRYSTAL_BIN) docs -o docs $(DOC_SOURCE)
.PHONY: lint .PHONY: lint
lint: build lint: ## Run ameba on ameba's code base
./bin/ameba lint: $(BUILD_TARGET)
$(BUILD_TARGET)
.PHONY: spec .PHONY: spec
spec: ## Run the spec suite
spec: spec:
$(CRYSTAL_BIN) spec $(CRYSTAL_BIN) spec
.PHONY: clean .PHONY: clean
clean: ## Remove application binary
clean: clean:
rm -f ./bin/ameba ./bin/ameba.dwarf @rm -f "$(BUILD_TARGET)" "$(BUILD_TARGET).dwarf"
.PHONY: install .PHONY: install
install: build install: ## Install application binary into $DESTDIR
mkdir -p $(PREFIX)/bin install: $(BUILD_TARGET)
cp ./bin/ameba $(PREFIX)/bin $(INSTALL_BIN) -m 0755 "$(BUILD_TARGET)" "$(BINDIR)/ameba"
.PHONY: bin .PHONY: bin
bin: build bin: build
mkdir -p $(SHARD_BIN) mkdir -p $(SHARD_BIN)
cp ./bin/ameba $(SHARD_BIN) cp $(BUILD_TARGET) $(SHARD_BIN)
.PHONY: run_file
run_file:
cp -n ./bin/ameba.cr $(SHARD_BIN) || true
.PHONY: test .PHONY: test
test: ## Run the spec suite and linter
test: spec lint test: spec lint
.PHONY: help
help: ## Show this help
@echo
@printf '\033[34mtargets:\033[0m\n'
@grep -hE '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) |\
sort |\
awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}'
@echo
@printf '\033[34moptional variables:\033[0m\n'
@grep -hE '^[a-zA-Z_-]+ \?=.*?## .*$$' $(MAKEFILE_LIST) |\
sort |\
awk 'BEGIN {FS = " \\?=.*?## "}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}'
@echo
@printf '\033[34mrecipes:\033[0m\n'
@grep -hE '^##.*$$' $(MAKEFILE_LIST) |\
awk 'BEGIN {FS = "## "}; /^## [a-zA-Z_-]/ {printf " \033[36m%s\033[0m\n", $$2}; /^## / {printf " %s\n", $$2}'

View File

@ -8,7 +8,7 @@
</sup> </sup>
</p> </p>
<p align="center"> <p align="center">
<a href="https://github.com/crystal-ameba/ameba/actions?query=workflow%3ACI"><img src="https://github.com/crystal-ameba/ameba/workflows/CI/badge.svg"></a> <a href="https://github.com/crystal-ameba/ameba/actions/workflows/ci.yml"><img src="https://github.com/crystal-ameba/ameba/actions/workflows/ci.yml/badge.svg"></a>
<a href="https://github.com/crystal-ameba/ameba/releases"><img src="https://img.shields.io/github/release/crystal-ameba/ameba.svg?maxAge=360"></a> <a href="https://github.com/crystal-ameba/ameba/releases"><img src="https://img.shields.io/github/release/crystal-ameba/ameba.svg?maxAge=360"></a>
<a href="https://github.com/crystal-ameba/ameba/blob/master/LICENSE"><img src="https://img.shields.io/github/license/crystal-ameba/ameba.svg"></a> <a href="https://github.com/crystal-ameba/ameba/blob/master/LICENSE"><img src="https://img.shields.io/github/license/crystal-ameba/ameba.svg"></a>
</p> </p>
@ -118,7 +118,6 @@ Add this to your application's `shard.yml`:
development_dependencies: development_dependencies:
ameba: ameba:
github: crystal-ameba/ameba github: crystal-ameba/ameba
version: ~> 1.4.0
``` ```
Build `bin/ameba` binary within your project directory while running `shards install`. Build `bin/ameba` binary within your project directory while running `shards install`.
@ -165,7 +164,7 @@ Generate new file by running `ameba --gen-config`.
**List of sources to run Ameba on can be configured globally via:** **List of sources to run Ameba on can be configured globally via:**
- `Globs` section - an array of wildcards (or paths) to include to the - `Globs` section - an array of wildcards (or paths) to include to the
inspection. Defaults to `%w(**/*.cr !lib)`, meaning it includes all project inspection. Defaults to `%w[**/*.cr !lib]`, meaning it includes all project
files with `*.cr` extension except those which exist in `lib` folder. files with `*.cr` extension except those which exist in `lib` folder.
- `Excluded` section - an array of wildcards (or paths) to exclude from the - `Excluded` section - an array of wildcards (or paths) to exclude from the
source list defined by `Globs`. Defaults to an empty array. source list defined by `Globs`. Defaults to an empty array.
@ -186,8 +185,8 @@ Excluded:
``` yaml ``` yaml
Style/RedundantBegin: Style/RedundantBegin:
Excluded: Excluded:
- src/server/processor.cr - src/server/processor.cr
- src/server/api.cr - src/server/api.cr
``` ```
### Rules ### Rules
@ -240,4 +239,4 @@ time = Time.epoch(1483859302) # ameba:disable Style, Lint
## Contributors ## Contributors
- [veelenga](https://github.com/veelenga) Vitalii Elenhaupt - creator, maintainer - [veelenga](https://github.com/veelenga) Vitalii Elenhaupt - creator, maintainer
- [Sija](https://github.com/Sija) Sijawusz Pur Rahnama - maintainer - [Sija](https://github.com/Sija) Sijawusz Pur Rahnama - contributor, maintainer

View File

@ -15,7 +15,7 @@ Benchmark.ips do |x|
20, 20,
30, 30,
40, 40,
].each do |n| ].each do |n| # ameba:disable Naming/BlockParameterName
config = Ameba::Config.load config = Ameba::Config.load
config.formatter = Ameba::Formatter::BaseFormatter.new config.formatter = Ameba::Formatter::BaseFormatter.new
config.globs = get_files(n) config.globs = get_files(n)

View File

@ -1,20 +1,22 @@
name: ameba name: ameba
version: 1.4.3 version: 1.6.1
authors: authors:
- Vitalii Elenhaupt <velenhaupt@gmail.com> - Vitalii Elenhaupt <velenhaupt@gmail.com>
- Sijawusz Pur Rahnama <sija@sija.pl>
targets: targets:
ameba: ameba:
main: src/cli.cr main: src/cli.cr
scripts: scripts:
# TODO: remove pre-compiled executable in future releases postinstall: shards build -Dpreview_mt
postinstall: make bin && make run_file
# TODO: remove pre-compiled executable in future releases
executables: executables:
- ameba - ameba
- ameba.cr
crystal: "~> 1.7.0" crystal: ~> 1.10
license: MIT license: MIT

View File

@ -298,6 +298,34 @@ module Ameba::AST
end end
end end
context "Crystal::Call" do
context "loop" do
it "constructs a branch in block" do
branch = branch_of_assign_in_def <<-CRYSTAL
def method(a)
loop do
b = (a = 1)
end
end
CRYSTAL
branch.to_s.should eq "b = (a = 1)"
end
end
context "other" do
it "skips constructing a branch in block" do
branch = branch_of_assign_in_def <<-CRYSTAL
def method(a)
1.upto(10) do
b = (a = 1)
end
end
CRYSTAL
branch.should be_nil
end
end
end
describe "#initialize" do describe "#initialize" do
it "creates new branch" do it "creates new branch" do
nodes = as_nodes <<-CRYSTAL nodes = as_nodes <<-CRYSTAL
@ -358,6 +386,30 @@ module Ameba::AST
branch = Branch.new nodes.assign_nodes.first, branchable branch = Branch.new nodes.assign_nodes.first, branchable
branch.in_loop?.should be_false branch.in_loop?.should be_false
end end
context "Crystal::Call" do
it "returns true if branch is in a loop" do
nodes = as_nodes <<-CRYSTAL
loop do
a = 1
end
CRYSTAL
branchable = Branchable.new nodes.call_nodes.first
branch = Branch.new nodes.assign_nodes.first, branchable
branch.in_loop?.should be_true
end
it "returns false if branch is not in a loop" do
nodes = as_nodes <<-CRYSTAL
1.upto(10) do
a = 1
end
CRYSTAL
branchable = Branchable.new nodes.call_nodes.first
branch = Branch.new nodes.assign_nodes.first, branchable
branch.in_loop?.should be_false
end
end
end end
end end
end end

View File

@ -57,13 +57,15 @@ module Ameba::AST
end end
end end
CRYSTAL CRYSTAL
scope = Scope.new nodes.def_nodes.first
var_node = nodes.var_nodes.first var_node = nodes.var_nodes.first
scope.add_variable var_node
scope = Scope.new nodes.def_nodes.first
scope.add_variable(var_node)
scope.inner_scopes << Scope.new(nodes.block_nodes.first, scope) scope.inner_scopes << Scope.new(nodes.block_nodes.first, scope)
variable = Variable.new(var_node, scope) variable = Variable.new(var_node, scope)
variable.reference nodes.var_nodes.first, scope.inner_scopes.first variable.reference(nodes.var_nodes.first, scope.inner_scopes.first)
scope.references?(variable).should be_true scope.references?(variable).should be_true
end end
@ -77,13 +79,15 @@ module Ameba::AST
end end
end end
CRYSTAL CRYSTAL
scope = Scope.new nodes.def_nodes.first
var_node = nodes.var_nodes.first var_node = nodes.var_nodes.first
scope.add_variable var_node
scope = Scope.new nodes.def_nodes.first
scope.add_variable(var_node)
scope.inner_scopes << Scope.new(nodes.block_nodes.first, scope) scope.inner_scopes << Scope.new(nodes.block_nodes.first, scope)
variable = Variable.new(var_node, scope) variable = Variable.new(var_node, scope)
variable.reference nodes.var_nodes.first, scope.inner_scopes.first variable.reference(nodes.var_nodes.first, scope.inner_scopes.first)
scope.references?(variable, check_inner_scopes: false).should be_false scope.references?(variable, check_inner_scopes: false).should be_false
end end
@ -98,9 +102,11 @@ module Ameba::AST
end end
end end
CRYSTAL CRYSTAL
scope = Scope.new nodes.def_nodes.first
var_node = nodes.var_nodes.first var_node = nodes.var_nodes.first
scope.add_variable var_node
scope = Scope.new nodes.def_nodes.first
scope.add_variable(var_node)
scope.inner_scopes << Scope.new(nodes.block_nodes.first, scope) scope.inner_scopes << Scope.new(nodes.block_nodes.first, scope)
variable = Variable.new(var_node, scope) variable = Variable.new(var_node, scope)
@ -120,7 +126,7 @@ module Ameba::AST
describe "#find_variable" do describe "#find_variable" do
it "returns the variable in the scope by name" do it "returns the variable in the scope by name" do
scope = Scope.new as_node("foo = 1") scope = Scope.new as_node("foo = 1")
scope.add_variable Crystal::Var.new "foo" scope.add_variable(Crystal::Var.new "foo")
scope.find_variable("foo").should_not be_nil scope.find_variable("foo").should_not be_nil
end end
@ -133,7 +139,7 @@ module Ameba::AST
describe "#assign_variable" do describe "#assign_variable" do
it "creates a new assignment" do it "creates a new assignment" do
scope = Scope.new as_node("foo = 1") scope = Scope.new as_node("foo = 1")
scope.add_variable Crystal::Var.new "foo" scope.add_variable(Crystal::Var.new "foo")
scope.assign_variable("foo", Crystal::Var.new "foo") scope.assign_variable("foo", Crystal::Var.new "foo")
var = scope.find_variable("foo").should_not be_nil var = scope.find_variable("foo").should_not be_nil
var.assignments.size.should eq 1 var.assignments.size.should eq 1
@ -141,7 +147,7 @@ module Ameba::AST
it "does not create the assignment if variable is wrong" do it "does not create the assignment if variable is wrong" do
scope = Scope.new as_node("foo = 1") scope = Scope.new as_node("foo = 1")
scope.add_variable Crystal::Var.new "foo" scope.add_variable(Crystal::Var.new "foo")
scope.assign_variable("bar", Crystal::Var.new "bar") scope.assign_variable("bar", Crystal::Var.new "bar")
var = scope.find_variable("foo").should_not be_nil var = scope.find_variable("foo").should_not be_nil
var.assignments.size.should eq 0 var.assignments.size.should eq 0
@ -174,6 +180,28 @@ module Ameba::AST
end end
end end
describe "#def?" do
context "when check_outer_scopes: true" do
it "returns true if outer scope is Crystal::Def" do
nodes = as_nodes("def foo; 3.times {}; end")
outer_scope = Scope.new nodes.def_nodes.first
scope = Scope.new nodes.block_nodes.first, outer_scope
scope.def?(check_outer_scopes: true).should be_true
end
end
it "returns true if Crystal::Def" do
nodes = as_nodes("def foo; end")
scope = Scope.new nodes.def_nodes.first
scope.def?.should be_true
end
it "returns false otherwise" do
scope = Scope.new as_node("a = 1")
scope.def?.should be_false
end
end
describe "#in_macro?" do describe "#in_macro?" do
it "returns true if Crystal::Macro" do it "returns true if Crystal::Macro" do
nodes = as_nodes <<-CRYSTAL nodes = as_nodes <<-CRYSTAL

View File

@ -36,6 +36,43 @@ module Ameba::AST
end end
end end
describe "#static/dynamic_literal?" do
[
Crystal::ArrayLiteral.new,
Crystal::ArrayLiteral.new([Crystal::StringLiteral.new("foo")] of Crystal::ASTNode),
Crystal::BoolLiteral.new(false),
Crystal::CharLiteral.new('a'),
Crystal::HashLiteral.new,
Crystal::NamedTupleLiteral.new,
Crystal::NilLiteral.new,
Crystal::NumberLiteral.new(42),
Crystal::RegexLiteral.new(Crystal::StringLiteral.new("")),
Crystal::StringLiteral.new("foo"),
Crystal::SymbolLiteral.new("foo"),
Crystal::TupleLiteral.new([] of Crystal::ASTNode),
Crystal::TupleLiteral.new([Crystal::StringLiteral.new("foo")] of Crystal::ASTNode),
Crystal::RangeLiteral.new(
Crystal::NumberLiteral.new(0),
Crystal::NumberLiteral.new(10),
true),
].each do |literal|
it "properly identifies static node #{literal}" do
subject.static_literal?(literal).should be_true
subject.dynamic_literal?(literal).should be_false
end
end
[
Crystal::ArrayLiteral.new([Crystal::Path.new(%w[IO])] of Crystal::ASTNode),
Crystal::TupleLiteral.new([Crystal::Path.new(%w[IO])] of Crystal::ASTNode),
].each do |literal|
it "properly identifies dynamic node #{literal}" do
subject.dynamic_literal?(literal).should be_true
subject.static_literal?(literal).should be_false
end
end
end
describe "#node_source" do describe "#node_source" do
it "returns original source of the node" do it "returns original source of the node" do
s = <<-CRYSTAL s = <<-CRYSTAL

View File

@ -85,30 +85,5 @@ module Ameba::AST
assignment.branch.should be_nil assignment.branch.should be_nil
end end
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
end end

View File

@ -85,13 +85,16 @@ module Ameba::AST
3.times { |i| a = a + i } 3.times { |i| a = a + i }
end end
CRYSTAL CRYSTAL
scope = Scope.new nodes.def_nodes.first
var_node = nodes.var_nodes.first var_node = nodes.var_nodes.first
scope.add_variable var_node
scope = Scope.new(nodes.def_nodes.first)
scope.add_variable(var_node)
scope.inner_scopes << Scope.new(nodes.block_nodes.first, scope) scope.inner_scopes << Scope.new(nodes.block_nodes.first, scope)
variable = Variable.new(var_node, scope) variable = Variable.new(var_node, scope)
variable.reference nodes.var_nodes.last, scope.inner_scopes.last variable.reference(nodes.var_nodes.last, scope.inner_scopes.last)
variable.captured_by_block?.should be_truthy variable.captured_by_block?.should be_truthy
end end
@ -101,8 +104,10 @@ module Ameba::AST
a = 1 a = 1
end end
CRYSTAL CRYSTAL
scope.add_variable Crystal::Var.new "a"
scope.add_variable(Crystal::Var.new "a")
variable = scope.variables.first variable = scope.variables.first
variable.captured_by_block?.should be_falsey variable.captured_by_block?.should be_falsey
end end
end end

View File

@ -1,8 +1,6 @@
require "../../../spec_helper" require "../../../spec_helper"
module Ameba::AST module Ameba::AST
source = Source.new ""
describe FlowExpressionVisitor do describe FlowExpressionVisitor do
it "creates an expression for return" do it "creates an expression for return" do
rule = FlowExpressionRule.new rule = FlowExpressionRule.new

View File

@ -10,14 +10,16 @@ module Ameba::Rule
end end
it "contains rules across all the available groups" do it "contains rules across all the available groups" do
Rule.rules.map(&.group_name).uniq!.reject!(&.empty?).sort.should eq %w( Rule.rules.map(&.group_name).uniq!.reject!(&.empty?).sort.should eq %w[
Ameba Ameba
Documentation
Layout Layout
Lint Lint
Metrics Metrics
Naming
Performance Performance
Style Style
) ]
end end
end end
@ -48,25 +50,25 @@ module Ameba::Rule
it "returns false if source is not excluded from this rule" do it "returns false if source is not excluded from this rule" do
rule = DummyRule.new rule = DummyRule.new
rule.excluded = %w(some_source.cr) rule.excluded = %w[some_source.cr]
rule.excluded?(Source.new "", "another_source.cr").should_not be_true rule.excluded?(Source.new "", "another_source.cr").should_not be_true
end end
it "returns true if source is excluded from this rule" do it "returns true if source is excluded from this rule" do
rule = DummyRule.new rule = DummyRule.new
rule.excluded = %w(source.cr) rule.excluded = %w[source.cr]
rule.excluded?(Source.new "", "source.cr").should be_true rule.excluded?(Source.new "", "source.cr").should be_true
end end
it "returns true if source matches the wildcard" do it "returns true if source matches the wildcard" do
rule = DummyRule.new rule = DummyRule.new
rule.excluded = %w(**/*.cr) rule.excluded = %w[**/*.cr]
rule.excluded?(Source.new "", __FILE__).should be_true rule.excluded?(Source.new "", __FILE__).should be_true
end end
it "returns false if source does not match the wildcard" do it "returns false if source does not match the wildcard" do
rule = DummyRule.new rule = DummyRule.new
rule.excluded = %w(*_spec.cr) rule.excluded = %w[*_spec.cr]
rule.excluded?(Source.new "", "source.cr").should be_false rule.excluded?(Source.new "", "source.cr").should be_false
end end
end end

View File

@ -5,97 +5,97 @@ module Ameba::Cli
describe "Cmd" do describe "Cmd" do
describe ".run" do describe ".run" do
it "runs ameba" do it "runs ameba" do
r = Cli.run %w(-f silent file.cr) r = Cli.run %w[-f silent file.cr]
r.should be_nil r.should be_nil
end end
end end
describe ".parse_args" do describe ".parse_args" do
%w(-s --silent).each do |f| %w[-s --silent].each do |flag|
it "accepts #{f} flag" do it "accepts #{flag} flag" do
c = Cli.parse_args [f] c = Cli.parse_args [flag]
c.formatter.should eq :silent c.formatter.should eq :silent
end end
end end
%w(-c --config).each do |f| %w[-c --config].each do |flag|
it "accepts #{f} flag" do it "accepts #{flag} flag" do
c = Cli.parse_args [f, "config.yml"] c = Cli.parse_args [flag, "config.yml"]
c.config.should eq Path["config.yml"] c.config.should eq Path["config.yml"]
end end
end end
%w(-f --format).each do |f| %w[-f --format].each do |flag|
it "accepts #{f} flag" do it "accepts #{flag} flag" do
c = Cli.parse_args [f, "my-formatter"] c = Cli.parse_args [flag, "my-formatter"]
c.formatter.should eq "my-formatter" c.formatter.should eq "my-formatter"
end end
end end
it "accepts --only flag" do it "accepts --only flag" do
c = Cli.parse_args ["--only", "RULE1,RULE2"] c = Cli.parse_args ["--only", "RULE1,RULE2"]
c.only.should eq %w(RULE1 RULE2) c.only.should eq %w[RULE1 RULE2]
end end
it "accepts --except flag" do it "accepts --except flag" do
c = Cli.parse_args ["--except", "RULE1,RULE2"] c = Cli.parse_args ["--except", "RULE1,RULE2"]
c.except.should eq %w(RULE1 RULE2) c.except.should eq %w[RULE1 RULE2]
end end
it "defaults rules? flag to false" do it "defaults rules? flag to false" do
c = Cli.parse_args %w(file.cr) c = Cli.parse_args %w[file.cr]
c.rules?.should be_false c.rules?.should be_false
end end
it "defaults skip_reading_config? flag to false" do it "defaults skip_reading_config? flag to false" do
c = Cli.parse_args %w(file.cr) c = Cli.parse_args %w[file.cr]
c.skip_reading_config?.should be_false c.skip_reading_config?.should be_false
end end
it "accepts --rules flag" do it "accepts --rules flag" do
c = Cli.parse_args %w(--rules) c = Cli.parse_args %w[--rules]
c.rules?.should eq true c.rules?.should eq true
end 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 be_false c.all?.should be_false
end end
it "accepts --all flag" do it "accepts --all flag" do
c = Cli.parse_args %w(--all) c = Cli.parse_args %w[--all]
c.all?.should eq true c.all?.should eq true
end end
it "accepts --gen-config flag" do it "accepts --gen-config flag" do
c = Cli.parse_args %w(--gen-config) c = Cli.parse_args %w[--gen-config]
c.formatter.should eq :todo c.formatter.should eq :todo
end end
it "accepts --no-color flag" do it "accepts --no-color flag" do
c = Cli.parse_args %w(--no-color) c = Cli.parse_args %w[--no-color]
c.colors?.should be_false c.colors?.should be_false
end end
it "accepts --without-affected-code flag" do it "accepts --without-affected-code flag" do
c = Cli.parse_args %w(--without-affected-code) c = Cli.parse_args %w[--without-affected-code]
c.without_affected_code?.should be_true c.without_affected_code?.should be_true
end end
it "doesn't disable colors by default" do it "doesn't disable colors by default" do
c = Cli.parse_args %w(--all) c = Cli.parse_args %w[--all]
c.colors?.should be_true c.colors?.should be_true
end end
it "ignores --config if --gen-config flag passed" do it "ignores --config if --gen-config flag passed" do
c = Cli.parse_args %w(--gen-config --config my_config.yml) c = Cli.parse_args %w[--gen-config --config my_config.yml]
c.formatter.should eq :todo c.formatter.should eq :todo
c.skip_reading_config?.should be_true c.skip_reading_config?.should be_true
end end
describe "-e/--explain" do describe "-e/--explain" do
it "configures file/line/column" do it "configures file/line/column" do
c = Cli.parse_args %w(--explain src/file.cr:3:5) c = Cli.parse_args %w[--explain src/file.cr:3:5]
location_to_explain = c.location_to_explain.should_not be_nil 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 "src/file.cr"
@ -105,59 +105,59 @@ module Ameba::Cli
it "raises an error if location is not valid" do it "raises an error if location is not valid" do
expect_raises(Exception, "location should have PATH:line:column") 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 src/file.cr:3]
end end
end end
it "raises an error if line number is not valid" do it "raises an error if line number is not valid" do
expect_raises(Exception, "location should have PATH:line:column") 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 src/file.cr:a:3]
end end
end end
it "raises an error if column number is not valid" do it "raises an error if column number is not valid" do
expect_raises(Exception, "location should have PATH:line:column") 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 src/file.cr:3:&]
end end
end end
it "raises an error if line/column are missing" do it "raises an error if line/column are missing" do
expect_raises(Exception, "location should have PATH:line:column") do expect_raises(Exception, "location should have PATH:line:column") do
Cli.parse_args %w(--explain src/file.cr) Cli.parse_args %w[--explain src/file.cr]
end end
end end
end end
context "--fail-level" do context "--fail-level" do
it "configures fail level Convention" do it "configures fail level Convention" do
c = Cli.parse_args %w(--fail-level convention) c = Cli.parse_args %w[--fail-level convention]
c.fail_level.should eq Severity::Convention c.fail_level.should eq Severity::Convention
end end
it "configures fail level Warning" do it "configures fail level Warning" do
c = Cli.parse_args %w(--fail-level Warning) c = Cli.parse_args %w[--fail-level Warning]
c.fail_level.should eq Severity::Warning c.fail_level.should eq Severity::Warning
end end
it "configures fail level Error" do it "configures fail level Error" do
c = Cli.parse_args %w(--fail-level error) c = Cli.parse_args %w[--fail-level error]
c.fail_level.should eq Severity::Error c.fail_level.should eq Severity::Error
end end
it "raises if fail level is incorrect" do it "raises if fail level is incorrect" do
expect_raises(Exception, "Incorrect severity name JohnDoe") do expect_raises(Exception, "Incorrect severity name JohnDoe") do
Cli.parse_args %w(--fail-level JohnDoe) Cli.parse_args %w[--fail-level JohnDoe]
end end
end end
end end
it "accepts unknown args as globs" do it "accepts unknown args as globs" do
c = Cli.parse_args %w(source1.cr source2.cr) c = Cli.parse_args %w[source1.cr source2.cr]
c.globs.should eq %w(source1.cr source2.cr) c.globs.should eq %w[source1.cr source2.cr]
end end
it "accepts one unknown arg as explain location if it has correct format" do it "accepts one unknown arg as explain location if it has correct format" do
c = Cli.parse_args %w(source.cr:3:22) c = Cli.parse_args %w[source.cr:3:22]
location_to_explain = c.location_to_explain.should_not be_nil location_to_explain = c.location_to_explain.should_not be_nil
location_to_explain[:file].should eq "source.cr" location_to_explain[:file].should eq "source.cr"

View File

@ -2,7 +2,7 @@ require "../spec_helper"
module Ameba module Ameba
describe Config do describe Config do
config_sample = "config/ameba.yml" config_sample = "spec/fixtures/config.yml"
it "should have a list of available formatters" do it "should have a list of available formatters" do
Config::AVAILABLE_FORMATTERS.should_not be_nil Config::AVAILABLE_FORMATTERS.should_not be_nil
@ -21,7 +21,7 @@ module Ameba
Globs: src/*.cr Globs: src/*.cr
CONFIG CONFIG
config = Config.new(yml) config = Config.new(yml)
config.globs.should eq %w(src/*.cr) config.globs.should eq %w[src/*.cr]
end end
it "initializes globs as array" do it "initializes globs as array" do
@ -32,7 +32,7 @@ module Ameba
- "!spec" - "!spec"
CONFIG CONFIG
config = Config.new(yml) config = Config.new(yml)
config.globs.should eq %w(src/*.cr !spec) config.globs.should eq %w[src/*.cr !spec]
end end
it "raises if Globs has a wrong type" do it "raises if Globs has a wrong type" do
@ -51,7 +51,7 @@ module Ameba
Excluded: spec Excluded: spec
CONFIG CONFIG
config = Config.new(yml) config = Config.new(yml)
config.excluded.should eq %w(spec) config.excluded.should eq %w[spec]
end end
it "initializes excluded as array" do it "initializes excluded as array" do
@ -62,7 +62,7 @@ module Ameba
- lib/*.cr - lib/*.cr
CONFIG CONFIG
config = Config.new(yml) config = Config.new(yml)
config.excluded.should eq %w(spec lib/*.cr) config.excluded.should eq %w[spec lib/*.cr]
end end
it "raises if Excluded has a wrong type" do it "raises if Excluded has a wrong type" do
@ -84,6 +84,12 @@ module Ameba
config.formatter.should_not be_nil config.formatter.should_not be_nil
end end
it "raises when custom config file doesn't exist" do
expect_raises(Exception, "Unable to load config file: Config file does not exist") do
Config.load "foo.yml"
end
end
it "loads default config" do it "loads default config" do
config = Config.load config = Config.load
config.should_not be_nil config.should_not be_nil
@ -128,12 +134,12 @@ module Ameba
end end
it "returns a list of sources matching globs" do it "returns a list of sources matching globs" do
config.globs = %w(**/config_spec.cr) config.globs = %w[**/config_spec.cr]
config.sources.size.should eq(1) config.sources.size.should eq(1)
end end
it "returns a list of sources excluding 'Excluded'" do it "returns a list of sources excluding 'Excluded'" do
config.excluded = %w(**/config_spec.cr) config.excluded = %w[**/config_spec.cr]
config.sources.any?(&.fullpath.==(__FILE__)).should be_false config.sources.any?(&.fullpath.==(__FILE__)).should be_false
end end
end end
@ -175,7 +181,7 @@ module Ameba
it "updates excluded property" do it "updates excluded property" do
name = DummyRule.rule_name name = DummyRule.rule_name
excluded = %w(spec/source.cr) excluded = %w[spec/source.cr]
config.update_rule name, excluded: excluded config.update_rule name, excluded: excluded
rule = config.rules.find!(&.name.== name) rule = config.rules.find!(&.name.== name)
rule.excluded.should eq excluded rule.excluded.should eq excluded
@ -194,7 +200,7 @@ module Ameba
it "updates multiple rules by excluded property" do it "updates multiple rules by excluded property" do
name = DummyRule.rule_name name = DummyRule.rule_name
excluded = %w(spec/source.cr) excluded = %w[spec/source.cr]
config.update_rules [name], excluded: excluded config.update_rules [name], excluded: excluded
rule = config.rules.find!(&.name.== name) rule = config.rules.find!(&.name.== name)
rule.excluded.should eq excluded rule.excluded.should eq excluded
@ -209,7 +215,7 @@ module Ameba
it "updates a group by excluded property" do it "updates a group by excluded property" do
name = DummyRule.group_name name = DummyRule.group_name
excluded = %w(spec/source.cr) excluded = %w[spec/source.cr]
config.update_rules [name], excluded: excluded config.update_rules [name], excluded: excluded
rule = config.rules.find!(&.name.== DummyRule.rule_name) rule = config.rules.find!(&.name.== DummyRule.rule_name)
rule.excluded.should eq excluded rule.excluded.should eq excluded

View File

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

@ -1,6 +1,6 @@
require "../../../spec_helper" require "../../../spec_helper"
module Ameba::Rule::Lint module Ameba::Rule::Documentation
subject = Documentation.new subject = Documentation.new
.tap(&.ignore_classes = false) .tap(&.ignore_classes = false)
.tap(&.ignore_modules = false) .tap(&.ignore_modules = false)

View File

@ -4,16 +4,16 @@ module Ameba
subject = Rule::Lint::EmptyExpression.new subject = Rule::Lint::EmptyExpression.new
private def it_detects_empty_expression(code, *, file = __FILE__, line = __LINE__) private def it_detects_empty_expression(code, *, file = __FILE__, line = __LINE__)
it %(detects empty expression "#{code}"), file, line do it "detects empty expression #{code.inspect}", file, line do
s = Source.new code source = Source.new code
rule = Rule::Lint::EmptyExpression.new rule = Rule::Lint::EmptyExpression.new
rule.catch(s).should_not be_valid, file: file, line: line rule.catch(source).should_not be_valid, file: file, line: line
end end
end end
describe Rule::Lint::EmptyExpression do describe Rule::Lint::EmptyExpression do
it "passes if there is no empty expression" do it "passes if there is no empty expression" do
s = Source.new <<-CRYSTAL expect_no_issues subject, <<-CRYSTAL
def method() def method()
end end
@ -31,7 +31,6 @@ module Ameba
begin "" end begin "" end
[nil] << nil [nil] << nil
CRYSTAL CRYSTAL
subject.catch(s).should be_valid
end end
it_detects_empty_expression %(()) it_detects_empty_expression %(())
@ -91,10 +90,10 @@ module Ameba
) )
it "does not report empty expression in macro" do it "does not report empty expression in macro" do
s = Source.new %q( expect_no_issues subject, <<-CRYSTAL
module MyModule module MyModule
macro conditional_error_for_inline_callbacks macro conditional_error_for_inline_callbacks
\{% \\{%
raise "" raise ""
%} %}
end end
@ -102,8 +101,7 @@ module Ameba
macro before_save(x = nil) macro before_save(x = nil)
end end
end end
) CRYSTAL
subject.catch(s).should be_valid
end end
end end
end end

View File

@ -6,8 +6,12 @@ module Ameba::Rule::Lint
describe LiteralsComparison do describe LiteralsComparison do
it "passes for valid cases" do it "passes for valid cases" do
expect_no_issues subject, <<-CRYSTAL expect_no_issues subject, <<-CRYSTAL
{start.year, start.month} == {stop.year, stop.month}
["foo"] === [foo]
"foo" == foo "foo" == foo
"foo" != foo "foo" != foo
"foo" == FOO
FOO == "foo"
foo == "foo" foo == "foo"
foo != "foo" foo != "foo"
CRYSTAL CRYSTAL
@ -15,8 +19,8 @@ module Ameba::Rule::Lint
it "reports if there is a dynamic comparison possibly evaluating to the same" do it "reports if there is a dynamic comparison possibly evaluating to the same" do
expect_issue subject, <<-CRYSTAL expect_issue subject, <<-CRYSTAL
[foo] === ["foo"] [foo] === [foo]
# ^^^^^^^^^^^^^^^ error: Comparison most likely evaluates to the same # ^^^^^^^^^^^^^ error: Comparison most likely evaluates to the same
CRYSTAL CRYSTAL
end end

View File

@ -11,6 +11,7 @@ module Ameba::Rule::Lint
(1..3).index { |i| i > 2 }.not_nil!(:foo) (1..3).index { |i| i > 2 }.not_nil!(:foo)
(1..3).rindex { |i| i > 2 }.not_nil!(:foo) (1..3).rindex { |i| i > 2 }.not_nil!(:foo)
(1..3).find { |i| i > 2 }.not_nil!(:foo) (1..3).find { |i| i > 2 }.not_nil!(:foo)
/(.)(.)(.)/.match("abc", &.itself).not_nil!
CRYSTAL CRYSTAL
end end
@ -36,6 +37,17 @@ module Ameba::Rule::Lint
CRYSTAL CRYSTAL
end end
it "reports if there is an `match` call followed by `not_nil!`" do
source = expect_issue subject, <<-CRYSTAL
/(.)(.)(.)/.match("abc").not_nil![2]
# ^^^^^^^^^^^^^^^^^^^^^ error: Use `match! {...}` instead of `match {...}.not_nil!`
CRYSTAL
expect_correction source, <<-CRYSTAL
/(.)(.)(.)/.match!("abc")[2]
CRYSTAL
end
it "reports if there is an `index` call with block followed by `not_nil!`" do it "reports if there is an `index` call with block followed by `not_nil!`" do
source = expect_issue subject, <<-CRYSTAL source = expect_issue subject, <<-CRYSTAL
(1..3).index { |i| i > 2 }.not_nil! (1..3).index { |i| i > 2 }.not_nil!

View File

@ -6,41 +6,41 @@ module Ameba::Rule::Lint
it "passes if percent arrays are written correctly" do it "passes if percent arrays are written correctly" do
s = Source.new %q( s = Source.new %q(
%i(one two three) %i[one two three]
%w(one two three) %w[one two three]
%i(1 2 3) %i[1 2 3]
%w(1 2 3) %w[1 2 3]
%i() %i[]
%w() %w[]
) )
subject.catch(s).should be_valid subject.catch(s).should be_valid
end end
it "fails if string percent array has commas" do it "fails if string percent array has commas" do
s = Source.new %( %w(one, two) ) s = Source.new %( %w[one, two] )
subject.catch(s).should_not be_valid subject.catch(s).should_not be_valid
end end
it "fails if string percent array has quotes" do it "fails if string percent array has quotes" do
s = Source.new %( %w("one" "two") ) s = Source.new %( %w["one" "two"] )
subject.catch(s).should_not be_valid subject.catch(s).should_not be_valid
end end
it "fails if symbols percent array has commas" do it "fails if symbols percent array has commas" do
s = Source.new %( %i(one, two) ) s = Source.new %( %i[one, two] )
subject.catch(s).should_not be_valid subject.catch(s).should_not be_valid
end end
it "fails if symbols percent array has a colon" do it "fails if symbols percent array has a colon" do
s = Source.new %( %i(:one :two) ) s = Source.new %( %i[:one :two] )
subject.catch(s).should_not be_valid subject.catch(s).should_not be_valid
end end
it "reports rule, location and message for %i" do it "reports rule, location and message for %i" do
s = Source.new %( s = Source.new %(
%i(:one) %i[:one]
), "source.cr" ), "source.cr"
subject.catch(s).should_not be_valid subject.catch(s).should_not be_valid
@ -54,7 +54,7 @@ module Ameba::Rule::Lint
it "reports rule, location and message for %w" do it "reports rule, location and message for %w" do
s = Source.new %( s = Source.new %(
%w("one") %w["one"]
), "source.cr" ), "source.cr"
subject.catch(s).should_not be_valid subject.catch(s).should_not be_valid
@ -71,14 +71,14 @@ module Ameba::Rule::Lint
it "#string_array_unwanted_symbols" do it "#string_array_unwanted_symbols" do
rule = PercentArrays.new rule = PercentArrays.new
rule.string_array_unwanted_symbols = "," rule.string_array_unwanted_symbols = ","
s = Source.new %( %w("one") ) s = Source.new %( %w["one"] )
rule.catch(s).should be_valid rule.catch(s).should be_valid
end end
it "#symbol_array_unwanted_symbols" do it "#symbol_array_unwanted_symbols" do
rule = PercentArrays.new rule = PercentArrays.new
rule.symbol_array_unwanted_symbols = "," rule.symbol_array_unwanted_symbols = ","
s = Source.new %( %i(:one) ) s = Source.new %( %i[:one] )
rule.catch(s).should be_valid rule.catch(s).should be_valid
end end
end end

View File

@ -31,6 +31,30 @@ module Ameba::Rule::Lint
CRYSTAL CRYSTAL
end end
pending "reports if there is a shadowing in an unpacked variable in a block" do
expect_issue subject, <<-CRYSTAL
def some_method
foo = 1
[{3}].each do |(foo)|
# ^ error: Shadowing outer local variable `foo`
end
end
CRYSTAL
end
pending "reports if there is a shadowing in an unpacked variable in a block (2)" do
expect_issue subject, <<-CRYSTAL
def some_method
foo = 1
[{[3]}].each do |((foo))|
# ^ error: Shadowing outer local variable `foo`
end
end
CRYSTAL
end
it "does not report outer vars declared below shadowed block" do it "does not report outer vars declared below shadowed block" do
expect_no_issues subject, <<-CRYSTAL expect_no_issues subject, <<-CRYSTAL
methods = klass.methods.select { |m| m.annotation(MyAnn) } methods = klass.methods.select { |m| m.annotation(MyAnn) }
@ -44,7 +68,7 @@ module Ameba::Rule::Lint
foo = 1 foo = 1
-> (foo : Int32) {} -> (foo : Int32) {}
# ^ error: Shadowing outer local variable `foo` # ^^^^^^^^^^^ error: Shadowing outer local variable `foo`
end end
CRYSTAL CRYSTAL
end end
@ -69,7 +93,7 @@ module Ameba::Rule::Lint
3.times do |foo| 3.times do |foo|
# ^ error: Shadowing outer local variable `foo` # ^ error: Shadowing outer local variable `foo`
-> (foo : Int32) { foo + 1 } -> (foo : Int32) { foo + 1 }
# ^ error: Shadowing outer local variable `foo` # ^^^^^^^^^^^ error: Shadowing outer local variable `foo`
end end
CRYSTAL CRYSTAL
end end

View File

@ -39,7 +39,7 @@ module Ameba::Rule::Lint
CRYSTAL CRYSTAL
end end
it "reports if there is a shared var in spawn" do it "reports if there is a shared var in spawn (while)" do
source = expect_issue subject, <<-CRYSTAL source = expect_issue subject, <<-CRYSTAL
i = 0 i = 0
while i < 10 while i < 10
@ -56,6 +56,24 @@ module Ameba::Rule::Lint
expect_no_corrections source expect_no_corrections source
end end
it "reports if there is a shared var in spawn (loop)" do
source = expect_issue subject, <<-CRYSTAL
i = 0
loop do
break if i >= 10
spawn do
puts(i)
# ^ error: Shared variable `i` is used in fiber
end
i += 1
end
Fiber.yield
CRYSTAL
expect_no_corrections source
end
it "reports reassigned reference to shared var in spawn" do it "reports reassigned reference to shared var in spawn" do
source = expect_issue subject, <<-CRYSTAL source = expect_issue subject, <<-CRYSTAL
channel = Channel(String).new channel = Channel(String).new

View File

@ -0,0 +1,36 @@
require "../../../spec_helper"
module Ameba::Rule::Lint
subject = SpecFilename.new
describe SpecFilename do
it "passes if filename is correct" do
expect_no_issues subject, code: "", path: "spec/foo_spec.cr"
expect_no_issues subject, code: "", path: "spec/foo/bar_spec.cr"
end
it "fails if filename is wrong" do
expect_issue subject, <<-CRYSTAL, path: "spec/foo.cr"
# ^{} error: Spec filename should have `_spec` suffix: foo_spec.cr, not foo.cr
CRYSTAL
end
context "properties" do
context "#ignored_dirs" do
it "provide sane defaults" do
expect_no_issues subject, code: "", path: "spec/support/foo.cr"
expect_no_issues subject, code: "", path: "spec/fixtures/foo.cr"
expect_no_issues subject, code: "", path: "spec/data/foo.cr"
end
end
context "#ignored_filenames" do
it "ignores spec_helper by default" do
expect_no_issues subject, code: "", path: "spec/spec_helper.cr"
expect_no_issues subject, code: "", path: "spec/foo/spec_helper.cr"
end
end
end
end
end

View File

@ -115,12 +115,12 @@ module Ameba::Rule::Lint
first.rule.should_not be_nil first.rule.should_not be_nil
first.location.to_s.should eq "source_spec.cr:1:11" 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" first.message.should eq "Focused spec item detected"
second.rule.should_not be_nil second.rule.should_not be_nil
second.location.to_s.should eq "source_spec.cr:2:13" 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" second.message.should eq "Focused spec item detected"
end end
end end

View File

@ -0,0 +1,35 @@
require "../../../spec_helper"
private def check_typos_bin!
unless Ameba::Rule::Lint::Typos::BIN_PATH
pending! "`typos` executable is not available"
end
end
module Ameba::Rule::Lint
subject = Typos.new
.tap(&.fail_on_error = true)
describe Typos do
it "reports typos" do
check_typos_bin!
source = expect_issue subject, <<-CRYSTAL
# method with no arugments
# ^^^^^^^^^ error: Typo found: arugments -> arguments
def tpos
# ^^^^ error: Typo found: tpos -> typos
:otput
# ^^^^^ error: Typo found: otput -> output
end
CRYSTAL
expect_correction source, <<-CRYSTAL
# method with no arguments
def typos
:output
end
CRYSTAL
end
end
end

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,93 @@
require "../../../spec_helper"
module Ameba::Rule::Naming
subject = AccessorMethodName.new
describe AccessorMethodName do
it "passes if accessor method name is correct" do
expect_no_issues subject, <<-CRYSTAL
class Foo
def self.instance
end
def self.instance=(value)
end
def user
end
def user=(user)
end
end
CRYSTAL
end
it "passes if accessor method is defined in top-level scope" do
expect_no_issues subject, <<-CRYSTAL
def get_user
end
def set_user(user)
end
CRYSTAL
end
it "fails if accessor method is defined with receiver in top-level scope" do
expect_issue subject, <<-CRYSTAL
def Foo.get_user
# ^^^^^^^^ error: Favour method name 'user' over 'get_user'
end
def Foo.set_user(user)
# ^^^^^^^^ error: Favour method name 'user=' over 'set_user'
end
CRYSTAL
end
it "fails if accessor method name is wrong" do
expect_issue subject, <<-CRYSTAL
class Foo
def self.get_instance
# ^^^^^^^^^^^^ error: Favour method name 'instance' over 'get_instance'
end
def self.set_instance(value)
# ^^^^^^^^^^^^ error: Favour method name 'instance=' over 'set_instance'
end
def get_user
# ^^^^^^^^ error: Favour method name 'user' over 'get_user'
end
def set_user(user)
# ^^^^^^^^ error: Favour method name 'user=' over 'set_user'
end
end
CRYSTAL
end
it "ignores if alternative name isn't valid syntax" do
expect_no_issues subject, <<-CRYSTAL
class Foo
def get_404
end
def set_404(value)
end
end
CRYSTAL
end
it "ignores if the method has unexpected arity" do
expect_no_issues subject, <<-CRYSTAL
class Foo
def get_user(type)
end
def set_user(user, type)
end
end
CRYSTAL
end
end
end

View File

@ -0,0 +1,151 @@
require "../../../spec_helper"
module Ameba::Rule::Naming
subject = AsciiIdentifiers.new
describe AsciiIdentifiers do
it "reports classes with names containing non-ascii characters" do
expect_issue subject, <<-CRYSTAL
class BigAwesome🐺
# ^^^^^^^^^^^ error: Identifier contains non-ascii characters
@🐺_name : String
# ^^^^^^^ error: Identifier contains non-ascii characters
end
CRYSTAL
end
it "reports modules with names containing non-ascii characters" do
expect_issue subject, <<-CRYSTAL
module Bąk
# ^^^ error: Identifier contains non-ascii characters
@@bąk_name : String
# ^^^^^^^^^^ error: Identifier contains non-ascii characters
end
CRYSTAL
end
it "reports enums with names containing non-ascii characters" do
expect_issue subject, <<-CRYSTAL
enum TypeOf🔥
# ^^^^^^^ error: Identifier contains non-ascii characters
end
CRYSTAL
end
it "reports defs with names containing non-ascii characters" do
expect_issue subject, <<-CRYSTAL
def łó
# ^^^^ error: Identifier contains non-ascii characters
end
CRYSTAL
end
it "reports defs with parameter names containing non-ascii characters" do
expect_issue subject, <<-CRYSTAL
def forest_adventure(include_🐺 = true, include_🐿 = true)
# ^ error: Identifier contains non-ascii characters
# ^ error: Identifier contains non-ascii characters
end
CRYSTAL
end
it "reports defs with parameter default values containing non-ascii characters" do
expect_issue subject, <<-CRYSTAL
def forest_adventure(animal_type = :🐺)
# ^^ error: Identifier contains non-ascii characters
end
CRYSTAL
end
it "reports argument names containing non-ascii characters" do
expect_issue subject, <<-CRYSTAL
%w[wensleydale cheddar brie].each { |🧀| nil }
# ^ error: Identifier contains non-ascii characters
CRYSTAL
end
it "reports calls with arguments containing non-ascii characters" do
expect_issue subject, <<-CRYSTAL
%i[🐺 🐿].index!(:🐺)
# ^^ error: Identifier contains non-ascii characters
CRYSTAL
end
it "reports calls with named arguments containing non-ascii characters" do
expect_issue subject, <<-CRYSTAL
%i[🐺 🐿].index!(obj: :🐺)
# ^^ error: Identifier contains non-ascii characters
CRYSTAL
end
it "reports aliases with names containing non-ascii characters" do
expect_issue subject, <<-CRYSTAL
alias JSON🧀 = JSON::Any
# ^^^^^ error: Identifier contains non-ascii characters
CRYSTAL
end
it "reports constants with names containing non-ascii characters" do
expect_issue subject, <<-CRYSTAL
I_LOVE_🍣 = true
# ^^^^^^ error: Identifier contains non-ascii characters
CRYSTAL
end
it "reports assignments with variable names containing non-ascii characters" do
expect_issue subject, <<-CRYSTAL
space_👾 = true
# ^^^^^ error: Identifier contains non-ascii characters
CRYSTAL
end
it "reports multiple assignments with variable names containing non-ascii characters" do
expect_issue subject, <<-CRYSTAL
foo, space_👾 = true, true
# ^^^^^^^ error: Identifier contains non-ascii characters
CRYSTAL
end
it "reports assignments with symbol literals containing non-ascii characters" do
expect_issue subject, <<-CRYSTAL
foo = :
# ^^^ error: Identifier contains non-ascii characters
CRYSTAL
end
it "reports multiple assignments with symbol literals containing non-ascii characters" do
expect_issue subject, <<-CRYSTAL
foo, bar = :, true
# ^^^ error: Identifier contains non-ascii characters
CRYSTAL
end
it "passes for strings with non-ascii characters" do
expect_no_issues subject, <<-CRYSTAL
space = "👾"
space = :invader # 👾
CRYSTAL
end
context "properties" do
context "#ignore_symbols" do
it "returns `false` by default" do
rule = AsciiIdentifiers.new
rule.ignore_symbols?.should be_false
end
it "stops reporting symbol literals if set to `true`" do
rule = AsciiIdentifiers.new
rule.ignore_symbols = true
expect_no_issues rule, <<-CRYSTAL
def forest_adventure(animal_type = :🐺); end
%i[🐺 🐿].index!(:🐺)
foo, bar = :, true
foo = :
CRYSTAL
end
end
end
end
end

View File

@ -0,0 +1,50 @@
require "../../../spec_helper"
module Ameba::Rule::Naming
subject = BinaryOperatorParameterName.new
describe BinaryOperatorParameterName do
it "ignores `other` parameter name in binary method definitions" do
expect_no_issues subject, <<-CRYSTAL
def +(other); end
def -(other); end
def *(other); end
CRYSTAL
end
it "ignores binary method definitions with arity other than 1" do
expect_no_issues subject, <<-CRYSTAL
def +; end
def +(foo, bar); end
def -; end
def -(foo, bar); end
CRYSTAL
end
it "ignores non-binary method definitions" do
expect_no_issues subject, <<-CRYSTAL
def foo(bar); end
def bąk(genus); end
CRYSTAL
end
it "reports binary methods definitions with incorrectly named parameter" do
expect_issue subject, <<-CRYSTAL
def +(foo); end
# ^ error: When defining the `+` operator, name its argument `other`
def -(foo); end
# ^ error: When defining the `-` operator, name its argument `other`
def *(foo); end
# ^ error: When defining the `*` operator, name its argument `other`
CRYSTAL
end
it "ignores methods from #excluded_operators" do
subject.excluded_operators.each do |op|
expect_no_issues subject, <<-CRYSTAL
def #{op}(foo); end
CRYSTAL
end
end
end
end

View File

@ -0,0 +1,100 @@
require "../../../spec_helper"
module Ameba::Rule::Naming
subject = BlockParameterName.new
.tap(&.min_name_length = 3)
.tap(&.allowed_names = %w[_ e i j k v])
describe BlockParameterName do
it "passes if block parameter name matches #allowed_names" do
subject.allowed_names.each do |name|
expect_no_issues subject, <<-CRYSTAL
%w[].each { |#{name}| }
CRYSTAL
end
end
it "fails if block parameter name doesn't match #allowed_names" do
expect_issue subject, <<-CRYSTAL
%w[].each { |x| }
# ^ error: Disallowed block parameter name found
CRYSTAL
end
context "properties" do
context "#min_name_length" do
it "allows setting custom values" do
rule = BlockParameterName.new
rule.allowed_names = %w[a b c]
rule.min_name_length = 3
expect_issue rule, <<-CRYSTAL
%w[].each { |x| }
# ^ error: Disallowed block parameter name found
CRYSTAL
rule.min_name_length = 1
expect_no_issues rule, <<-CRYSTAL
%w[].each { |x| }
CRYSTAL
end
end
context "#allow_names_ending_in_numbers" do
it "allows setting custom values" do
rule = BlockParameterName.new
rule.min_name_length = 1
rule.allowed_names = %w[]
rule.allow_names_ending_in_numbers = false
expect_issue rule, <<-CRYSTAL
%w[].each { |x1| }
# ^ error: Disallowed block parameter name found
CRYSTAL
rule.allow_names_ending_in_numbers = true
expect_no_issues rule, <<-CRYSTAL
%w[].each { |x1| }
CRYSTAL
end
end
context "#allowed_names" do
it "allows setting custom names" do
rule = BlockParameterName.new
rule.min_name_length = 3
rule.allowed_names = %w[a b c]
expect_issue rule, <<-CRYSTAL
%w[].each { |x| }
# ^ error: Disallowed block parameter name found
CRYSTAL
rule.allowed_names = %w[x y z]
expect_no_issues rule, <<-CRYSTAL
%w[].each { |x| }
CRYSTAL
end
end
context "#forbidden_names" do
it "allows setting custom names" do
rule = BlockParameterName.new
rule.min_name_length = 1
rule.allowed_names = %w[]
rule.forbidden_names = %w[x y z]
expect_issue rule, <<-CRYSTAL
%w[].each { |x| }
# ^ error: Disallowed block parameter name found
CRYSTAL
rule.forbidden_names = %w[a b c]
expect_no_issues rule, <<-CRYSTAL
%w[].each { |x| }
CRYSTAL
end
end
end
end
end

View File

@ -1,11 +1,11 @@
require "../../../spec_helper" require "../../../spec_helper"
module Ameba module Ameba
subject = Rule::Style::ConstantNames.new subject = Rule::Naming::ConstantNames.new
private def it_reports_constant(name, value, expected, *, file = __FILE__, line = __LINE__) private def it_reports_constant(name, value, expected, *, file = __FILE__, line = __LINE__)
it "reports constant name #{expected}", file, line do it "reports constant name #{expected}", file, line do
rule = Rule::Style::ConstantNames.new rule = Rule::Naming::ConstantNames.new
expect_issue rule, <<-CRYSTAL, name: name, file: file, line: line expect_issue rule, <<-CRYSTAL, name: name, file: file, line: line
%{name} = #{value} %{name} = #{value}
# ^{name} error: Constant name should be screaming-cased: #{expected}, not #{name} # ^{name} error: Constant name should be screaming-cased: #{expected}, not #{name}
@ -13,7 +13,7 @@ module Ameba
end end
end end
describe Rule::Style::ConstantNames do describe Rule::Naming::ConstantNames do
it "passes if type names are screaming-cased" do it "passes if type names are screaming-cased" do
expect_no_issues subject, <<-CRYSTAL expect_no_issues subject, <<-CRYSTAL
LUCKY_NUMBERS = [3, 7, 11] LUCKY_NUMBERS = [3, 7, 11]

View File

@ -0,0 +1,19 @@
require "../../../spec_helper"
module Ameba::Rule::Naming
subject = Filename.new
describe Filename do
it "passes if filename is correct" do
expect_no_issues subject, code: "", path: "src/foo.cr"
expect_no_issues subject, code: "", path: "src/foo_bar.cr"
end
it "fails if filename is wrong" do
expect_issue subject, <<-CRYSTAL, path: "src/fooBar.cr"
# ^{} error: Filename should be underscore-cased: foo_bar.cr, not fooBar.cr
CRYSTAL
end
end
end

View File

@ -1,11 +1,11 @@
require "../../../spec_helper" require "../../../spec_helper"
module Ameba module Ameba
subject = Rule::Style::MethodNames.new subject = Rule::Naming::MethodNames.new
private def it_reports_method_name(name, expected, *, file = __FILE__, line = __LINE__) private def it_reports_method_name(name, expected, *, file = __FILE__, line = __LINE__)
it "reports method name #{expected}", file, line do it "reports method name #{expected}", file, line do
rule = Rule::Style::MethodNames.new rule = Rule::Naming::MethodNames.new
expect_issue rule, <<-CRYSTAL, name: name, file: file, line: line expect_issue rule, <<-CRYSTAL, name: name, file: file, line: line
def %{name}; end def %{name}; end
# ^{name} error: Method name should be underscore-cased: #{expected}, not %{name} # ^{name} error: Method name should be underscore-cased: #{expected}, not %{name}
@ -13,7 +13,7 @@ module Ameba
end end
end end
describe Rule::Style::MethodNames do describe Rule::Naming::MethodNames do
it "passes if method names are underscore-cased" do it "passes if method names are underscore-cased" do
expect_no_issues subject, <<-CRYSTAL expect_no_issues subject, <<-CRYSTAL
class Person class Person

View File

@ -1,6 +1,6 @@
require "../../../spec_helper" require "../../../spec_helper"
module Ameba::Rule::Style module Ameba::Rule::Naming
subject = PredicateName.new subject = PredicateName.new
describe PredicateName do describe PredicateName do
@ -21,8 +21,18 @@ module Ameba::Rule::Style
it "fails if predicate name is wrong" do it "fails if predicate name is wrong" do
expect_issue subject, <<-CRYSTAL expect_issue subject, <<-CRYSTAL
class Image
def self.is_valid?(x)
# ^^^^^^^^^ error: Favour method name 'valid?' over 'is_valid?'
end
end
def is_valid?(x) def is_valid?(x)
# ^^^^^^^^^^^^^^ error: Favour method name 'valid?' over 'is_valid?' # ^^^^^^^^^ error: Favour method name 'valid?' over 'is_valid?'
end
def is_valid(x)
# ^^^^^^^^ error: Favour method name 'valid?' over 'is_valid'
end end
CRYSTAL CRYSTAL
end end

View File

@ -1,6 +1,6 @@
require "../../../spec_helper" require "../../../spec_helper"
module Ameba::Rule::Style module Ameba::Rule::Naming
subject = QueryBoolMethods.new subject = QueryBoolMethods.new
describe QueryBoolMethods do describe QueryBoolMethods do

View File

@ -0,0 +1,53 @@
require "../../../spec_helper"
module Ameba::Rule::Naming
subject = RescuedExceptionsVariableName.new
describe RescuedExceptionsVariableName do
it "passes if exception handler variable name matches #allowed_names" do
subject.allowed_names.each do |name|
expect_no_issues subject, <<-CRYSTAL
def foo
raise "foo"
rescue #{name}
nil
end
CRYSTAL
end
end
it "fails if exception handler variable name doesn't match #allowed_names" do
expect_issue subject, <<-CRYSTAL
def foo
raise "foo"
rescue wtf
# ^^^^^^^^ error: Disallowed variable name, use one of these instead: 'e', 'ex', 'exception', 'error'
nil
end
CRYSTAL
end
context "properties" do
context "#allowed_names" do
it "returns sensible defaults" do
rule = RescuedExceptionsVariableName.new
rule.allowed_names.should eq %w[e ex exception error]
end
it "allows setting custom names" do
rule = RescuedExceptionsVariableName.new
rule.allowed_names = %w[foo]
expect_issue rule, <<-CRYSTAL
def foo
raise "foo"
rescue e
# ^^^^^^ error: Disallowed variable name, use 'foo' instead
nil
end
CRYSTAL
end
end
end
end
end

View File

@ -1,19 +1,19 @@
require "../../../spec_helper" require "../../../spec_helper"
module Ameba module Ameba
subject = Rule::Style::TypeNames.new subject = Rule::Naming::TypeNames.new
private def it_reports_name(type, name, expected, *, file = __FILE__, line = __LINE__) private def it_reports_name(type, name, expected, *, file = __FILE__, line = __LINE__)
it "reports type name #{expected}", file, line do it "reports type name #{expected}", file, line do
rule = Rule::Style::TypeNames.new rule = Rule::Naming::TypeNames.new
expect_issue rule, <<-CRYSTAL, type: type, name: name, file: file, line: line expect_issue rule, <<-CRYSTAL, type: type, name: name, file: file, line: line
%{type} %{name}; end %{type} %{name}; end
# ^{type}^{name}^^^^ error: Type name should be camelcased: #{expected}, but it was %{name} _{type} # ^{name} error: Type name should be camelcased: #{expected}, but it was %{name}
CRYSTAL CRYSTAL
end end
end end
describe Rule::Style::TypeNames do describe Rule::Naming::TypeNames do
it "passes if type names are camelcased" do it "passes if type names are camelcased" do
expect_no_issues subject, <<-CRYSTAL expect_no_issues subject, <<-CRYSTAL
class ParseError < Exception class ParseError < Exception
@ -46,7 +46,7 @@ module Ameba
it "reports alias name" do it "reports alias name" do
expect_issue subject, <<-CRYSTAL expect_issue subject, <<-CRYSTAL
alias Numeric_value = Int32 alias Numeric_value = Int32
# ^{} error: Type name should be camelcased: NumericValue, but it was Numeric_value # ^^^^^^^^^^^^^ error: Type name should be camelcased: NumericValue, but it was Numeric_value
CRYSTAL CRYSTAL
end end
end end

View File

@ -1,11 +1,11 @@
require "../../../spec_helper" require "../../../spec_helper"
module Ameba module Ameba
subject = Rule::Style::VariableNames.new subject = Rule::Naming::VariableNames.new
private def it_reports_var_name(name, value, expected, *, file = __FILE__, line = __LINE__) private def it_reports_var_name(name, value, expected, *, file = __FILE__, line = __LINE__)
it "reports variable name #{expected}", file, line do it "reports variable name #{expected}", file, line do
rule = Rule::Style::VariableNames.new rule = Rule::Naming::VariableNames.new
expect_issue rule, <<-CRYSTAL, name: name, file: file, line: line expect_issue rule, <<-CRYSTAL, name: name, file: file, line: line
%{name} = #{value} %{name} = #{value}
# ^{name} error: Var name should be underscore-cased: #{expected}, not %{name} # ^{name} error: Var name should be underscore-cased: #{expected}, not %{name}
@ -13,7 +13,7 @@ module Ameba
end end
end end
describe Rule::Style::VariableNames do describe Rule::Naming::VariableNames do
it "passes if var names are underscore-cased" do it "passes if var names are underscore-cased" do
expect_no_issues subject, <<-CRYSTAL expect_no_issues subject, <<-CRYSTAL
class Greeting class Greeting

View File

@ -48,7 +48,7 @@ module Ameba::Rule::Performance
context "properties" do context "properties" do
it "#filter_names" do it "#filter_names" do
rule = AnyAfterFilter.new rule = AnyAfterFilter.new
rule.filter_names = %w(select) rule.filter_names = %w[select]
expect_no_issues rule, <<-CRYSTAL expect_no_issues rule, <<-CRYSTAL
[1, 2, 3].reject { |e| e > 2 }.any? [1, 2, 3].reject { |e| e > 2 }.any?

View File

@ -46,7 +46,7 @@ module Ameba::Rule::Performance
context "properties" do context "properties" do
it "#call_names" do it "#call_names" do
rule = ChainedCallWithNoBang.new rule = ChainedCallWithNoBang.new
rule.call_names = %w(uniq) rule.call_names = %w[uniq]
expect_no_issues rule, <<-CRYSTAL expect_no_issues rule, <<-CRYSTAL
[1, 2, 3].select { |e| e > 2 }.reverse [1, 2, 3].select { |e| e > 2 }.reverse

View File

@ -6,7 +6,6 @@ module Ameba::Rule::Performance
describe ExcessiveAllocations do describe ExcessiveAllocations do
it "passes if there is no potential performance improvements" do it "passes if there is no potential performance improvements" do
expect_no_issues subject, <<-CRYSTAL expect_no_issues subject, <<-CRYSTAL
"Alice".chars.each
"Alice".chars.each(arg) { |c| puts c } "Alice".chars.each(arg) { |c| puts c }
"Alice".chars(arg).each { |c| puts c } "Alice".chars(arg).each { |c| puts c }
"Alice\nBob".lines.each(arg) { |l| puts l } "Alice\nBob".lines.each(arg) { |l| puts l }

View File

@ -64,7 +64,7 @@ module Ameba::Rule::Performance
context "properties" do context "properties" do
it "#filter_names" do it "#filter_names" do
rule = FirstLastAfterFilter.new rule = FirstLastAfterFilter.new
rule.filter_names = %w(reject) rule.filter_names = %w[reject]
expect_no_issues rule, <<-CRYSTAL expect_no_issues rule, <<-CRYSTAL
[1, 2, 3].select { |e| e > 2 }.first [1, 2, 3].select { |e| e > 2 }.first

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

@ -46,7 +46,7 @@ module Ameba::Rule::Performance
context "properties" do context "properties" do
it "#filter_names" do it "#filter_names" do
rule = SizeAfterFilter.new rule = SizeAfterFilter.new
rule.filter_names = %w(select) rule.filter_names = %w[select]
expect_no_issues rule, <<-CRYSTAL expect_no_issues rule, <<-CRYSTAL
[1, 2, 3].reject(&.empty?).size [1, 2, 3].reject(&.empty?).size

View File

@ -44,7 +44,7 @@ module Ameba::Rule::Style
context "properties" do context "properties" do
it "#filter_names" do it "#filter_names" do
rule = IsAFilter.new rule = IsAFilter.new
rule.filter_names = %w(select) rule.filter_names = %w[select]
expect_no_issues rule, <<-CRYSTAL expect_no_issues rule, <<-CRYSTAL
[1, 2, nil].reject(&.nil?) [1, 2, nil].reject(&.nil?)

View File

@ -123,7 +123,7 @@ module Ameba
it "#int_min_digits" do it "#int_min_digits" do
rule = Rule::Style::LargeNumbers.new rule = Rule::Style::LargeNumbers.new
rule.int_min_digits = 10 rule.int_min_digits = 10
expect_no_issues rule, %q(1200000) expect_no_issues rule, "1200000"
end end
end end
end end

View File

@ -4,7 +4,7 @@ module Ameba::Rule::Style
subject = ParenthesesAroundCondition.new subject = ParenthesesAroundCondition.new
describe ParenthesesAroundCondition do describe ParenthesesAroundCondition do
{% for keyword in %w(if unless while until) %} {% for keyword in %w[if unless while until] %}
context "{{ keyword.id }}" do context "{{ keyword.id }}" do
it "reports if redundant parentheses are found" do it "reports if redundant parentheses are found" do
source = expect_issue subject, <<-CRYSTAL, keyword: {{ keyword }} source = expect_issue subject, <<-CRYSTAL, keyword: {{ keyword }}

View File

@ -95,7 +95,7 @@ module Ameba
end end
it "does not run other rules" do it "does not run other rules" do
rules = [Rule::Lint::Syntax.new, Rule::Style::ConstantNames.new] of Rule::Base rules = [Rule::Lint::Syntax.new, Rule::Naming::ConstantNames.new] of Rule::Base
source = Source.new <<-CRYSTAL source = Source.new <<-CRYSTAL
MyBadConstant = 1 MyBadConstant = 1

View File

@ -22,23 +22,23 @@ module Ameba
DELIMITER_START STRING INTERPOLATION_START NUMBER } DELIMITER_END EOF DELIMITER_START STRING INTERPOLATION_START NUMBER } DELIMITER_END EOF
) )
it_tokenizes %(%w(1 2)), it_tokenizes %(%w[1 2]),
%w(STRING_ARRAY_START STRING STRING STRING_ARRAY_END EOF) %w[STRING_ARRAY_START STRING STRING STRING_ARRAY_END EOF]
it_tokenizes %(%i(one two)), it_tokenizes %(%i[one two]),
%w(SYMBOL_ARRAY_START STRING STRING STRING_ARRAY_END EOF) %w[SYMBOL_ARRAY_START STRING STRING STRING_ARRAY_END EOF]
it_tokenizes %( it_tokenizes %(
class A class A
def method def method
puts "hello" puts "hello"
end
end end
), %w( end
), %w[
NEWLINE SPACE IDENT SPACE CONST NEWLINE SPACE IDENT SPACE IDENT NEWLINE SPACE IDENT SPACE CONST NEWLINE SPACE IDENT SPACE IDENT
NEWLINE SPACE IDENT SPACE DELIMITER_START STRING DELIMITER_END NEWLINE SPACE IDENT SPACE DELIMITER_START STRING DELIMITER_END
NEWLINE SPACE IDENT NEWLINE SPACE IDENT NEWLINE SPACE EOF NEWLINE SPACE IDENT NEWLINE SPACE IDENT NEWLINE SPACE EOF
) ]
end end
end end
end end

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

@ -0,0 +1,2 @@
Lint/ComparisonToBoolean:
Enabled: true

View File

@ -6,7 +6,7 @@ module Ameba
# Dummy Rule which does nothing. # Dummy Rule which does nothing.
class DummyRule < Rule::Base class DummyRule < Rule::Base
properties do properties do
description : String = "Dummy rule that does nothing." description "Dummy rule that does nothing."
dummy true dummy true
end end
@ -92,7 +92,7 @@ module Ameba
class PerfRule < Rule::Performance::Base class PerfRule < Rule::Performance::Base
properties do properties do
description : String = "Sample performance rule" description "Sample performance rule"
end end
def test(source) def test(source)
@ -259,6 +259,7 @@ module Ameba
Crystal::MacroLiteral, Crystal::MacroLiteral,
Crystal::Expressions, Crystal::Expressions,
Crystal::ControlExpression, Crystal::ControlExpression,
Crystal::Call,
} }
def initialize(node) def initialize(node)
@ -282,6 +283,13 @@ module Ameba
end end
end end
def with_presenter(klass, &)
io = IO::Memory.new
presenter = klass.new(io)
yield presenter, io
end
def as_node(source) def as_node(source)
Crystal::Parser.new(source).parse Crystal::Parser.new(source).parse
end end

View File

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

View File

@ -1,3 +1,5 @@
require "./util"
module Ameba::AST module Ameba::AST
# Represents the branch in Crystal code. # Represents the branch in Crystal code.
# Branch is a part of a branchable statement. # Branch is a part of a branchable statement.
@ -67,6 +69,8 @@ module Ameba::AST
# :nodoc: # :nodoc:
private class BranchVisitor < Crystal::Visitor private class BranchVisitor < Crystal::Visitor
include Util
@current_branch : Crystal::ASTNode? @current_branch : Crystal::ASTNode?
property branchable : Branchable? property branchable : Branchable?
@ -79,7 +83,7 @@ module Ameba::AST
on_branchable_start(node, branches) on_branchable_start(node, branches)
end end
private def on_branchable_start(node, branches : Array | Tuple) private def on_branchable_start(node, branches : Enumerable)
@branchable = Branchable.new(node, @branchable) @branchable = Branchable.new(node, @branchable)
branches.each do |branch_node| branches.each do |branch_node|
@ -172,6 +176,18 @@ module Ameba::AST
def end_visit(node : Crystal::MacroFor) def end_visit(node : Crystal::MacroFor)
on_branchable_end node on_branchable_end node
end end
def visit(node : Crystal::Call)
if loop?(node) && (block = node.block)
on_branchable_start node, block.body
end
end
def end_visit(node : Crystal::Call)
if loop?(node) && node.block
on_branchable_end node
end
end
end end
end end
end end

View File

@ -34,9 +34,8 @@ module Ameba::AST
# The actual AST node that represents a current scope. # The actual AST node that represents a current scope.
getter node : Crystal::ASTNode getter node : Crystal::ASTNode
delegate to_s, to: node delegate location, end_location, to_s,
delegate location, to: node to: @node
delegate end_location, to: node
def_equals_and_hash node, location def_equals_and_hash node, location
@ -181,14 +180,19 @@ module Ameba::AST
@visibility || outer_scope.try(&.visibility) @visibility || outer_scope.try(&.visibility)
end end
# Returns `true` if current scope is a def, `false` otherwise. {% for type in %w[Def ClassDef ModuleDef EnumDef LibDef FunDef].map(&.id) %}
def def? {% method_name = type.underscore %}
node.is_a?(Crystal::Def) # Returns `true` if current scope is a {{ method_name[0..-5] }} def, `false` otherwise.
end def {{ method_name }}?(*, check_outer_scopes = false)
node.is_a?(Crystal::{{ type }}) ||
!!(check_outer_scopes &&
outer_scope.try(&.{{ method_name }}?(check_outer_scopes: true)))
end
{% end %}
# Returns `true` if this scope is a top level scope, `false` otherwise. # Returns `true` if this scope is a top level scope, `false` otherwise.
def top_level? def top_level?
outer_scope.nil? || type_definition? outer_scope.nil?
end end
# Returns `true` if var is an argument in current scope, `false` otherwise. # Returns `true` if var is an argument in current scope, `false` otherwise.

View File

@ -21,8 +21,8 @@ module Ameba::AST::Util
static_literal?(node.to)} static_literal?(node.to)}
when Crystal::ArrayLiteral, when Crystal::ArrayLiteral,
Crystal::TupleLiteral Crystal::TupleLiteral
{true, node.elements.all? do |el| {true, node.elements.all? do |element|
static_literal?(el) static_literal?(element)
end} end}
when Crystal::HashLiteral when Crystal::HashLiteral
{true, node.entries.all? do |entry| {true, node.entries.all? do |entry|

View File

@ -19,9 +19,8 @@ module Ameba::AST
# Variable of this argument (may be the same node) # Variable of this argument (may be the same node)
getter variable : Variable getter variable : Variable
delegate location, to: @node delegate location, end_location, to_s,
delegate end_location, to: @node to: @node
delegate to_s, to: @node
# Creates a new argument. # Creates a new argument.
# #

View File

@ -19,9 +19,8 @@ module Ameba::AST
# A scope assignment belongs to # A scope assignment belongs to
getter scope : Scope getter scope : Scope
delegate to_s, to: @node delegate location, end_location, to_s,
delegate location, to: @node to: @node
delegate end_location, to: @node
# Creates a new assignment. # Creates a new assignment.
# #
@ -32,9 +31,7 @@ module Ameba::AST
return unless scope = @variable.scope return unless scope = @variable.scope
@branch = Branch.of(@node, scope) @branch = Branch.of(@node, scope)
@referenced = true if @variable.special? || @referenced = true if @variable.special? || referenced_in_loop?
@variable.scope.type_definition? ||
referenced_in_loop?
end end
def referenced_in_loop? def referenced_in_loop?
@ -75,31 +72,5 @@ module Ameba::AST
node node
end end
end end
# Indicates whether the node is a transformed assignment by the compiler.
# i.e.
#
# ```
# collection.each do |(a, b)|
# puts b
# end
# ```
#
# is transformed to:
#
# ```
# collection.each do |__arg0|
# a = __arg0[0]
# b = __arg0[1]
# puts(b)
# end
# ```
def transformed?
return false unless (assign = node).is_a?(Crystal::Assign)
return false unless (value = assign.value).is_a?(Crystal::Call)
return false unless (obj = value.obj).is_a?(Crystal::Var)
obj.name.starts_with? "__arg"
end
end end
end end

View File

@ -2,10 +2,8 @@ module Ameba::AST
class InstanceVariable class InstanceVariable
getter node : Crystal::InstanceVar getter node : Crystal::InstanceVar
delegate location, to: @node delegate location, end_location, name, to_s,
delegate end_location, to: @node to: @node
delegate name, to: @node
delegate to_s, to: @node
def initialize(@node) def initialize(@node)
end end

View File

@ -2,9 +2,8 @@ module Ameba::AST
class TypeDecVariable class TypeDecVariable
getter node : Crystal::TypeDeclaration getter node : Crystal::TypeDeclaration
delegate location, to: @node delegate location, end_location, to_s,
delegate end_location, to: @node to: @node
delegate to_s, to: @node
def initialize(@node) def initialize(@node)
end end

View File

@ -17,10 +17,8 @@ module Ameba::AST
# Node of the first assignment which can be available before any reference. # Node of the first assignment which can be available before any reference.
getter assign_before_reference : Crystal::ASTNode? getter assign_before_reference : Crystal::ASTNode?
delegate location, to: @node delegate location, end_location, name, to_s,
delegate end_location, to: @node to: @node
delegate name, to: @node
delegate to_s, to: @node
# Creates a new variable(in the scope). # Creates a new variable(in the scope).
# #
@ -54,7 +52,7 @@ module Ameba::AST
# #
# ``` # ```
# variable = Variable.new(node, scope) # variable = Variable.new(node, scope)
# variable.reference(var_node) # variable.reference(var_node, some_scope)
# variable.referenced? # => true # variable.referenced? # => true
# ``` # ```
def referenced? def referenced?
@ -74,6 +72,11 @@ module Ameba::AST
end end
end end
# :ditto:
def reference(scope : Scope)
reference(node, scope)
end
# Reference variable's assignments. # Reference variable's assignments.
# #
# ``` # ```
@ -136,7 +139,7 @@ module Ameba::AST
case assign case assign
when Crystal::Assign then eql?(assign.target) when Crystal::Assign then eql?(assign.target)
when Crystal::OpAssign then eql?(assign.target) when Crystal::OpAssign then eql?(assign.target)
when Crystal::MultiAssign then assign.targets.any? { |t| eql?(t) } when Crystal::MultiAssign then assign.targets.any? { |target| eql?(target) }
when Crystal::UninitializedVar then eql?(assign.var) when Crystal::UninitializedVar then eql?(assign.var)
else else
false false
@ -208,9 +211,9 @@ module Ameba::AST
return if references.size > assignments.size return if references.size > assignments.size
return if assignments.any?(&.op_assign?) return if assignments.any?(&.op_assign?)
@assign_before_reference = assignments.find { |ass| @assign_before_reference = assignments
!ass.in_branch? .find(&.in_branch?.!)
}.try &.node .try(&.node)
end end
end end
end end

View File

@ -24,7 +24,7 @@ module Ameba::AST
# Uses the same logic than rubocop. See # Uses the same logic than rubocop. See
# https://github.com/rubocop-hq/rubocop/blob/master/lib/rubocop/cop/metrics/cyclomatic_complexity.rb#L21 # https://github.com/rubocop-hq/rubocop/blob/master/lib/rubocop/cop/metrics/cyclomatic_complexity.rb#L21
# Except "for", because crystal doesn't have a "for" loop. # Except "for", because crystal doesn't have a "for" loop.
{% for node in %i(if while until rescue or and) %} {% for node in %i[if while until rescue or and] %}
# :nodoc: # :nodoc:
def visit(node : Crystal::{{ node.id.capitalize }}) def visit(node : Crystal::{{ node.id.capitalize }})
@complexity += 1 unless macro_condition? @complexity += 1 unless macro_condition?

View File

@ -32,6 +32,7 @@ module Ameba::AST
IsA, IsA,
LibDef, LibDef,
ModuleDef, ModuleDef,
MultiAssign,
NilLiteral, NilLiteral,
StringInterpolation, StringInterpolation,
Unless, Unless,

View File

@ -43,7 +43,7 @@ module Ameba::AST
end end
private def traverse_case(node) private def traverse_case(node)
node.whens.each { |n| traverse_node n.body } node.whens.each { |when_node| traverse_node when_node.body }
traverse_node(node.else) traverse_node(node.else)
end end
@ -54,7 +54,7 @@ module Ameba::AST
private def traverse_exception_handler(node) private def traverse_exception_handler(node)
traverse_node node.body traverse_node node.body
traverse_node node.else traverse_node node.else
node.rescues.try &.each { |n| traverse_node n.body } node.rescues.try &.each { |rescue_node| traverse_node rescue_node.body }
end end
end end
end end

View File

@ -21,7 +21,6 @@ module Ameba::AST
} }
SPECIAL_NODE_NAMES = %w[super previous_def] SPECIAL_NODE_NAMES = %w[super previous_def]
RECORD_NODE_NAME = "record"
@scope_queue = [] of Scope @scope_queue = [] of Scope
@current_scope : Scope @current_scope : Scope
@ -154,7 +153,7 @@ module Ameba::AST
# :nodoc: # :nodoc:
def visit(node : Crystal::Var) def visit(node : Crystal::Var)
variable = @current_scope.find_variable node.name variable = @current_scope.find_variable(node.name)
case case
when @current_scope.arg?(node) # node is an argument when @current_scope.arg?(node) # node is an argument
@ -162,7 +161,7 @@ module Ameba::AST
when variable.nil? && @current_assign # node is a variable when variable.nil? && @current_assign # node is a variable
@current_scope.add_variable(node) @current_scope.add_variable(node)
when variable # node is a reference when variable # node is a reference
reference = variable.reference node, @current_scope reference = variable.reference(node, @current_scope)
if @current_assign.is_a?(Crystal::OpAssign) || !reference.target_of?(@current_assign) if @current_assign.is_a?(Crystal::OpAssign) || !reference.target_of?(@current_assign)
variable.reference_assignments! variable.reference_assignments!
end end
@ -171,26 +170,39 @@ module Ameba::AST
# :nodoc: # :nodoc:
def visit(node : Crystal::Call) def visit(node : Crystal::Call)
case scope = @current_scope
when @current_scope.def?
if node.name.in?(SPECIAL_NODE_NAMES) && node.args.empty?
@current_scope.arguments.each do |arg|
variable = arg.variable
ref = variable.reference(variable.node, @current_scope) case
ref.explicit = false when (scope.top_level? || scope.type_definition?) && record_macro?(node)
end return false
when scope.type_definition? && accessor_macro?(node)
return false
when scope.def? && special_node?(node)
scope.arguments.each do |arg|
ref = arg.variable.reference(scope)
ref.explicit = false
end end
true
when @current_scope.top_level? && record_macro?(node)
false
else
true
end end
true
end
private def special_node?(node)
node.name.in?(SPECIAL_NODE_NAMES) && node.args.empty?
end
private def accessor_macro?(node)
node.name.matches? /^(class_)?(getter[?!]?|setter|property[?!]?)$/
end end
private def record_macro?(node) private def record_macro?(node)
node.name == RECORD_NODE_NAME && node.args.first?.is_a?(Crystal::Path) return false unless node.name == "record"
case node.args.first?
when Crystal::Path, Crystal::Generic
true
else
false
end
end end
private def skip?(node) private def skip?(node)

View File

@ -28,7 +28,14 @@ module Ameba::Cli
configure_rules(config, opts) configure_rules(config, opts)
if opts.rules? 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 end
runner = Ameba.run(config) runner = Ameba.run(config)
@ -49,6 +56,7 @@ module Ameba::Cli
property globs : Array(String)? property globs : Array(String)?
property only : Array(String)? property only : Array(String)?
property except : Array(String)? property except : Array(String)?
property describe_rule : 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? skip_reading_config = false property? skip_reading_config = false
@ -67,11 +75,11 @@ module Ameba::Cli
parser.on("-h", "--help", "Show this help") { print_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("-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 |arr|
if f.size == 1 && f.first =~ /.+:\d+:\d+/ if arr.size == 1 && arr.first.matches?(/.+:\d+:\d+/)
configure_explain_opts(f.first, opts) configure_explain_opts(arr.first, opts)
else else
opts.globs = f unless f.empty? opts.globs = arr unless arr.empty?
end end
end end
@ -119,6 +127,11 @@ module Ameba::Cli
configure_explain_opts(loc, opts) configure_explain_opts(loc, opts)
end 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", parser.on("--without-affected-code",
"Stop showing affected code while using a default formatter") do "Stop showing affected code while using a default formatter") do
opts.without_affected_code = true opts.without_affected_code = true
@ -152,6 +165,11 @@ module Ameba::Cli
opts.without_affected_code? opts.without_affected_code?
end 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) private def configure_explain_opts(loc, opts)
location_to_explain = parse_explain_location(loc) location_to_explain = parse_explain_location(loc)
opts.location_to_explain = location_to_explain opts.location_to_explain = location_to_explain
@ -183,14 +201,13 @@ module Ameba::Cli
exit 0 exit 0
end end
private def print_rules(config) private def describe_rule(rule)
config.rules.each do |rule| Presenter::RulePresenter.new.run(rule)
puts "%s [%s] - %s" % { exit 0
rule.name.colorize(:white), end
rule.severity.symbol.to_s.colorize(:green),
rule.description.colorize(:dark_gray), private def print_rules(rules)
} Presenter::RuleCollectionPresenter.new.run(rules)
end
exit 0 exit 0
end end
end end

View File

@ -97,8 +97,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)
return unless formatter_name = load_formatter_name(config) if formatter_name = load_formatter_name(config)
self.formatter = formatter_name self.formatter = formatter_name
end
end end
# Loads YAML configuration file by `path`. # Loads YAML configuration file by `path`.
@ -115,12 +116,13 @@ class Ameba::Config
end end
Config.new YAML.parse(content) Config.new YAML.parse(content)
rescue e rescue e
raise "Config file is invalid: #{e.message}" raise "Unable to load config file: #{e.message}"
end end
protected def self.read_config(path = nil) protected def self.read_config(path = nil)
if path if path
return File.exists?(path) ? File.read(path) : nil return File.read(path) if File.exists?(path)
raise "Config file does not exist"
end end
each_config_path do |config_path| each_config_path do |config_path|
return File.read(config_path) if File.exists?(config_path) return File.read(config_path) if File.exists?(config_path)
@ -202,13 +204,13 @@ class Ameba::Config
# #
# ``` # ```
# config = Ameba::Config.load # config = Ameba::Config.load
# config.update_rules %w(Rule1 Rule2), enabled: true # config.update_rules %w[Rule1 Rule2], enabled: true
# ``` # ```
# #
# also it allows to update groups of rules: # also it allows to update groups of rules:
# #
# ``` # ```
# 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, enabled = true, excluded = nil)
names.try &.each do |name| names.try &.each do |name|
@ -243,20 +245,20 @@ class Ameba::Config
# Define rule properties # Define rule properties
macro properties(&block) macro properties(&block)
{% definitions = [] of NamedTuple %} {% definitions = [] of NamedTuple %}
{% if block.body.is_a? Assign %} {% if (prop = block.body).is_a? Call %}
{% definitions << {var: block.body.target, value: block.body.value} %} {% if (named_args = prop.named_args) && (type = named_args.select(&.name.== "as".id).first) %}
{% elsif block.body.is_a? Call %} {% definitions << {var: prop.name, value: prop.args.first, type: type.value} %}
{% definitions << {var: block.body.name, value: block.body.args.first} %} {% else %}
{% elsif block.body.is_a? TypeDeclaration %} {% definitions << {var: prop.name, value: prop.args.first} %}
{% definitions << {var: block.body.var, value: block.body.value, type: block.body.type} %} {% end %}
{% elsif block.body.is_a? Expressions %} {% elsif block.body.is_a? Expressions %}
{% for prop in block.body.expressions %} {% for prop in block.body.expressions %}
{% if prop.is_a? Assign %} {% if prop.is_a? Call %}
{% definitions << {var: prop.target, value: prop.value} %} {% if (named_args = prop.named_args) && (type = named_args.select(&.name.== "as".id).first) %}
{% elsif prop.is_a? Call %} {% definitions << {var: prop.name, value: prop.args.first, type: type.value} %}
{% definitions << {var: prop.name, value: prop.args.first} %} {% else %}
{% elsif prop.is_a? TypeDeclaration %} {% definitions << {var: prop.name, value: prop.args.first} %}
{% definitions << {var: prop.var, value: prop.value, type: prop.type} %} {% end %}
{% end %} {% end %}
{% end %} {% end %}
{% end %} {% end %}
@ -322,9 +324,10 @@ class Ameba::Config
macro included macro included
GROUP_SEVERITY = { GROUP_SEVERITY = {
Lint: Ameba::Severity::Warning, Documentation: Ameba::Severity::Warning,
Metrics: Ameba::Severity::Warning, Lint: Ameba::Severity::Warning,
Performance: Ameba::Severity::Warning, Metrics: Ameba::Severity::Warning,
Performance: Ameba::Severity::Warning,
} }
class_getter default_severity : Ameba::Severity do class_getter default_severity : Ameba::Severity do

View File

@ -17,13 +17,13 @@ module Ameba::Formatter
# A list of sources to inspect is passed as an argument. # A list of sources to inspect is passed as an argument.
def started(sources); end def started(sources); end
# Callback that indicates when source inspection is finished. # Callback that indicates when source inspection is started.
# A corresponding source is passed as an argument. # A corresponding source is passed as an argument.
def source_finished(source : Source); end def source_started(source : Source); end
# Callback that indicates when source inspection is finished. # Callback that indicates when source inspection is finished.
# A corresponding source is passed as an argument. # A corresponding source is passed as an argument.
def source_started(source : Source); end def source_finished(source : Source); end
# Callback that indicates when inspection is finished. # Callback that indicates when inspection is finished.
# A list of inspected sources is passed as an argument. # A list of inspected sources is passed as an argument.

View File

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

View File

@ -3,7 +3,7 @@ module Ameba::Formatter
# Basically, it takes all issues reported and disables corresponding rules # Basically, it takes all issues reported and disables corresponding rules
# or excludes failed sources from these rules. # or excludes failed sources from these rules.
class TODOFormatter < DotFormatter class TODOFormatter < DotFormatter
def initialize(@output = STDOUT) def initialize(@output = STDOUT, @config_path : Path = Config::DEFAULT_PATH)
end end
def finished(sources) def finished(sources)
@ -26,25 +26,30 @@ module Ameba::Formatter
end end
private def generate_todo_config(issues) private def generate_todo_config(issues)
file = File.new(Config::DEFAULT_PATH, mode: "w") File.open(@config_path, mode: "w") do |file|
file << header file << header
rule_issues_map(issues).each do |rule, rule_issues|
file << "\n# Problems found: #{rule_issues.size}" rule_issues_map(issues).each do |rule, rule_issues|
file << "\n# Run `ameba --only #{rule.name}` for details" rule_todo = rule_todo(rule, rule_issues)
file << rule_todo(rule, rule_issues).gsub("---", "") rule_todo =
{rule_todo.name => rule_todo}
.to_yaml.gsub("---", "")
file << "\n# Problems found: #{rule_issues.size}"
file << "\n# Run `ameba --only #{rule.name}` for details"
file << rule_todo
end
file
end end
file
ensure
file.close if file
end end
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 |hash|
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)
next if issue.correctable? && config[:autocorrect]? next if issue.correctable? && config[:autocorrect]?
(h[issue.rule] ||= Array(Issue).new) << issue (hash[issue.rule] ||= Array(Issue).new) << issue
end end
end end
end end
@ -60,11 +65,11 @@ module Ameba::Formatter
end end
private def rule_todo(rule, issues) private def rule_todo(rule, issues)
rule.excluded = issues rule.dup.tap do |rule_todo|
.compact_map(&.location.try &.filename.try &.to_s) rule_todo.excluded = issues
.uniq! .compact_map(&.location.try &.filename.try &.to_s)
.uniq!
{rule.name => rule}.to_yaml end
end end
end end
end end

View File

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

@ -1,6 +1,10 @@
require "./ast/util"
module Ameba module Ameba
# Represents a module used to report issues. # Represents a module used to report issues.
module Reportable module Reportable
include AST::Util
# List of reported issues. # List of reported issues.
getter issues = [] of Issue getter issues = [] of Issue
@ -30,13 +34,19 @@ module Ameba
end end
# Adds a new issue for Crystal AST *node*. # Adds a new issue for Crystal AST *node*.
def add_issue(rule, node : Crystal::ASTNode, message, status : Issue::Status? = nil, block : (Source::Corrector ->)? = nil) : Issue def add_issue(rule, node : Crystal::ASTNode, message, status : Issue::Status? = nil, block : (Source::Corrector ->)? = nil, *, prefer_name_location = false) : Issue
add_issue rule, node.location, node.end_location, message, status, block location = name_location(node) if prefer_name_location
location ||= node.location
end_location = name_end_location(node) if prefer_name_location
end_location ||= node.end_location
add_issue rule, location, end_location, message, status, block
end end
# :ditto: # :ditto:
def add_issue(rule, node : Crystal::ASTNode, message, status : Issue::Status? = nil, &block : Source::Corrector ->) : Issue def add_issue(rule, node : Crystal::ASTNode, message, status : Issue::Status? = nil, *, prefer_name_location = false, &block : Source::Corrector ->) : Issue
add_issue rule, node, message, status, block add_issue rule, node, message, status, block, prefer_name_location: prefer_name_location
end end
# Adds a new issue for Crystal *token*. # Adds a new issue for Crystal *token*.

View File

@ -32,14 +32,15 @@ module Ameba::Rule
# 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
# that are tested by this rule, it should add an issue. # that are tested by this rule, it should add an issue.
# #
# Be default it uses a node visitor to traverse all the nodes in the source. # By default it uses a node visitor to traverse all the nodes in the source.
#
# NOTE: Must be overridden for other type of rules. # NOTE: Must be overridden for other type of rules.
def test(source : Source) def test(source : Source)
AST::NodeVisitor.new self, source AST::NodeVisitor.new self, source
end end
# NOTE: Can't be abstract
def test(source : Source, node : Crystal::ASTNode, *opts) def test(source : Source, node : Crystal::ASTNode, *opts)
# can't be abstract
end end
# A convenient addition to `#test` method that does the same # A convenient addition to `#test` method that does the same
@ -114,7 +115,7 @@ module Ameba::Rule
# Adds an issue to the *source* # Adds an issue to the *source*
macro issue_for(*args, **kwargs, &block) macro issue_for(*args, **kwargs, &block)
source.add_issue(self, {{ *args }}, {{ **kwargs }}) {{ block }} source.add_issue(self, {{ args.splat }}, {{ kwargs.double_splat }}) {{ block }}
end end
protected def self.rule_name protected def self.rule_name

View File

@ -1,11 +1,11 @@
module Ameba::Rule::Lint module Ameba::Rule::Documentation
# A rule that enforces documentation for public types: # A rule that enforces documentation for public types:
# modules, classes, enums, methods and macros. # modules, classes, enums, methods and macros.
# #
# YAML configuration example: # YAML configuration example:
# #
# ``` # ```
# Lint/Documentation: # Documentation/Documentation:
# Enabled: true # Enabled: true
# IgnoreClasses: false # IgnoreClasses: false
# IgnoreModules: true # IgnoreModules: true

View File

@ -0,0 +1,96 @@
module Ameba::Rule::Documentation
# 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:
#
# ```
# Documentation/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

@ -28,7 +28,7 @@ module Ameba::Rule::Lint
end end
MSG = "Comparison to a boolean is pointless" MSG = "Comparison to a boolean is pointless"
OP_NAMES = %w(== != ===) OP_NAMES = %w[== != ===]
def test(source, node : Crystal::Call) def test(source, node : Crystal::Call)
return unless node.name.in?(OP_NAMES) return unless node.name.in?(OP_NAMES)

View File

@ -18,7 +18,7 @@ module Ameba::Rule::Lint
class DebugCalls < Base class DebugCalls < Base
properties do properties do
description "Disallows debug-related calls" description "Disallows debug-related calls"
method_names %w(p p! pp pp!) method_names %w[p p! pp pp!]
end end
MSG = "Possibly forgotten debug-related `%s` call detected" MSG = "Possibly forgotten debug-related `%s` call detected"

View File

@ -28,8 +28,6 @@ module Ameba::Rule::Lint
# Enabled: true # Enabled: true
# ``` # ```
class EmptyExpression < Base class EmptyExpression < Base
include AST::Util
properties do properties do
description "Disallows empty expressions" description "Disallows empty expressions"
end end

View File

@ -23,7 +23,7 @@ module Ameba::Rule::Lint
description "Identifies comparisons between literals" description "Identifies comparisons between literals"
end end
OP_NAMES = %w(=== == !=) OP_NAMES = %w[=== == !=]
MSG = "Comparison always evaluates to %s" MSG = "Comparison always evaluates to %s"
MSG_LIKELY = "Comparison most likely evaluates to %s" MSG_LIKELY = "Comparison most likely evaluates to %s"
@ -36,14 +36,15 @@ module Ameba::Rule::Lint
arg_is_literal, arg_is_static = literal_kind?(arg) arg_is_literal, arg_is_static = literal_kind?(arg)
return unless obj_is_literal && arg_is_literal return unless obj_is_literal && arg_is_literal
return unless obj.to_s == arg.to_s
is_dynamic = !obj_is_static || !arg_is_static is_dynamic = !obj_is_static || !arg_is_static
what = what =
case node.name case node.name
when "===" then "the same" when "===" then "the same"
when "==" then (obj.to_s == arg.to_s).to_s when "==" then "true"
when "!=" then (obj.to_s != arg.to_s).to_s when "!=" then "false"
end end
issue_for node, (is_dynamic ? MSG_LIKELY : MSG) % what issue_for node, (is_dynamic ? MSG_LIKELY : MSG) % what

View File

@ -20,8 +20,6 @@ module Ameba::Rule::Lint
# Enabled: true # Enabled: true
# ``` # ```
class MissingBlockArgument < Base class MissingBlockArgument < Base
include AST::Util
properties do properties do
description "Disallows yielding method definitions without block argument" description "Disallows yielding method definitions without block argument"
end end
@ -36,10 +34,7 @@ module Ameba::Rule::Lint
def test(source, node : Crystal::Def, scope : AST::Scope) def test(source, node : Crystal::Def, scope : AST::Scope)
return if !scope.yields? || node.block_arg return if !scope.yields? || node.block_arg
return unless location = node.name_location issue_for node, MSG, prefer_name_location: true
end_location = name_end_location(node)
issue_for location, end_location, MSG
end end
end end
end end

View File

@ -26,27 +26,21 @@ module Ameba::Rule::Lint
# Enabled: true # Enabled: true
# ``` # ```
class NotNil < Base class NotNil < Base
include AST::Util
properties do properties do
description "Identifies usage of `not_nil!` calls" description "Identifies usage of `not_nil!` calls"
end end
NOT_NIL_NAME = "not_nil!" MSG = "Avoid using `not_nil!`"
MSG = "Avoid using `not_nil!`"
def test(source) def test(source)
AST::NodeVisitor.new self, source, skip: :macro AST::NodeVisitor.new self, source, skip: :macro
end end
def test(source, node : Crystal::Call) 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 node.obj && node.args.empty?
return unless name_location = node.name_location issue_for node, MSG, prefer_name_location: true
return unless end_location = name_end_location(node)
issue_for name_location, end_location, MSG
end end
end end
end end

View File

@ -1,17 +1,17 @@
module Ameba::Rule::Lint module Ameba::Rule::Lint
# This rule is used to identify usage of `index/rindex/find` calls # This rule is used to identify usage of `index/rindex/find/match` calls
# followed by a call to `not_nil!`. # followed by a call to `not_nil!`.
# #
# For example, this is considered a code smell: # For example, this is considered a code smell:
# #
# ``` # ```
# %w[Alice Bob].find(&.match(/^A./)).not_nil! # %w[Alice Bob].find(&.chars.any?(&.in?('o', 'b'))).not_nil!
# ``` # ```
# #
# And can be written as this: # And can be written as this:
# #
# ``` # ```
# %w[Alice Bob].find!(&.match(/^A./)) # %w[Alice Bob].find!(&.chars.any?(&.in?('o', 'b')))
# ``` # ```
# #
# YAML configuration example: # YAML configuration example:
@ -24,25 +24,24 @@ module Ameba::Rule::Lint
include AST::Util include AST::Util
properties do properties do
description "Identifies usage of `index/rindex/find` calls followed by `not_nil!`" description "Identifies usage of `index/rindex/find/match` calls followed by `not_nil!`"
end end
BLOCK_CALL_NAMES = %w(index rindex find) MSG = "Use `%s! {...}` instead of `%s {...}.not_nil!`"
CALL_NAMES = %w(index rindex)
NOT_NIL_NAME = "not_nil!" BLOCK_CALL_NAMES = %w[index rindex find]
MSG = "Use `%s! {...}` instead of `%s {...}.not_nil!`" CALL_NAMES = %w[index rindex match]
def test(source) def test(source)
AST::NodeVisitor.new self, source, skip: :macro AST::NodeVisitor.new self, source, skip: :macro
end end
def test(source, node : Crystal::Call) 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 = node.obj).is_a?(Crystal::Call)
return unless obj.name.in?(obj.block ? BLOCK_CALL_NAMES : CALL_NAMES) return unless obj.name.in?(obj.block ? BLOCK_CALL_NAMES : CALL_NAMES)
return unless name_location = obj.name_location return unless name_location = name_location(obj)
return unless name_location_end = name_end_location(obj) return unless name_location_end = name_end_location(obj)
return unless end_location = name_end_location(node) return unless end_location = name_end_location(node)

View File

@ -4,15 +4,15 @@ module Ameba::Rule::Lint
# For example, this is usually written by mistake: # For example, this is usually written by mistake:
# #
# ``` # ```
# %i(:one, :two) # %i[:one, :two]
# %w("one", "two") # %w["one", "two"]
# ``` # ```
# #
# And the expected example is: # And the expected example is:
# #
# ``` # ```
# %i(one two) # %i[one two]
# %w(one two) # %w[one two]
# ``` # ```
# #
# YAML configuration example: # YAML configuration example:
@ -42,7 +42,7 @@ module Ameba::Rule::Lint
start_token = token.dup start_token = token.dup
when .string? when .string?
if (_start = start_token) && !issue if (_start = start_token) && !issue
issue = array_entry_invalid?(token.value, _start.raw) issue = array_entry_invalid?(token.value.to_s, _start.raw)
end end
when .string_array_end? when .string_array_end?
if (_start = start_token) && (_issue = issue) if (_start = start_token) && (_issue = issue)
@ -63,7 +63,7 @@ module Ameba::Rule::Lint
end end
private def check_array_entry(entry, symbols, literal) private def check_array_entry(entry, symbols, literal)
MSG % {symbols, literal} if entry =~ /[#{symbols}]/ MSG % {symbols, literal} if entry.matches?(/[#{Regex.escape(symbols)}]/)
end end
end end
end end

View File

@ -30,8 +30,8 @@ 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 do |n| string_coercion_nodes(node).each do |expr|
issue_for n.name_location, n.end_location, MSG issue_for name_location(expr), expr.end_location, MSG
end end
end end

View File

@ -51,7 +51,7 @@ module Ameba::Rule::Lint
end end
private def report(source, node, msg) private def report(source, node, msg)
issue_for node.name_location, node.name_end_location, msg issue_for node, msg, prefer_name_location: true
end end
end end
end end

View File

@ -40,7 +40,7 @@ module Ameba::Rule::Lint
!(block = node.block) || !(block = node.block) ||
with_index_arg?(block) with_index_arg?(block)
issue_for node.name_location, node.name_end_location, MSG issue_for node, MSG, prefer_name_location: true
end end
private def with_index_arg?(block : Crystal::Block) private def with_index_arg?(block : Crystal::Block)

View File

@ -53,13 +53,16 @@ module Ameba::Rule::Lint
return unless outer_scope = scope.outer_scope return unless outer_scope = scope.outer_scope
scope.arguments.reject(&.ignored?).each do |arg| scope.arguments.reject(&.ignored?).each do |arg|
variable = outer_scope.find_variable(arg.name) # TODO: handle unpacked variables from `Block#unpacks`
next unless name = arg.name.presence
variable = outer_scope.find_variable(name)
next if variable.nil? || !variable.declared_before?(arg) next if variable.nil? || !variable.declared_before?(arg)
next if outer_scope.assigns_ivar?(arg.name) next if outer_scope.assigns_ivar?(name)
next if outer_scope.assigns_type_dec?(arg.name) next if outer_scope.assigns_type_dec?(name)
issue_for arg.node, MSG % arg.name issue_for arg.node, MSG % name
end end
end end
end end

View File

@ -0,0 +1,50 @@
require "file_utils"
module Ameba::Rule::Lint
# A rule that enforces spec filenames to have `_spec` suffix.
#
# YAML configuration example:
#
# ```
# Lint/SpecFilename:
# Enabled: true
# ```
class SpecFilename < Base
properties do
description "Enforces spec filenames to have `_spec` suffix"
ignored_dirs %w[spec/support spec/fixtures spec/data]
ignored_filenames %w[spec_helper]
end
MSG = "Spec filename should have `_spec` suffix: %s.cr, not %s.cr"
private LOCATION = {1, 1}
# TODO: fix the assumption that *source.path* contains relative path
def test(source : Source)
path_ = Path[source.path].to_posix
name = path_.stem
path = path_.to_s
# check files only within spec/ directory
return unless path.starts_with?("spec/")
# ignore files having `_spec` suffix
return if name.ends_with?("_spec")
# ignore known false-positives
ignored_dirs.each do |substr|
return if path.starts_with?("#{substr}/")
end
return if name.in?(ignored_filenames)
expected = "#{name}_spec"
issue_for LOCATION, MSG % {expected, name} do
new_path =
path_.sibling(expected + path_.extension)
FileUtils.mv(path, new_path)
end
end
end
end

View File

@ -49,8 +49,9 @@ module Ameba::Rule::Lint
description "Reports focused spec items" description "Reports focused spec items"
end end
MSG = "Focused spec item detected" MSG = "Focused spec item detected"
SPEC_ITEM_NAMES = %w(describe context it pending)
SPEC_ITEM_NAMES = %w[describe context it pending]
def test(source) def test(source)
return unless source.spec? return unless source.spec?

View File

@ -0,0 +1,97 @@
module Ameba::Rule::Lint
# A rule that reports typos found in source files.
#
# NOTE: Needs [typos](https://github.com/crate-ci/typos) CLI tool.
# NOTE: See the chapter on [false positives](https://github.com/crate-ci/typos#false-positives).
#
# YAML configuration example:
#
# ```
# Lint/Typos:
# Enabled: true
# BinPath: ~
# FailOnError: false
# ```
class Typos < Base
properties do
description "Reports typos found in source files"
bin_path nil, as: String?
fail_on_error false
end
MSG = "Typo found: %s -> %s"
BIN_PATH = Process.find_executable("typos")
def bin_path : String?
@bin_path || BIN_PATH
end
def test(source : Source)
typos = typos_from(source)
typos.try &.each do |typo|
corrections = typo.corrections
message = MSG % {
typo.typo, corrections.join(" | "),
}
if corrections.size == 1
issue_for typo.location, typo.end_location, message do |corrector|
corrector.replace(typo.location, typo.end_location, corrections.first)
end
else
issue_for typo.location, typo.end_location, message
end
end
rescue ex
raise ex if fail_on_error?
end
private record Typo,
path : String,
typo : String,
corrections : Array(String),
location : {Int32, Int32},
end_location : {Int32, Int32} do
def self.parse(str) : self?
issue = JSON.parse(str)
return unless issue["type"] == "typo"
typo = issue["typo"].as_s
corrections = issue["corrections"].as_a.map(&.as_s)
return if typo.empty? || corrections.empty?
path = issue["path"].as_s
line_no = issue["line_num"].as_i
col_no = issue["byte_offset"].as_i + 1
end_col_no = col_no + typo.size - 1
new(path, typo, corrections,
{line_no, col_no}, {line_no, end_col_no})
end
end
protected def typos_from(source : Source) : Array(Typo)?
unless bin_path = self.bin_path
if fail_on_error?
raise RuntimeError.new "Could not find `typos` executable"
end
return
end
status = Process.run(bin_path, args: %w[--format json -],
input: IO::Memory.new(source.code),
output: output = IO::Memory.new,
)
return if status.success?
([] of Typo).tap do |typos|
# NOTE: `--format json` is actually JSON Lines (`jsonl`)
output.to_s.each_line do |line|
Typo.parse(line).try { |typo| typos << typo }
end
end
end
end
end

View File

@ -42,8 +42,6 @@ module Ameba::Rule::Lint
# Enabled: true # Enabled: true
# ``` # ```
class UnreachableCode < Base class UnreachableCode < Base
include AST::Util
properties do properties do
description "Reports unreachable code" description "Reports unreachable code"
end end

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