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

View File

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

View File

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

View File

@ -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
# The shards command to use
SHARDS_BIN ?= shards
PREFIX ?= /usr/local
# The install command to use
INSTALL_BIN ?= /usr/bin/install
SHARD_BIN ?= ../../bin
CRFLAGS ?= -Dpreview_mt
SRC_SOURCES ::= $(shell find src -name '*.cr' 2>/dev/null)
DOC_SOURCE ::= src/**
.PHONY: all
all: build
.PHONY: build
build:
build: ## Build the application binary
build: $(BUILD_TARGET)
$(BUILD_TARGET): $(SRC_SOURCES)
$(SHARDS_BIN) build $(CRFLAGS)
docs: ## Generate API docs
docs: $(SRC_SOURCES)
$(CRYSTAL_BIN) docs -o docs $(DOC_SOURCE)
.PHONY: lint
lint: build
./bin/ameba
lint: ## Run ameba on ameba's code base
lint: $(BUILD_TARGET)
$(BUILD_TARGET)
.PHONY: spec
spec: ## Run the spec suite
spec:
$(CRYSTAL_BIN) spec
.PHONY: clean
clean: ## Remove application binary
clean:
rm -f ./bin/ameba ./bin/ameba.dwarf
@rm -f "$(BUILD_TARGET)" "$(BUILD_TARGET).dwarf"
.PHONY: install
install: build
mkdir -p $(PREFIX)/bin
cp ./bin/ameba $(PREFIX)/bin
install: ## Install application binary into $DESTDIR
install: $(BUILD_TARGET)
$(INSTALL_BIN) -m 0755 "$(BUILD_TARGET)" "$(BINDIR)/ameba"
.PHONY: bin
bin: build
mkdir -p $(SHARD_BIN)
cp ./bin/ameba $(SHARD_BIN)
.PHONY: run_file
run_file:
cp -n ./bin/ameba.cr $(SHARD_BIN) || true
cp $(BUILD_TARGET) $(SHARD_BIN)
.PHONY: test
test: ## Run the spec suite and linter
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>
</p>
<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/blob/master/LICENSE"><img src="https://img.shields.io/github/license/crystal-ameba/ameba.svg"></a>
</p>
@ -118,7 +118,6 @@ Add this to your application's `shard.yml`:
development_dependencies:
ameba:
github: crystal-ameba/ameba
version: ~> 1.4.0
```
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:**
- `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.
- `Excluded` section - an array of wildcards (or paths) to exclude from the
source list defined by `Globs`. Defaults to an empty array.
@ -186,8 +185,8 @@ Excluded:
``` yaml
Style/RedundantBegin:
Excluded:
- src/server/processor.cr
- src/server/api.cr
- src/server/processor.cr
- src/server/api.cr
```
### Rules
@ -240,4 +239,4 @@ time = Time.epoch(1483859302) # ameba:disable Style, Lint
## Contributors
- [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,
30,
40,
].each do |n|
].each do |n| # ameba:disable Naming/BlockParameterName
config = Ameba::Config.load
config.formatter = Ameba::Formatter::BaseFormatter.new
config.globs = get_files(n)

View File

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

View File

@ -298,6 +298,34 @@ module Ameba::AST
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
it "creates new branch" do
nodes = as_nodes <<-CRYSTAL
@ -358,6 +386,30 @@ module Ameba::AST
branch = Branch.new nodes.assign_nodes.first, branchable
branch.in_loop?.should be_false
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

View File

@ -57,13 +57,15 @@ module Ameba::AST
end
end
CRYSTAL
scope = Scope.new nodes.def_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)
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
end
@ -77,13 +79,15 @@ module Ameba::AST
end
end
CRYSTAL
scope = Scope.new nodes.def_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)
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
end
@ -98,9 +102,11 @@ module Ameba::AST
end
end
CRYSTAL
scope = Scope.new nodes.def_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)
variable = Variable.new(var_node, scope)
@ -120,7 +126,7 @@ module Ameba::AST
describe "#find_variable" do
it "returns the variable in the scope by name" do
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
end
@ -133,7 +139,7 @@ module Ameba::AST
describe "#assign_variable" do
it "creates a new assignment" do
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")
var = scope.find_variable("foo").should_not be_nil
var.assignments.size.should eq 1
@ -141,7 +147,7 @@ module Ameba::AST
it "does not create the assignment if variable is wrong" do
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")
var = scope.find_variable("foo").should_not be_nil
var.assignments.size.should eq 0
@ -174,6 +180,28 @@ module Ameba::AST
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
it "returns true if Crystal::Macro" do
nodes = as_nodes <<-CRYSTAL

View File

@ -36,6 +36,43 @@ module Ameba::AST
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
it "returns original source of the node" do
s = <<-CRYSTAL

View File

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

View File

@ -85,13 +85,16 @@ module Ameba::AST
3.times { |i| a = a + i }
end
CRYSTAL
scope = Scope.new nodes.def_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)
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
end
@ -101,8 +104,10 @@ module Ameba::AST
a = 1
end
CRYSTAL
scope.add_variable Crystal::Var.new "a"
scope.add_variable(Crystal::Var.new "a")
variable = scope.variables.first
variable.captured_by_block?.should be_falsey
end
end

View File

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

View File

@ -10,14 +10,16 @@ module Ameba::Rule
end
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
Documentation
Layout
Lint
Metrics
Naming
Performance
Style
)
]
end
end
@ -48,25 +50,25 @@ module Ameba::Rule
it "returns false if source is not excluded from this rule" do
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
end
it "returns true if source is excluded from this rule" do
rule = DummyRule.new
rule.excluded = %w(source.cr)
rule.excluded = %w[source.cr]
rule.excluded?(Source.new "", "source.cr").should be_true
end
it "returns true if source matches the wildcard" do
rule = DummyRule.new
rule.excluded = %w(**/*.cr)
rule.excluded = %w[**/*.cr]
rule.excluded?(Source.new "", __FILE__).should be_true
end
it "returns false if source does not match the wildcard" do
rule = DummyRule.new
rule.excluded = %w(*_spec.cr)
rule.excluded = %w[*_spec.cr]
rule.excluded?(Source.new "", "source.cr").should be_false
end
end

View File

@ -5,97 +5,97 @@ module Ameba::Cli
describe "Cmd" do
describe ".run" do
it "runs ameba" do
r = Cli.run %w(-f silent file.cr)
r = Cli.run %w[-f silent file.cr]
r.should be_nil
end
end
describe ".parse_args" do
%w(-s --silent).each do |f|
it "accepts #{f} flag" do
c = Cli.parse_args [f]
%w[-s --silent].each do |flag|
it "accepts #{flag} flag" do
c = Cli.parse_args [flag]
c.formatter.should eq :silent
end
end
%w(-c --config).each do |f|
it "accepts #{f} flag" do
c = Cli.parse_args [f, "config.yml"]
%w[-c --config].each do |flag|
it "accepts #{flag} flag" do
c = Cli.parse_args [flag, "config.yml"]
c.config.should eq Path["config.yml"]
end
end
%w(-f --format).each do |f|
it "accepts #{f} flag" do
c = Cli.parse_args [f, "my-formatter"]
%w[-f --format].each do |flag|
it "accepts #{flag} flag" do
c = Cli.parse_args [flag, "my-formatter"]
c.formatter.should eq "my-formatter"
end
end
it "accepts --only flag" do
c = Cli.parse_args ["--only", "RULE1,RULE2"]
c.only.should eq %w(RULE1 RULE2)
c.only.should eq %w[RULE1 RULE2]
end
it "accepts --except flag" do
c = Cli.parse_args ["--except", "RULE1,RULE2"]
c.except.should eq %w(RULE1 RULE2)
c.except.should eq %w[RULE1 RULE2]
end
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
end
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
end
it "accepts --rules flag" do
c = Cli.parse_args %w(--rules)
c = Cli.parse_args %w[--rules]
c.rules?.should eq true
end
it "defaults all? flag to false" do
c = Cli.parse_args %w(file.cr)
c = Cli.parse_args %w[file.cr]
c.all?.should be_false
end
it "accepts --all flag" do
c = Cli.parse_args %w(--all)
c = Cli.parse_args %w[--all]
c.all?.should eq true
end
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
end
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
end
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
end
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
end
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.skip_reading_config?.should be_true
end
describe "-e/--explain" do
it "configures file/line/column" do
c = Cli.parse_args %w(--explain src/file.cr:3:5)
c = Cli.parse_args %w[--explain src/file.cr:3:5]
location_to_explain = c.location_to_explain.should_not be_nil
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
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
it "raises an error if line number is not valid" do
expect_raises(Exception, "location should have PATH:line:column") do
Cli.parse_args %w(--explain src/file.cr:a:3)
Cli.parse_args %w[--explain src/file.cr:a:3]
end
end
it "raises an error if column number is not valid" do
expect_raises(Exception, "location should have PATH:line:column") do
Cli.parse_args %w(--explain src/file.cr:3:&)
Cli.parse_args %w[--explain src/file.cr:3:&]
end
end
it "raises an error if line/column are missing" do
expect_raises(Exception, "location should have PATH:line:column") do
Cli.parse_args %w(--explain src/file.cr)
Cli.parse_args %w[--explain src/file.cr]
end
end
end
context "--fail-level" 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
end
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
end
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
end
it "raises if fail level is incorrect" 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
it "accepts unknown args as globs" do
c = Cli.parse_args %w(source1.cr source2.cr)
c.globs.should eq %w(source1.cr source2.cr)
c = Cli.parse_args %w[source1.cr source2.cr]
c.globs.should eq %w[source1.cr source2.cr]
end
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[:file].should eq "source.cr"

View File

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

View File

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

View File

@ -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"
module Ameba::Rule::Lint
module Ameba::Rule::Documentation
subject = Documentation.new
.tap(&.ignore_classes = false)
.tap(&.ignore_modules = false)

View File

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

View File

@ -6,8 +6,12 @@ module Ameba::Rule::Lint
describe LiteralsComparison do
it "passes for valid cases" do
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"
CRYSTAL
@ -15,8 +19,8 @@ module Ameba::Rule::Lint
it "reports if there is a dynamic comparison possibly evaluating to the same" do
expect_issue subject, <<-CRYSTAL
[foo] === ["foo"]
# ^^^^^^^^^^^^^^^ error: Comparison most likely evaluates to the same
[foo] === [foo]
# ^^^^^^^^^^^^^ error: Comparison most likely evaluates to the same
CRYSTAL
end

View File

@ -11,6 +11,7 @@ module Ameba::Rule::Lint
(1..3).index { |i| i > 2 }.not_nil!(:foo)
(1..3).rindex { |i| i > 2 }.not_nil!(:foo)
(1..3).find { |i| i > 2 }.not_nil!(:foo)
/(.)(.)(.)/.match("abc", &.itself).not_nil!
CRYSTAL
end
@ -36,6 +37,17 @@ module Ameba::Rule::Lint
CRYSTAL
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
source = expect_issue subject, <<-CRYSTAL
(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
s = Source.new %q(
%i(one two three)
%w(one two three)
%i[one two three]
%w[one two three]
%i(1 2 3)
%w(1 2 3)
%i[1 2 3]
%w[1 2 3]
%i()
%w()
%i[]
%w[]
)
subject.catch(s).should be_valid
end
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
end
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
end
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
end
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
end
it "reports rule, location and message for %i" do
s = Source.new %(
%i(:one)
%i[:one]
), "source.cr"
subject.catch(s).should_not be_valid
@ -54,7 +54,7 @@ module Ameba::Rule::Lint
it "reports rule, location and message for %w" do
s = Source.new %(
%w("one")
%w["one"]
), "source.cr"
subject.catch(s).should_not be_valid
@ -71,14 +71,14 @@ module Ameba::Rule::Lint
it "#string_array_unwanted_symbols" do
rule = PercentArrays.new
rule.string_array_unwanted_symbols = ","
s = Source.new %( %w("one") )
s = Source.new %( %w["one"] )
rule.catch(s).should be_valid
end
it "#symbol_array_unwanted_symbols" do
rule = PercentArrays.new
rule.symbol_array_unwanted_symbols = ","
s = Source.new %( %i(:one) )
s = Source.new %( %i[:one] )
rule.catch(s).should be_valid
end
end

View File

@ -31,6 +31,30 @@ module Ameba::Rule::Lint
CRYSTAL
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
expect_no_issues subject, <<-CRYSTAL
methods = klass.methods.select { |m| m.annotation(MyAnn) }
@ -44,7 +68,7 @@ module Ameba::Rule::Lint
foo = 1
-> (foo : Int32) {}
# ^ error: Shadowing outer local variable `foo`
# ^^^^^^^^^^^ error: Shadowing outer local variable `foo`
end
CRYSTAL
end
@ -69,7 +93,7 @@ module Ameba::Rule::Lint
3.times do |foo|
# ^ error: Shadowing outer local variable `foo`
-> (foo : Int32) { foo + 1 }
# ^ error: Shadowing outer local variable `foo`
# ^^^^^^^^^^^ error: Shadowing outer local variable `foo`
end
CRYSTAL
end

View File

@ -39,7 +39,7 @@ module Ameba::Rule::Lint
CRYSTAL
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
i = 0
while i < 10
@ -56,6 +56,24 @@ module Ameba::Rule::Lint
expect_no_corrections source
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
source = expect_issue subject, <<-CRYSTAL
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.location.to_s.should eq "source_spec.cr:1:11"
first.end_location.to_s.should eq ""
first.end_location.to_s.should eq "source_spec.cr:1:21"
first.message.should eq "Focused spec item detected"
second.rule.should_not be_nil
second.location.to_s.should eq "source_spec.cr:2:13"
second.end_location.to_s.should eq ""
second.end_location.to_s.should eq "source_spec.cr:2:23"
second.message.should eq "Focused spec item detected"
end
end

View File

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

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"
module Ameba
subject = Rule::Style::ConstantNames.new
subject = Rule::Naming::ConstantNames.new
private def it_reports_constant(name, value, expected, *, file = __FILE__, line = __LINE__)
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
%{name} = #{value}
# ^{name} error: Constant name should be screaming-cased: #{expected}, not #{name}
@ -13,7 +13,7 @@ module Ameba
end
end
describe Rule::Style::ConstantNames do
describe Rule::Naming::ConstantNames do
it "passes if type names are screaming-cased" do
expect_no_issues subject, <<-CRYSTAL
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"
module Ameba
subject = Rule::Style::MethodNames.new
subject = Rule::Naming::MethodNames.new
private def it_reports_method_name(name, expected, *, file = __FILE__, line = __LINE__)
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
def %{name}; end
# ^{name} error: Method name should be underscore-cased: #{expected}, not %{name}
@ -13,7 +13,7 @@ module Ameba
end
end
describe Rule::Style::MethodNames do
describe Rule::Naming::MethodNames do
it "passes if method names are underscore-cased" do
expect_no_issues subject, <<-CRYSTAL
class Person

View File

@ -1,6 +1,6 @@
require "../../../spec_helper"
module Ameba::Rule::Style
module Ameba::Rule::Naming
subject = PredicateName.new
describe PredicateName do
@ -21,8 +21,18 @@ module Ameba::Rule::Style
it "fails if predicate name is wrong" do
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)
# ^^^^^^^^^^^^^^ 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
CRYSTAL
end

View File

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

View File

@ -1,11 +1,11 @@
require "../../../spec_helper"
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__)
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
%{name} = #{value}
# ^{name} error: Var name should be underscore-cased: #{expected}, not %{name}
@ -13,7 +13,7 @@ module Ameba
end
end
describe Rule::Style::VariableNames do
describe Rule::Naming::VariableNames do
it "passes if var names are underscore-cased" do
expect_no_issues subject, <<-CRYSTAL
class Greeting

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -95,7 +95,7 @@ module Ameba
end
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
MyBadConstant = 1

View File

@ -22,23 +22,23 @@ module Ameba
DELIMITER_START STRING INTERPOLATION_START NUMBER } DELIMITER_END EOF
)
it_tokenizes %(%w(1 2)),
%w(STRING_ARRAY_START STRING STRING STRING_ARRAY_END EOF)
it_tokenizes %(%w[1 2]),
%w[STRING_ARRAY_START STRING STRING STRING_ARRAY_END EOF]
it_tokenizes %(%i(one two)),
%w(SYMBOL_ARRAY_START STRING STRING STRING_ARRAY_END EOF)
it_tokenizes %(%i[one two]),
%w[SYMBOL_ARRAY_START STRING STRING STRING_ARRAY_END EOF]
it_tokenizes %(
class A
def method
puts "hello"
end
class A
def method
puts "hello"
end
), %w(
end
), %w[
NEWLINE SPACE IDENT SPACE CONST NEWLINE SPACE IDENT SPACE IDENT
NEWLINE SPACE IDENT SPACE DELIMITER_START STRING DELIMITER_END
NEWLINE SPACE IDENT NEWLINE SPACE IDENT NEWLINE SPACE EOF
)
]
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.
class DummyRule < Rule::Base
properties do
description : String = "Dummy rule that does nothing."
description "Dummy rule that does nothing."
dummy true
end
@ -92,7 +92,7 @@ module Ameba
class PerfRule < Rule::Performance::Base
properties do
description : String = "Sample performance rule"
description "Sample performance rule"
end
def test(source)
@ -259,6 +259,7 @@ module Ameba
Crystal::MacroLiteral,
Crystal::Expressions,
Crystal::ControlExpression,
Crystal::Call,
}
def initialize(node)
@ -282,6 +283,13 @@ module Ameba
end
end
def with_presenter(klass, &)
io = IO::Memory.new
presenter = klass.new(io)
yield presenter, io
end
def as_node(source)
Crystal::Parser.new(source).parse
end

View File

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

View File

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

View File

@ -34,9 +34,8 @@ module Ameba::AST
# The actual AST node that represents a current scope.
getter node : Crystal::ASTNode
delegate to_s, to: node
delegate location, to: node
delegate end_location, to: node
delegate location, end_location, to_s,
to: @node
def_equals_and_hash node, location
@ -181,14 +180,19 @@ module Ameba::AST
@visibility || outer_scope.try(&.visibility)
end
# Returns `true` if current scope is a def, `false` otherwise.
def def?
node.is_a?(Crystal::Def)
end
{% for type in %w[Def ClassDef ModuleDef EnumDef LibDef FunDef].map(&.id) %}
{% method_name = type.underscore %}
# Returns `true` if current scope is a {{ method_name[0..-5] }} def, `false` otherwise.
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.
def top_level?
outer_scope.nil? || type_definition?
outer_scope.nil?
end
# 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)}
when Crystal::ArrayLiteral,
Crystal::TupleLiteral
{true, node.elements.all? do |el|
static_literal?(el)
{true, node.elements.all? do |element|
static_literal?(element)
end}
when Crystal::HashLiteral
{true, node.entries.all? do |entry|

View File

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

View File

@ -19,9 +19,8 @@ module Ameba::AST
# A scope assignment belongs to
getter scope : Scope
delegate to_s, to: @node
delegate location, to: @node
delegate end_location, to: @node
delegate location, end_location, to_s,
to: @node
# Creates a new assignment.
#
@ -32,9 +31,7 @@ module Ameba::AST
return unless scope = @variable.scope
@branch = Branch.of(@node, scope)
@referenced = true if @variable.special? ||
@variable.scope.type_definition? ||
referenced_in_loop?
@referenced = true if @variable.special? || referenced_in_loop?
end
def referenced_in_loop?
@ -75,31 +72,5 @@ module Ameba::AST
node
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

View File

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

View File

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

View File

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

View File

@ -24,7 +24,7 @@ module Ameba::AST
# Uses the same logic than rubocop. See
# 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.
{% for node in %i(if while until rescue or and) %}
{% for node in %i[if while until rescue or and] %}
# :nodoc:
def visit(node : Crystal::{{ node.id.capitalize }})
@complexity += 1 unless macro_condition?

View File

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

View File

@ -43,7 +43,7 @@ module Ameba::AST
end
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)
end
@ -54,7 +54,7 @@ module Ameba::AST
private def traverse_exception_handler(node)
traverse_node node.body
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

View File

@ -21,7 +21,6 @@ module Ameba::AST
}
SPECIAL_NODE_NAMES = %w[super previous_def]
RECORD_NODE_NAME = "record"
@scope_queue = [] of Scope
@current_scope : Scope
@ -154,7 +153,7 @@ module Ameba::AST
# :nodoc:
def visit(node : Crystal::Var)
variable = @current_scope.find_variable node.name
variable = @current_scope.find_variable(node.name)
case
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
@current_scope.add_variable(node)
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)
variable.reference_assignments!
end
@ -171,26 +170,39 @@ module Ameba::AST
# :nodoc:
def visit(node : Crystal::Call)
case
when @current_scope.def?
if node.name.in?(SPECIAL_NODE_NAMES) && node.args.empty?
@current_scope.arguments.each do |arg|
variable = arg.variable
scope = @current_scope
ref = variable.reference(variable.node, @current_scope)
ref.explicit = false
end
case
when (scope.top_level? || scope.type_definition?) && record_macro?(node)
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
true
when @current_scope.top_level? && record_macro?(node)
false
else
true
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
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
private def skip?(node)

View File

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

View File

@ -97,8 +97,9 @@ class Ameba::Config
@excluded = load_array_section(config, "Excluded")
@globs = load_array_section(config, "Globs", DEFAULT_GLOBS)
return unless formatter_name = load_formatter_name(config)
self.formatter = formatter_name
if formatter_name = load_formatter_name(config)
self.formatter = formatter_name
end
end
# Loads YAML configuration file by `path`.
@ -115,12 +116,13 @@ class Ameba::Config
end
Config.new YAML.parse(content)
rescue e
raise "Config file is invalid: #{e.message}"
raise "Unable to load config file: #{e.message}"
end
protected def self.read_config(path = nil)
if path
return File.exists?(path) ? File.read(path) : nil
return File.read(path) if File.exists?(path)
raise "Config file does not exist"
end
each_config_path do |config_path|
return File.read(config_path) if File.exists?(config_path)
@ -202,13 +204,13 @@ class Ameba::Config
#
# ```
# 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:
#
# ```
# config.update_rules %w(Group1 Group2), enabled: true
# config.update_rules %w[Group1 Group2], enabled: true
# ```
def update_rules(names, enabled = true, excluded = nil)
names.try &.each do |name|
@ -243,20 +245,20 @@ class Ameba::Config
# Define rule properties
macro properties(&block)
{% definitions = [] of NamedTuple %}
{% if block.body.is_a? Assign %}
{% definitions << {var: block.body.target, value: block.body.value} %}
{% elsif block.body.is_a? Call %}
{% definitions << {var: block.body.name, value: block.body.args.first} %}
{% elsif block.body.is_a? TypeDeclaration %}
{% definitions << {var: block.body.var, value: block.body.value, type: block.body.type} %}
{% if (prop = block.body).is_a? Call %}
{% if (named_args = prop.named_args) && (type = named_args.select(&.name.== "as".id).first) %}
{% definitions << {var: prop.name, value: prop.args.first, type: type.value} %}
{% else %}
{% definitions << {var: prop.name, value: prop.args.first} %}
{% end %}
{% elsif block.body.is_a? Expressions %}
{% for prop in block.body.expressions %}
{% if prop.is_a? Assign %}
{% definitions << {var: prop.target, value: prop.value} %}
{% elsif prop.is_a? Call %}
{% definitions << {var: prop.name, value: prop.args.first} %}
{% elsif prop.is_a? TypeDeclaration %}
{% definitions << {var: prop.var, value: prop.value, type: prop.type} %}
{% if prop.is_a? Call %}
{% if (named_args = prop.named_args) && (type = named_args.select(&.name.== "as".id).first) %}
{% definitions << {var: prop.name, value: prop.args.first, type: type.value} %}
{% else %}
{% definitions << {var: prop.name, value: prop.args.first} %}
{% end %}
{% end %}
{% end %}
{% end %}
@ -322,9 +324,10 @@ class Ameba::Config
macro included
GROUP_SEVERITY = {
Lint: Ameba::Severity::Warning,
Metrics: Ameba::Severity::Warning,
Performance: Ameba::Severity::Warning,
Documentation: Ameba::Severity::Warning,
Lint: Ameba::Severity::Warning,
Metrics: Ameba::Severity::Warning,
Performance: Ameba::Severity::Warning,
}
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.
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.
def source_finished(source : Source); end
def source_started(source : Source); end
# Callback that indicates when source inspection is finished.
# 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.
# 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 specific location.
class ExplainFormatter
HEADING_MARKER = "## "
include Util
getter output : IO::FileDescriptor | IO::Memory
@ -64,9 +62,8 @@ module Ameba::Formatter
rule.name.colorize(:magenta),
rule.severity.to_s.colorize(rule.severity.color),
}
if rule.responds_to?(:description)
output_paragraph rule.description
if rule_description = colorize_code_fences(rule.description)
output_paragraph rule_description
end
rule_doc = colorize_code_fences(rule.class.parsed_doc)
@ -84,7 +81,7 @@ module Ameba::Formatter
end
private def output_title(title)
output << HEADING_MARKER.colorize(:yellow)
output << "### ".colorize(:yellow)
output << title.upcase.colorize(:yellow)
output << "\n\n"
end
@ -95,7 +92,7 @@ module Ameba::Formatter
private def output_paragraph(paragraph : Array)
paragraph.each do |line|
output << ' ' << line << '\n'
output << " " << line << '\n'
end
output << '\n'
end

View File

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

View File

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

View File

@ -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
# Represents a module used to report issues.
module Reportable
include AST::Util
# List of reported issues.
getter issues = [] of Issue
@ -30,13 +34,19 @@ module Ameba
end
# 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
add_issue rule, node.location, node.end_location, message, status, block
def add_issue(rule, node : Crystal::ASTNode, message, status : Issue::Status? = nil, block : (Source::Corrector ->)? = nil, *, prefer_name_location = false) : Issue
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
# :ditto:
def add_issue(rule, node : Crystal::ASTNode, message, status : Issue::Status? = nil, &block : Source::Corrector ->) : Issue
add_issue rule, node, message, status, block
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, prefer_name_location: prefer_name_location
end
# 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
# 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.
def test(source : Source)
AST::NodeVisitor.new self, source
end
# NOTE: Can't be abstract
def test(source : Source, node : Crystal::ASTNode, *opts)
# can't be abstract
end
# A convenient addition to `#test` method that does the same
@ -114,7 +115,7 @@ module Ameba::Rule
# Adds an issue to the *source*
macro issue_for(*args, **kwargs, &block)
source.add_issue(self, {{ *args }}, {{ **kwargs }}) {{ block }}
source.add_issue(self, {{ args.splat }}, {{ kwargs.double_splat }}) {{ block }}
end
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:
# modules, classes, enums, methods and macros.
#
# YAML configuration example:
#
# ```
# Lint/Documentation:
# Documentation/Documentation:
# Enabled: true
# IgnoreClasses: false
# 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
MSG = "Comparison to a boolean is pointless"
OP_NAMES = %w(== != ===)
OP_NAMES = %w[== != ===]
def test(source, node : Crystal::Call)
return unless node.name.in?(OP_NAMES)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,17 +1,17 @@
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!`.
#
# 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:
#
# ```
# %w[Alice Bob].find!(&.match(/^A./))
# %w[Alice Bob].find!(&.chars.any?(&.in?('o', 'b')))
# ```
#
# YAML configuration example:
@ -24,25 +24,24 @@ module Ameba::Rule::Lint
include AST::Util
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
BLOCK_CALL_NAMES = %w(index rindex find)
CALL_NAMES = %w(index rindex)
MSG = "Use `%s! {...}` instead of `%s {...}.not_nil!`"
NOT_NIL_NAME = "not_nil!"
MSG = "Use `%s! {...}` instead of `%s {...}.not_nil!`"
BLOCK_CALL_NAMES = %w[index rindex find]
CALL_NAMES = %w[index rindex match]
def test(source)
AST::NodeVisitor.new self, source, skip: :macro
end
def test(source, node : Crystal::Call)
return unless node.name == NOT_NIL_NAME && node.args.empty?
return unless node.name == "not_nil!" && node.args.empty?
return unless (obj = node.obj).is_a?(Crystal::Call)
return unless obj.name.in?(obj.block ? BLOCK_CALL_NAMES : CALL_NAMES)
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 end_location = name_end_location(node)

View File

@ -4,15 +4,15 @@ module Ameba::Rule::Lint
# For example, this is usually written by mistake:
#
# ```
# %i(:one, :two)
# %w("one", "two")
# %i[:one, :two]
# %w["one", "two"]
# ```
#
# And the expected example is:
#
# ```
# %i(one two)
# %w(one two)
# %i[one two]
# %w[one two]
# ```
#
# YAML configuration example:
@ -42,7 +42,7 @@ module Ameba::Rule::Lint
start_token = token.dup
when .string?
if (_start = start_token) && !issue
issue = array_entry_invalid?(token.value, _start.raw)
issue = array_entry_invalid?(token.value.to_s, _start.raw)
end
when .string_array_end?
if (_start = start_token) && (_issue = issue)
@ -63,7 +63,7 @@ module Ameba::Rule::Lint
end
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

View File

@ -30,8 +30,8 @@ module Ameba::Rule::Lint
MSG = "Redundant use of `Object#to_s` in interpolation"
def test(source, node : Crystal::StringInterpolation)
string_coercion_nodes(node).each do |n|
issue_for n.name_location, n.end_location, MSG
string_coercion_nodes(node).each do |expr|
issue_for name_location(expr), expr.end_location, MSG
end
end

View File

@ -51,7 +51,7 @@ module Ameba::Rule::Lint
end
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

View File

@ -40,7 +40,7 @@ module Ameba::Rule::Lint
!(block = node.block) ||
with_index_arg?(block)
issue_for node.name_location, node.name_end_location, MSG
issue_for node, MSG, prefer_name_location: true
end
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
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 outer_scope.assigns_ivar?(arg.name)
next if outer_scope.assigns_type_dec?(arg.name)
next if outer_scope.assigns_ivar?(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

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"
end
MSG = "Focused spec item detected"
SPEC_ITEM_NAMES = %w(describe context it pending)
MSG = "Focused spec item detected"
SPEC_ITEM_NAMES = %w[describe context it pending]
def test(source)
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
# ```
class UnreachableCode < Base
include AST::Util
properties do
description "Reports unreachable code"
end

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