Compare commits

...

206 Commits

Author SHA1 Message Date
Sijawusz Pur Rahnama 6d03cef6df
Merge pull request #460 from crystal-ameba/fix-issue-459
Make sure we only return files from `GlobUtils#expand` method
2024-04-17 23:46:44 +02:00
Sijawusz Pur Rahnama f12e7f6c5d Add spec 2024-04-17 23:43:03 +02:00
Sijawusz Pur Rahnama 1bd59c1bf0 Make `GlobUtils` extend `self` for easier access 2024-04-17 23:42:05 +02:00
Sijawusz Pur Rahnama 5403aee899 Make sure we only return files from `GlobUtils#expand` method 2024-04-17 12:07:44 +02:00
dependabot[bot] e6a5fa9d71
Merge pull request #458 from crystal-ameba/dependabot/github_actions/szenius/set-timezone-2.0 2024-04-10 22:03:00 +00:00
dependabot[bot] a3f906a38a
Bump szenius/set-timezone from 1.2 to 2.0
Bumps [szenius/set-timezone](https://github.com/szenius/set-timezone) from 1.2 to 2.0.
- [Release notes](https://github.com/szenius/set-timezone/releases)
- [Commits](https://github.com/szenius/set-timezone/compare/v1.2...v2.0)

---
updated-dependencies:
- dependency-name: szenius/set-timezone
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-10 21:30:02 +00:00
Sijawusz Pur Rahnama 107c6e0ea6
Merge pull request #455 from crystal-ameba/tweak-spec-filename-rule 2024-03-10 00:51:12 +01:00
Sijawusz Pur Rahnama a661cf10fc Ignore files with extensions other than `.cr` 2024-03-09 22:38:47 +01:00
Sijawusz Pur Rahnama a2c9aa67cc Add missed spec case for files outside `spec/` folder 2024-03-09 22:38:23 +01:00
Sijawusz Pur Rahnama e2d6c69039 Include `SpecFilename` rule properties in its documentation 2024-03-09 22:37:58 +01:00
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 29e29b8e1d Fix `Performance/ExcessiveAllocations` to exclude `each` calls without a block 2023-06-30 21:44:47 +02:00
Sijawusz Pur Rahnama 21051acfff
Merge pull request #386 from crystal-ameba/fix-issue-385 2023-06-30 20:58:56 +02:00
Sijawusz Pur Rahnama abe5237802 Add `Performance/ExcessiveAllocations` rule 2023-06-30 15:17:40 +02:00
Sijawusz Pur Rahnama b7b21ffeb0
Merge pull request #384 from crystal-ameba/fix-issue-383
Fix `Style/VerboseBlock` rule to work with binary operations
2023-06-29 10:20:13 +02:00
Sijawusz Pur Rahnama 4d0125a0f3 Fix `Style/VerboseBlock` rule to work with binary operations 2023-06-29 08:15:39 +02:00
Sijawusz Pur Rahnama e1f5c81804
Merge pull request #379 from crystal-ameba/several-tweaks-and-refactors
Several tweaks and refactors
2023-06-14 16:54:27 +02:00
Sijawusz Pur Rahnama 16141a376e Cleanup properties definition macro 2023-06-14 15:08:19 +02:00
Sijawusz Pur Rahnama 596b0dd9d0 Misc tweaks and refactors 2023-06-14 15:06:24 +02:00
Sijawusz Pur Rahnama b4244d4c61
Merge pull request #378 from crystal-ameba/node-visitor-category 2023-06-13 11:36:05 +02:00
Sijawusz Pur Rahnama 1931a5f4ef Make `skip` a named argument 2023-06-12 23:17:14 +02:00
Sijawusz Pur Rahnama c09b36799a Make `AST::NodeVisitor::Category` a flag enum 2023-06-12 23:17:14 +02:00
Sijawusz Pur Rahnama 38b6751bc0 Add `AST::NodeVisitor::Category` simplifying code a bit 2023-06-12 23:17:14 +02:00
Sijawusz Pur Rahnama db59b23f9b Fix specs against Crystal nightly 2023-06-10 01:11:21 +02:00
Sijawusz Pur Rahnama 9a8538aa69
Update README.md 2023-06-09 09:57:26 +02:00
Sijawusz Pur Rahnama aceb054aa0
Merge pull request #374 from crystal-ameba/lint-documentation-rule
Add `Lint/Documentation` rule
2023-06-08 14:10:21 +02:00
Sijawusz Pur Rahnama b156a6a6a1 Add comments to macros 2023-06-08 14:03:35 +02:00
Sijawusz Pur Rahnama 94e31d4685 Do the same in `NodeVisitor` 2023-06-08 04:28:37 +02:00
Sijawusz Pur Rahnama e12d72cc88 Set the `ASTNode#visibility` as well 2023-06-08 04:28:17 +02:00
Sijawusz Pur Rahnama 262e31c35b Cleanup README mention (MT is enabled by default) 2023-06-08 02:26:39 +02:00
Sijawusz Pur Rahnama c9d25f3409 Do not run disabled rules against the codebase 2023-06-08 02:24:12 +02:00
Sijawusz Pur Rahnama 7caa47fb6a Several small refactors 2023-06-08 02:04:30 +02:00
Sijawusz Pur Rahnama 4d8346509e Implement `Documentation` rule on top of the `ScopeVisitor` 2023-06-08 02:04:06 +02:00
Sijawusz Pur Rahnama 4c740f394a Implement `Scope#visibility` 2023-06-08 01:58:58 +02:00
Sijawusz Pur Rahnama 85c3db4d74 Move `NODES` constant into its proper namespace 2023-05-31 13:15:01 +02:00
Sijawusz Pur Rahnama 6e5a9a60b3 Refactor `Lint::Documentation` and ignore macro hooks 2023-05-29 17:09:39 +02:00
Sijawusz Pur Rahnama 09fdac6be9 Refactor `Lint::Documentation` rule to use a custom visitor 2023-05-29 16:33:45 +02:00
Sijawusz Pur Rahnama 1a9a58b3cd Add `Lint/Documentation` rule 2023-05-29 16:32:51 +02:00
Sijawusz Pur Rahnama 60948fffd0
Merge pull request #373 from crystal-ameba/fix-372
Raise when empty severity provided to `SeverityYamlConverter.from_yaml`
2023-05-09 14:28:17 +02:00
Sijawusz Pur Rahnama d0d8b18c83
Raise when empty severity provided to `SeverityYamlConverter.from_yaml` 2023-05-09 13:02:10 +02:00
Sijawusz Pur Rahnama 14f6ba0c0b
Merge pull request #323 from crystal-ameba/Sija/lint-not-nil-after-no-bang-reports-rindex-calls
Make `Lint/NotNilAfterNoBang` report calls to `#rindex`
2023-05-01 11:16:42 +02:00
Vitalii Elenhaupt 149080ae16
Merge pull request #369 from daliborfilus/fix-crystal-1.8-pcre2-docker-build
Update Dockerfile for Crystal 1.8 + check if binary works
2023-04-24 09:46:09 +03:00
Dalibor Filus 454a747a68
Update Dockerfile for Crystal 1.8 + check if binary works
Crystal 1.8.0+ require pcre2.

Let's also add `RUN ameba -v` to check if the binary actually works.
Fixes cases like this:

```shell
$ docker run --rm -ti ameba:latest sh
Error loading shared library libpcre2-8.so.0: No such file or directory (needed by /usr/bin/ameba)
Error relocating /usr/bin/ameba: pcre2_get_ovector_count_8: symbol not found
Error relocating /usr/bin/ameba: pcre2_config_8: symbol not found
Error relocating /usr/bin/ameba: pcre2_get_ovector_pointer_8: symbol not found
Error relocating /usr/bin/ameba: pcre2_match_data_create_from_pattern_8: symbol not found
Error relocating /usr/bin/ameba: pcre2_jit_stack_assign_8: symbol not found
Error relocating /usr/bin/ameba: pcre2_code_free_8: symbol not found
Error relocating /usr/bin/ameba: pcre2_match_context_create_8: symbol not found
Error relocating /usr/bin/ameba: pcre2_jit_compile_8: symbol not found
Error relocating /usr/bin/ameba: pcre2_compile_8: symbol not found
Error relocating /usr/bin/ameba: pcre2_get_error_message_8: symbol not found
Error relocating /usr/bin/ameba: pcre2_jit_stack_create_8: symbol not found
Error relocating /usr/bin/ameba: pcre2_match_8: symbol not found
Error relocating /usr/bin/ameba: pcre2_match_data_free_8: symbol not found
Error relocating /usr/bin/ameba: pcre2_pattern_info_8: symbol not found
```
2023-04-22 11:41:49 +02:00
Vitalii Elenhaupt ef2d05e48a
Merge pull request #366 from straight-shoota/fix/severity-spec-yaml-converter
Fix severity type for YAML converter spec
2023-04-05 17:35:19 +03:00
Johannes Müller c7f2cba409
Fix severity type for YAML converter spec 2023-04-05 11:20:44 +02:00
Vitalii Elenhaupt 7c74d196d6
Bump v1.4.3 2023-03-17 10:24:44 +02:00
Vitalii Elenhaupt adac90c7c6
Merge pull request #363 from zw963/master
Skip all config when use with --gen-config.
2023-03-09 08:34:47 +02:00
Billy.Zheng 239f64c278 Refactor 2023-03-09 12:41:40 +08:00
Billy.Zheng e2528d93dd Refactor spec to use be_false replace `eq false` 2023-03-09 12:39:25 +08:00
Billy.Zheng 48c7a2bde6 Add spec for verifying skip_reading_config? flag defaults to false. 2023-03-09 02:23:48 +08:00
Billy.Zheng d45285d1c9 Skip all config when use with --gen-config. 2023-03-08 22:34:46 +08:00
Sijawusz Pur Rahnama 3fbbe3986e
Merge pull request #361 from crystal-ameba/issue-275
Support hierarchical loading of the config file
2023-03-07 11:11:35 +01:00
Sijawusz Pur Rahnama 4c59858f25 Apply code review suggestions 2023-03-06 21:22:47 +01:00
Sijawusz Pur Rahnama 8d56f22af1 Document the new `Config.load` behaviour 2023-03-06 21:19:47 +01:00
Sijawusz Pur Rahnama e481a8d139 Use `FILENAME` constant consistently 2023-03-06 20:54:35 +01:00
Sijawusz Pur Rahnama 102e2834b6 Honor `XDG_CONFIG_HOME` ENV variable
Following XDG Spec: https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
2023-03-06 20:54:35 +01:00
Sijawusz Pur Rahnama 749da0984c Rename `Config::PATH` to `Config::DEFAULT_PATH` 2023-03-06 20:54:35 +01:00
Sijawusz Pur Rahnama 9534104942 Support hierarchical loading of the config file 2023-03-06 20:54:17 +01:00
Sijawusz Pur Rahnama ce4dd7236a Make `Lint/NotNilAfterNoBang` report calls to `#rindex` 2023-01-10 13:37:28 +01:00
146 changed files with 3346 additions and 1067 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@v2.0
- 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
@ -36,4 +43,4 @@ jobs:
run: shards build -Dpreview_mt
- name: Run ameba linter
run: bin/ameba --all
run: bin/ameba

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

@ -6,8 +6,9 @@ COPY . /ameba/
RUN make clean && make
FROM alpine:latest
RUN apk add --update yaml pcre gc libevent libgcc
RUN apk add --update yaml pcre2 gc libevent libgcc
RUN mkdir /src
WORKDIR /src
COPY --from=builder /ameba/bin/ameba /usr/bin/
RUN ameba -v
ENTRYPOINT [ "/usr/bin/ameba" ]

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 --all
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>
@ -99,15 +99,6 @@ $ ameba --explain crystal/command/format.cr:26:83 # same thing
### Run in parallel
Starting from 0.31.0 Crystal [supports parallelism](https://crystal-lang.org/2019/09/06/parallelism-in-crystal.html).
It allows to run linting in parallel too.
In order to take advantage of this feature you need to build ameba with preview_mt support:
```sh
$ crystal build src/cli.cr -Dpreview_mt -o bin/ameba
$ make install
```
Some quick benchmark results measured while running Ameba on Crystal repo:
```sh
@ -127,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`.
@ -135,7 +125,7 @@ Build `bin/ameba` binary within your project directory while running `shards ins
### OS X
```sh
$ brew tap veelenga/tap
$ brew tap crystal-ameba/ameba
$ brew install ameba
```
@ -174,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.
@ -195,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
@ -249,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.2
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

@ -7,7 +7,7 @@ module Ameba::AST
node = as_node("return 22")
flow_expression = FlowExpression.new node, false
flow_expression.node.should_not be_nil
flow_expression.in_loop?.should eq false
flow_expression.in_loop?.should be_false
end
describe "#delegation" do

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
@ -93,7 +130,7 @@ module Ameba::AST
it "returns false if this is a break out of loop" do
node = as_node("break")
subject.flow_command?(node, false).should eq false
subject.flow_command?(node, false).should be_false
end
it "returns true if this is a next in a loop" do
@ -103,7 +140,7 @@ module Ameba::AST
it "returns false if this is a next out of loop" do
node = as_node("next")
subject.flow_command?(node, false).should eq false
subject.flow_command?(node, false).should be_false
end
it "returns true if this is raise" do
@ -123,7 +160,7 @@ module Ameba::AST
it "returns false otherwise" do
node = as_node("foobar")
subject.flow_command?(node, false).should eq false
subject.flow_command?(node, false).should be_false
end
end
@ -195,7 +232,7 @@ module Ameba::AST
break
end
CRYSTAL
subject.flow_expression?(node).should eq false
subject.flow_expression?(node).should be_false
end
it "returns true if this until consumed by flow expressions" do
@ -213,7 +250,7 @@ module Ameba::AST
break
end
CRYSTAL
subject.flow_expression?(node).should eq false
subject.flow_expression?(node).should be_false
end
it "returns true if this expressions consumed by flow expressions" do
@ -230,7 +267,7 @@ module Ameba::AST
exp1
exp2
CRYSTAL
subject.flow_expression?(node).should eq false
subject.flow_expression?(node).should be_false
end
end
@ -242,12 +279,12 @@ module Ameba::AST
it "returns false if it has a receiver" do
node = as_node "obj.raise e"
subject.raise?(node).should eq false
subject.raise?(node).should be_false
end
it "returns false if size of the arguments doesn't match" do
node = as_node "raise"
subject.raise?(node).should eq false
subject.raise?(node).should be_false
end
end
@ -264,12 +301,12 @@ module Ameba::AST
it "returns false if it has a receiver" do
node = as_node "obj.exit"
subject.exit?(node).should eq false
subject.exit?(node).should be_false
end
it "returns false if size of the arguments doesn't match" do
node = as_node "exit 1, 1"
subject.exit?(node).should eq false
subject.exit?(node).should be_false
end
end
@ -291,12 +328,12 @@ module Ameba::AST
it "returns false if it has a receiver" do
node = as_node "obj.abort"
subject.abort?(node).should eq false
subject.abort?(node).should be_false
end
it "returns false if size of the arguments doesn't match" do
node = as_node "abort 1, 1, 1"
subject.abort?(node).should eq false
subject.abort?(node).should be_false
end
end
@ -308,12 +345,12 @@ module Ameba::AST
it "returns false if it has a receiver" do
node = as_node "obj.loop"
subject.loop?(node).should eq false
subject.loop?(node).should be_false
end
it "returns false if size of the arguments doesn't match" do
node = as_node "loop 1"
subject.loop?(node).should eq false
subject.loop?(node).should be_false
end
end

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

@ -2,6 +2,17 @@ require "../../../spec_helper"
module Ameba::AST
describe ScopeVisitor do
{% for type in %w[class module enum].map(&.id) %}
it "creates a scope for the {{ type }} def" do
rule = ScopeRule.new
ScopeVisitor.new rule, Source.new <<-CRYSTAL
{{ type }} Foo
end
CRYSTAL
rule.scopes.size.should eq 1
end
{% end %}
it "creates a scope for the def" do
rule = ScopeRule.new
ScopeVisitor.new rule, Source.new <<-CRYSTAL
@ -54,5 +65,33 @@ module Ameba::AST
outer_block.outer_scope.should be_nil
end
end
context "#visibility" do
it "is being properly set" do
rule = ScopeRule.new
ScopeVisitor.new rule, Source.new <<-CRYSTAL
private class Foo
end
CRYSTAL
rule.scopes.size.should eq 1
rule.scopes.first.visibility.should eq Crystal::Visibility::Private
end
it "is being inherited from the outer scope(s)" do
rule = ScopeRule.new
ScopeVisitor.new rule, Source.new <<-CRYSTAL
private class Foo
class Bar
def baz
end
end
end
CRYSTAL
rule.scopes.size.should eq 3
rule.scopes.each &.visibility.should eq Crystal::Visibility::Private
rule.scopes.last.node.visibility.should eq Crystal::Visibility::Private
rule.scopes[0...-1].each &.node.visibility.should eq Crystal::Visibility::Public
end
end
end
end

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,92 +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"]
c.config.should eq "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.rules?.should eq false
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.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.all?.should eq false
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.config.should eq ""
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"
@ -100,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"
@ -166,7 +171,7 @@ module Ameba::Cli
c.globs.should be_nil
c.only.should be_nil
c.except.should be_nil
c.config.should eq Config::PATH
c.config.should be_nil
end
end
end

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::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 .ameba.yml"
io.to_s.should contain "Created #{CONFIG_PATH}"
end
end

View File

@ -1,11 +1,7 @@
require "../spec_helper"
module Ameba
struct GlobUtilsClass
include GlobUtils
end
subject = GlobUtilsClass.new
subject = GlobUtils
current_file_basename = File.basename(__FILE__)
current_file_path = "spec/ameba/#{current_file_basename}"
@ -45,6 +41,12 @@ module Ameba
subject.expand(["**/#{current_file_basename}", "**/#{current_file_basename}"])
.should eq [current_file_path]
end
it "does not list folders" do
subject.expand(["**/*"]).each do |path|
fail "#{path.inspect} should be a file" unless File.file?(path)
end
end
end
end
end

View File

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

View File

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

View File

@ -0,0 +1,113 @@
require "../../../spec_helper"
module Ameba::Rule::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

@ -0,0 +1,151 @@
require "../../../spec_helper"
module Ameba::Rule::Documentation
subject = Documentation.new
.tap(&.ignore_classes = false)
.tap(&.ignore_modules = false)
.tap(&.ignore_enums = false)
.tap(&.ignore_defs = false)
.tap(&.ignore_macros = false)
describe Documentation do
it "passes for undocumented private types" do
expect_no_issues subject, <<-CRYSTAL
private class Foo
def foo
end
end
private module Bar
def bar
end
end
private enum Baz
end
private def bat
end
private macro bag
end
CRYSTAL
end
it "passes for documented public types" do
expect_no_issues subject, <<-CRYSTAL
# Foo
class Foo
# foo
def foo
end
end
# Bar
module Bar
# bar
def bar
end
end
# Baz
enum Baz
end
# bat
def bat
end
# bag
macro bag
end
CRYSTAL
end
it "fails if there is an undocumented public type" do
expect_issue subject, <<-CRYSTAL
class Foo
# ^^^^^^^^^ error: Missing documentation
end
module Bar
# ^^^^^^^^^^ error: Missing documentation
end
enum Baz
# ^^^^^^^^ error: Missing documentation
end
def bat
# ^^^^^^^ error: Missing documentation
end
macro bag
# ^^^^^^^^^ error: Missing documentation
end
CRYSTAL
end
context "properties" do
describe "#ignore_classes" do
it "lets the rule to ignore method definitions if true" do
rule = Documentation.new
rule.ignore_classes = true
expect_no_issues rule, <<-CRYSTAL
class Foo
end
CRYSTAL
end
end
describe "#ignore_modules" do
it "lets the rule to ignore method definitions if true" do
rule = Documentation.new
rule.ignore_modules = true
expect_no_issues rule, <<-CRYSTAL
module Bar
end
CRYSTAL
end
end
describe "#ignore_enums" do
it "lets the rule to ignore method definitions if true" do
rule = Documentation.new
rule.ignore_enums = true
expect_no_issues rule, <<-CRYSTAL
enum Baz
end
CRYSTAL
end
end
describe "#ignore_defs" do
it "lets the rule to ignore method definitions if true" do
rule = Documentation.new
rule.ignore_defs = true
expect_no_issues rule, <<-CRYSTAL
def bat
end
CRYSTAL
end
end
describe "#ignore_macros" do
it "lets the rule to ignore macros if true" do
rule = Documentation.new
rule.ignore_macros = true
expect_no_issues rule, <<-CRYSTAL
macro bag
end
CRYSTAL
end
end
end
end
end

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

@ -7,8 +7,11 @@ module Ameba::Rule::Lint
it "passes for valid cases" do
expect_no_issues subject, <<-CRYSTAL
(1..3).index(1).not_nil!(:foo)
(1..3).rindex(1).not_nil!(:foo)
(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
@ -23,6 +26,28 @@ module Ameba::Rule::Lint
CRYSTAL
end
it "reports if there is an `rindex` call followed by `not_nil!`" do
source = expect_issue subject, <<-CRYSTAL
(1..3).rindex(1).not_nil!
# ^^^^^^^^^^^^^^^^^^ error: Use `rindex! {...}` instead of `rindex {...}.not_nil!`
CRYSTAL
expect_correction source, <<-CRYSTAL
(1..3).rindex!(1)
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!
@ -34,6 +59,17 @@ module Ameba::Rule::Lint
CRYSTAL
end
it "reports if there is an `rindex` call with block followed by `not_nil!`" do
source = expect_issue subject, <<-CRYSTAL
(1..3).rindex { |i| i > 2 }.not_nil!
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ error: Use `rindex! {...}` instead of `rindex {...}.not_nil!`
CRYSTAL
expect_correction source, <<-CRYSTAL
(1..3).rindex! { |i| i > 2 }
CRYSTAL
end
it "reports if there is a `find` call with block followed by `not_nil!`" do
source = expect_issue subject, <<-CRYSTAL
(1..3).find { |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,46 @@
require "../../../spec_helper"
module Ameba::Rule::Lint
subject = SpecFilename.new
describe SpecFilename do
it "passes if relative file path does not start with `spec/`" do
expect_no_issues subject, code: "", path: "src/spec/foo.cr"
expect_no_issues subject, code: "", path: "src/spec/foo/bar.cr"
end
it "passes if file extension is not `.cr`" do
expect_no_issues subject, code: "", path: "spec/foo.json"
expect_no_issues subject, code: "", path: "spec/foo/bar.json"
end
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

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

View File

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

@ -25,6 +25,7 @@ module Ameba::Rule::Style
(1..3).map { |l| l.to_i64 * l.to_i64 }
(1..3).map { |m| m.to_s[start: m.to_i64, count: 3]? }
(1..3).map { |n| n.to_s.split.map { |z| n.to_i * z.to_i }.join }
(1..3).map { |o| o.foo = foos[o.abs]? || 0 }
CRYSTAL
end

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

@ -45,7 +45,7 @@ module Ameba
include YAML::Serializable
@[YAML::Field(converter: Ameba::SeverityYamlConverter)]
property severity : Severity
property severity : Severity?
end
describe SeverityYamlConverter do

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
@ -43,6 +43,9 @@ module Ameba
description "Internal rule to test scopes"
end
def test(source, node : Crystal::VisibilityModifier, scope : AST::Scope)
end
def test(source, node : Crystal::ASTNode, scope : AST::Scope)
@scopes << scope
end
@ -89,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)
@ -256,6 +259,7 @@ module Ameba
Crystal::MacroLiteral,
Crystal::Expressions,
Crystal::ControlExpression,
Crystal::Call,
}
def initialize(node)
@ -279,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.
@ -62,11 +64,13 @@ module Ameba::AST
# Branch.of(assign_node, def_node)
# ```
def self.of(node : Crystal::ASTNode, parent_node : Crystal::ASTNode)
BranchVisitor.new(node).tap(&.accept parent_node).branch
BranchVisitor.new(node).tap(&.accept(parent_node)).branch
end
# :nodoc:
private class BranchVisitor < Crystal::Visitor
include Util
@current_branch : Crystal::ASTNode?
property branchable : Branchable?
@ -79,11 +83,12 @@ 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|
break if branch # branch found
@current_branch = branch_node if branch_node && !branch_node.nop?
branch_node.try &.accept(self)
end
@ -108,19 +113,11 @@ module Ameba::AST
true
end
def visit(node : Crystal::If)
def visit(node : Crystal::If | Crystal::Unless)
on_branchable_start node, node.cond, node.then, node.else
end
def end_visit(node : Crystal::If)
on_branchable_end node
end
def visit(node : Crystal::Unless)
on_branchable_start node, node.cond, node.then, node.else
end
def end_visit(node : Crystal::Unless)
def end_visit(node : Crystal::If | Crystal::Unless)
on_branchable_end node
end
@ -140,19 +137,11 @@ module Ameba::AST
on_branchable_end node
end
def visit(node : Crystal::While)
def visit(node : Crystal::While | Crystal::Until)
on_branchable_start node, node.cond, node.body
end
def end_visit(node : Crystal::While)
on_branchable_end node
end
def visit(node : Crystal::Until)
on_branchable_start node, node.cond, node.body
end
def end_visit(node : Crystal::Until)
def end_visit(node : Crystal::While | Crystal::Until)
on_branchable_end node
end
@ -187,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

@ -15,14 +15,15 @@ module Ameba::AST
class Branchable
include Util
getter branches = [] of Crystal::ASTNode
# The actual Crystal node.
getter node : Crystal::ASTNode
# Parent branchable (if any)
getter parent : Branchable?
# Array of branches
getter branches = [] of Crystal::ASTNode
# The actual Crystal node
getter node : Crystal::ASTNode
delegate to_s, to: @node
delegate location, to: @node
delegate end_location, to: @node

View File

@ -53,8 +53,11 @@ module Ameba::AST
case current_node = node
when Crystal::Expressions
control_flow_found = false
current_node.expressions.each do |exp|
unreachable_nodes << exp if control_flow_found
if control_flow_found
unreachable_nodes << exp
end
control_flow_found ||= !loop?(exp) && flow_expression?(exp, in_loop?)
end
when Crystal::BinaryOp

View File

@ -7,6 +7,9 @@ module Ameba::AST
# Whether the scope yields.
setter yields = false
# Scope visibility level
setter visibility : Crystal::Visibility?
# Link to local variables
getter variables = [] of Variable
@ -31,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
@ -43,7 +45,7 @@ module Ameba::AST
# scope = Scope.new(class_node, nil)
# ```
def initialize(@node, @outer_scope = nil)
@outer_scope.try &.inner_scopes.<<(self)
@outer_scope.try &.inner_scopes.<< self
end
# Creates a new variable in the current scope.
@ -94,7 +96,8 @@ module Ameba::AST
# scope.find_variable("foo")
# ```
def find_variable(name : String)
variables.find(&.name.==(name)) || outer_scope.try &.find_variable(name)
variables.find(&.name.==(name)) ||
outer_scope.try &.find_variable(name)
end
# Creates a new assignment for the variable.
@ -110,7 +113,8 @@ module Ameba::AST
# Returns `true` if current scope represents a block (or proc),
# `false` otherwise.
def block?
node.is_a?(Crystal::Block) || node.is_a?(Crystal::ProcLiteral)
node.is_a?(Crystal::Block) ||
node.is_a?(Crystal::ProcLiteral)
end
# Returns `true` if current scope represents a spawn block, e. g.
@ -121,15 +125,14 @@ module Ameba::AST
# end
# ```
def spawn_block?
return false unless node.is_a?(Crystal::Block)
call = node.as(Crystal::Block).call
call.try(&.name) == "spawn"
node.as?(Crystal::Block).try(&.call).try(&.name) == "spawn"
end
# Returns `true` if current scope sits inside a macro.
def in_macro?
(node.is_a?(Crystal::Macro) || node.is_a?(Crystal::MacroFor)) ||
(node.is_a?(Crystal::Macro) ||
node.is_a?(Crystal::MacroIf) ||
node.is_a?(Crystal::MacroFor)) ||
!!outer_scope.try(&.in_macro?)
end
@ -141,7 +144,8 @@ module Ameba::AST
# Returns `true` if type declaration variable is assigned in this scope.
def assigns_type_dec?(name)
type_dec_variables.any?(&.name.== name) || !!outer_scope.try(&.assigns_type_dec?(name))
type_dec_variables.any?(&.name.== name) ||
!!outer_scope.try(&.assigns_type_dec?(name))
end
# Returns `true` if and only if current scope represents some
@ -149,6 +153,7 @@ module Ameba::AST
def type_definition?
node.is_a?(Crystal::ClassDef) ||
node.is_a?(Crystal::ModuleDef) ||
node.is_a?(Crystal::EnumDef) ||
node.is_a?(Crystal::LibDef) ||
node.is_a?(Crystal::FunDef) ||
node.is_a?(Crystal::TypeDef) ||
@ -159,27 +164,35 @@ module Ameba::AST
# `false` otherwise.
def references?(variable : Variable, check_inner_scopes = true)
variable.references.any? do |reference|
return true if reference.scope == self
check_inner_scopes && inner_scopes.any?(&.references?(variable))
(reference.scope == self) ||
(check_inner_scopes && inner_scopes.any?(&.references?(variable)))
end || variable.used_in_macro?
end
# Returns `true` if current scope (or any of inner scopes) yields,
# `false` otherwise.
def yields?(check_inner_scopes = true)
return true if @yields
return inner_scopes.any?(&.yields?) if check_inner_scopes
false
@yields || (check_inner_scopes && inner_scopes.any?(&.yields?))
end
# Returns `true` if current scope is a def, `false` otherwise.
def def?
node.is_a?(Crystal::Def)
# Returns visibility of the current scope (could be inherited from the outer scope).
def visibility
@visibility || outer_scope.try(&.visibility)
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
@ -170,7 +173,7 @@ module Ameba::AST
private class MacroReferenceFinder < Crystal::Visitor
property? references = false
def initialize(node, @reference : String = reference)
def initialize(node, @reference : String)
node.accept self
end
@ -179,10 +182,6 @@ module Ameba::AST
val.to_s.includes?(@reference)
end
def visit(node : Crystal::ASTNode)
true
end
def visit(node : Crystal::MacroLiteral)
!(@references ||= includes_reference?(node.value))
end
@ -201,14 +200,20 @@ module Ameba::AST
includes_reference?(node.then) ||
includes_reference?(node.else))
end
def visit(node : Crystal::ASTNode)
true
end
end
private def update_assign_reference!
if @assign_before_reference.nil? &&
references.size <= assignments.size &&
assignments.none?(&.op_assign?)
@assign_before_reference = assignments.find { |ass| !ass.in_branch? }.try &.node
end
return if @assign_before_reference
return if references.size > assignments.size
return if assignments.any?(&.op_assign?)
@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

@ -8,11 +8,6 @@ module Ameba::AST
@loop_stack = [] of Crystal::ASTNode
# Creates a new flow expression visitor.
def initialize(@rule, @source)
@source.ast.accept self
end
# :nodoc:
def visit(node)
if flow_expression?(node, in_loop?)
@ -22,12 +17,7 @@ module Ameba::AST
end
# :nodoc:
def visit(node : Crystal::While)
on_loop_started(node)
end
# :nodoc:
def visit(node : Crystal::Until)
def visit(node : Crystal::While | Crystal::Until)
on_loop_started(node)
end
@ -37,12 +27,7 @@ module Ameba::AST
end
# :nodoc:
def end_visit(node : Crystal::While)
on_loop_ended(node)
end
# :nodoc:
def end_visit(node : Crystal::Until)
def end_visit(node : Crystal::While | Crystal::Until)
on_loop_ended(node)
end

View File

@ -1,34 +1,6 @@
require "./base_visitor"
module Ameba::AST
# List of nodes to be visited by Ameba's rules.
NODES = {
Alias,
IsA,
Assign,
Call,
Block,
Case,
ClassDef,
ClassVar,
Def,
EnumDef,
ExceptionHandler,
Expressions,
HashLiteral,
If,
InstanceVar,
LibDef,
ModuleDef,
NilLiteral,
StringInterpolation,
Unless,
Var,
When,
While,
Until,
}
# An AST Visitor that traverses the source and allows all nodes
# to be inspected by rules.
#
@ -36,13 +8,68 @@ module Ameba::AST
# visitor = Ameba::AST::NodeVisitor.new(rule, source)
# ```
class NodeVisitor < BaseVisitor
@[Flags]
enum Category
Macro
end
# List of nodes to be visited by Ameba's rules.
NODES = {
Alias,
Assign,
Block,
Call,
Case,
ClassDef,
ClassVar,
Def,
EnumDef,
ExceptionHandler,
Expressions,
HashLiteral,
If,
InstanceVar,
IsA,
LibDef,
ModuleDef,
MultiAssign,
NilLiteral,
StringInterpolation,
Unless,
Until,
Var,
When,
While,
}
@skip : Array(Crystal::ASTNode.class)?
def initialize(@rule, @source, skip = nil)
def self.category_to_node_classes(category : Category)
([] of Crystal::ASTNode.class).tap do |classes|
classes.push(
Crystal::Macro,
Crystal::MacroExpression,
Crystal::MacroIf,
Crystal::MacroFor,
) if category.macro?
end
end
def initialize(@rule, @source, *, skip : Category)
initialize @rule, @source,
skip: NodeVisitor.category_to_node_classes(skip)
end
def initialize(@rule, @source, *, skip : Array? = nil)
@skip = skip.try &.map(&.as(Crystal::ASTNode.class))
super @rule, @source
end
def visit(node : Crystal::VisibilityModifier)
node.exp.visibility = node.modifier
true
end
{% for name in NODES %}
# A visit callback for `Crystal::{{ name }}` node.
#

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

@ -16,35 +16,47 @@ module Ameba::AST
ProcLiteral,
Block,
Macro,
MacroIf,
MacroFor,
}
SPECIAL_NODE_NAMES = %w[super previous_def]
RECORD_NODE_NAME = "record"
@scope_queue = [] of Scope
@current_scope : Scope
@current_assign : Crystal::ASTNode?
@current_visibility : Crystal::Visibility?
@skip : Array(Crystal::ASTNode.class)?
def initialize(@rule, @source, skip = nil)
@skip = skip.try &.map(&.as(Crystal::ASTNode.class))
@current_scope = Scope.new(@source.ast) # top level scope
@source.ast.accept self
@scope_queue.each { |scope| @rule.test @source, scope.node, scope }
@skip = skip.try &.map(&.as(Crystal::ASTNode.class))
super @rule, @source
@scope_queue.each do |scope|
@rule.test @source, scope.node, scope
end
end
private def on_scope_enter(node)
return if skip?(node)
@current_scope = Scope.new(node, @current_scope)
scope = Scope.new(node, @current_scope)
scope.visibility = @current_visibility
@current_scope = scope
end
private def on_scope_end(node)
@scope_queue << @current_scope
@current_visibility = nil
# go up if this is not a top level scope
return unless outer_scope = @current_scope.outer_scope
@current_scope = outer_scope
if outer_scope = @current_scope.outer_scope
@current_scope = outer_scope
end
end
private def on_assign_end(target, node)
@ -64,6 +76,12 @@ module Ameba::AST
end
{% end %}
# :nodoc:
def visit(node : Crystal::VisibilityModifier)
@current_visibility = node.exp.visibility = node.modifier
true
end
# :nodoc:
def visit(node : Crystal::Yield)
@current_scope.yields = true
@ -83,6 +101,7 @@ module Ameba::AST
def end_visit(node : Crystal::Assign | Crystal::OpAssign)
on_assign_end(node.target, node)
@current_assign = nil
on_scope_end(node) if @current_scope.eql?(node)
end
@ -90,6 +109,7 @@ module Ameba::AST
def end_visit(node : Crystal::MultiAssign)
node.targets.each { |target| on_assign_end(target, node) }
@current_assign = nil
on_scope_end(node) if @current_scope.eql?(node)
end
@ -97,6 +117,7 @@ module Ameba::AST
def end_visit(node : Crystal::UninitializedVar)
on_assign_end(node.var, node)
@current_assign = nil
on_scope_end(node) if @current_scope.eql?(node)
end
@ -106,14 +127,17 @@ module Ameba::AST
@current_scope.add_variable(var)
@current_scope.add_type_dec_variable(node)
@current_assign = node.value unless node.value.nil?
@current_assign = node.value if node.value
end
# :nodoc:
def end_visit(node : Crystal::TypeDeclaration)
return unless (var = node.var).is_a?(Crystal::Var)
on_assign_end(var, node)
@current_assign = nil
on_scope_end(node) if @current_scope.eql?(node)
end
@ -129,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
@ -137,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
@ -146,24 +170,39 @@ module Ameba::AST
# :nodoc:
def visit(node : Crystal::Call)
scope = @current_scope
case
when @current_scope.def?
if node.name.in?(SPECIAL_NODE_NAMES) && node.args.empty?
@current_scope.arguments.each do |arg|
variable = arg.variable
variable.reference(variable.node, @current_scope).explicit = false
end
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

@ -14,7 +14,7 @@ module Ameba::Cli
raise "Invalid usage: Cannot explain an issue and autocorrect at the same time."
end
config = Config.load opts.config, opts.colors?
config = Config.load opts.config, opts.colors?, opts.skip_reading_config?
config.autocorrect = autocorrect
if globs = opts.globs
@ -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)
@ -44,13 +51,15 @@ module Ameba::Cli
end
private class Opts
property config = Config::PATH
property config : Path?
property formatter : Symbol | String | Nil
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
property? rules = false
property? all = false
property? colors = true
@ -66,17 +75,17 @@ 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
parser.on("-c", "--config PATH",
"Specify a configuration file") do |path|
opts.config = path unless opts.config.empty?
opts.config = Path[path] unless path.empty?
end
parser.on("-f", "--format FORMATTER",
@ -105,7 +114,7 @@ module Ameba::Cli
parser.on("--gen-config",
"Generate a configuration file acting as a TODO list") do
opts.formatter = :todo
opts.config = ""
opts.skip_reading_config = true
end
parser.on("--fail-level SEVERITY",
@ -118,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
@ -151,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
@ -182,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

@ -10,8 +10,30 @@ require "./glob_utils"
# config.formatter = my_formatter
# ```
#
# By default config loads `.ameba.yml` file in a current directory.
# By default config loads `.ameba.yml` file located in a current
# working directory.
#
# If it cannot be found until reaching the root directory, then it will be
# searched for in the users global config locations, which consists of a
# dotfile or a config file inside the XDG Base Directory specification.
#
# - `~/.ameba.yml`
# - `$XDG_CONFIG_HOME/ameba/config.yml` (expands to `~/.config/ameba/config.yml`
# if `$XDG_CONFIG_HOME` is not set)
#
# If both files exist, the dotfile will be selected.
#
# As an example, if Ameba is invoked from inside `/path/to/project/lib/utils`,
# then it will use the config as specified inside the first of the following files:
#
# - `/path/to/project/lib/utils/.ameba.yml`
# - `/path/to/project/lib/.ameba.yml`
# - `/path/to/project/.ameba.yml`
# - `/path/to/.ameba.yml`
# - `/path/.ameba.yml`
# - `/.ameba.yml`
# - `~/.ameba.yml`
# - `~/.config/ameba/config.yml`
class Ameba::Config
include GlobUtils
@ -24,7 +46,14 @@ class Ameba::Config
json: Formatter::JSONFormatter,
}
PATH = ".ameba.yml"
XDG_CONFIG_HOME = ENV.fetch("XDG_CONFIG_HOME", "~/.config")
FILENAME = ".ameba.yml"
DEFAULT_PATH = Path[Dir.current] / FILENAME
DEFAULT_PATHS = {
Path["~"] / FILENAME,
Path[XDG_CONFIG_HOME] / "ameba/config.yml",
}
DEFAULT_GLOBS = %w(
**/*.cr
@ -68,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`.
@ -77,12 +107,39 @@ class Ameba::Config
# ```
# config = Ameba::Config.load
# ```
def self.load(path = PATH, colors = true)
def self.load(path = nil, colors = true, skip_reading_config = false)
Colorize.enabled = colors
content = File.exists?(path) ? File.read path : "{}"
content = if skip_reading_config
"{}"
else
read_config(path) || "{}"
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.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)
end
end
protected def self.each_config_path(&)
path = Path[DEFAULT_PATH].expand(home: true)
search_paths = path.parents
search_paths.reverse_each do |search_path|
yield search_path / FILENAME
end
DEFAULT_PATHS.each do |default_path|
yield default_path
end
end
def self.formatter_names
@ -147,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|
@ -185,22 +242,23 @@ class Ameba::Config
# :nodoc:
module RuleConfig
# 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 %}
@ -218,7 +276,7 @@ class Ameba::Config
{% converter = SeverityYamlConverter %}
{% end %}
{% if type == nil %}
{% unless type %}
{% if value.is_a? BoolLiteral %}
{% type = Bool %}
{% elsif value.is_a? StringLiteral %}
@ -228,23 +286,23 @@ class Ameba::Config
{% type = Int32 %}
{% elsif value.kind == :i64 %}
{% type = Int64 %}
{% elsif value.kind == :i128 %}
{% type = Int128 %}
{% elsif value.kind == :f32 %}
{% type = Float32 %}
{% elsif value.kind == :f64 %}
{% type = Float64 %}
{% end %}
{% end %}
{% type = Nil if type == nil %}
{% end %}
{% properties[name] = {key: key, default: value, type: type, converter: converter} %}
@[YAML::Field(key: {{ key }}, converter: {{ converter }}, type: {{ type }})]
@[YAML::Field(key: {{ key }}, converter: {{ converter }})]
{% if type == Bool %}
property? {{ name }} : {{ type }} = {{ value }}
property? {{ name }}{{ " : #{type}".id if type }} = {{ value }}
{% else %}
property {{ name }} : {{ type }} = {{ value }}
property {{ name }}{{ " : #{type}".id if type }} = {{ value }}
{% end %}
{% end %}
@ -266,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::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

@ -1,6 +1,8 @@
module Ameba
# Helper module that is utilizes helpers for working with globs.
module GlobUtils
extend self
# Returns all files that match specified globs.
# Globs can have wildcards or be rejected:
#
@ -20,10 +22,13 @@ module Ameba
# expand(["spec/*.cr", "src"]) # => all files in src folder + first level specs
# ```
def expand(globs)
globs.flat_map do |glob|
glob += "/**/*.cr" if File.directory?(glob)
Dir[glob]
end.uniq!
globs
.flat_map do |glob|
glob += "/**/*.cr" if File.directory?(glob)
Dir[glob]
end
.uniq!
.select! { |path| File.file?(path) }
end
private def rejected_globs(globs)

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
@ -112,8 +113,9 @@ module Ameba::Rule
name.hash
end
# 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

@ -0,0 +1,80 @@
module Ameba::Rule::Documentation
# A rule that enforces documentation for public types:
# modules, classes, enums, methods and macros.
#
# YAML configuration example:
#
# ```
# Documentation/Documentation:
# Enabled: true
# IgnoreClasses: false
# IgnoreModules: true
# IgnoreEnums: false
# IgnoreDefs: true
# IgnoreMacros: false
# IgnoreMacroHooks: true
# ```
class Documentation < Base
properties do
enabled false
description "Enforces public types to be documented"
ignore_classes false
ignore_modules true
ignore_enums false
ignore_defs true
ignore_macros false
ignore_macro_hooks true
end
MSG = "Missing documentation"
MACRO_HOOK_NAMES = %w[
inherited
included extended
method_missing method_added
finished
]
def test(source)
AST::ScopeVisitor.new self, source
end
def test(source, node : Crystal::ClassDef, scope : AST::Scope)
ignore_classes? || check_missing_doc(source, node, scope)
end
def test(source, node : Crystal::ModuleDef, scope : AST::Scope)
ignore_modules? || check_missing_doc(source, node, scope)
end
def test(source, node : Crystal::EnumDef, scope : AST::Scope)
ignore_enums? || check_missing_doc(source, node, scope)
end
def test(source, node : Crystal::Def, scope : AST::Scope)
ignore_defs? || check_missing_doc(source, node, scope)
end
def test(source, node : Crystal::Macro, scope : AST::Scope)
return if ignore_macro_hooks? && node.name.in?(MACRO_HOOK_NAMES)
ignore_macros? || check_missing_doc(source, node, scope)
end
private def check_missing_doc(source, node, scope)
# bail out if the node is not public,
# i.e. `private def foo`
return if !node.visibility.public?
# bail out if the scope is not public,
# i.e. `def bar` inside `private class Foo`
return if (visibility = scope.visibility) && !visibility.public?
# bail out if the node has the documentation present
return if node.doc.presence
issue_for(node, MSG)
end
end
end

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 : Array(String) = %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

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