fastererer 0.12.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +75 -2
- data/README.md +97 -8
- data/config/locales/en.yml +60 -0
- data/lib/fastererer/analyzer.rb +28 -53
- data/lib/fastererer/cli.rb +21 -4
- data/lib/fastererer/explanation.rb +44 -0
- data/lib/fastererer/file_traverser.rb +43 -112
- data/lib/fastererer/finding.rb +18 -0
- data/lib/fastererer/formatters/base.rb +32 -0
- data/lib/fastererer/formatters/json_formatter.rb +48 -0
- data/lib/fastererer/formatters/rdjsonl_formatter.rb +34 -0
- data/lib/fastererer/formatters/text_formatter.rb +63 -0
- data/lib/fastererer/formatters/text_statistics.rb +45 -0
- data/lib/fastererer/formatters.rb +24 -0
- data/lib/fastererer/method_call.rb +139 -75
- data/lib/fastererer/method_definition.rb +61 -48
- data/lib/fastererer/offense.rb +5 -63
- data/lib/fastererer/offense_collector.rb +1 -1
- data/lib/fastererer/painter.rb +2 -1
- data/lib/fastererer/parser.rb +8 -4
- data/lib/fastererer/receiver.rb +14 -0
- data/lib/fastererer/report.rb +10 -0
- data/lib/fastererer/rescue_call.rb +10 -12
- data/lib/fastererer/rule_catalog.rb +83 -0
- data/lib/fastererer/rule_name.rb +17 -0
- data/lib/fastererer/scanners/for_loop_scanner.rb +23 -0
- data/lib/fastererer/scanners/method_call_scanner.rb +13 -17
- data/lib/fastererer/scanners/method_definition_scanner.rb +57 -29
- data/lib/fastererer/scanners/offensive.rb +2 -6
- data/lib/fastererer/scanners/rescue_call_scanner.rb +1 -1
- data/lib/fastererer/scanners/symbol_to_proc_check.rb +21 -8
- data/lib/fastererer/version.rb +1 -1
- metadata +19 -11
- data/.fastererer.yml +0 -27
- data/.ruby-version +0 -1
- data/CODE_OF_CONDUCT.md +0 -18
- data/CONTRIBUTING.md +0 -63
- data/Rakefile +0 -12
- data/SECURITY.md +0 -20
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ba7c93c8bf5af26d77535e03595b6e56720bd31f865d23714a36e02f27c21a1a
|
|
4
|
+
data.tar.gz: 6eedb452c7ec6ac71ff7c9b145060cdccf9a5ab7b6837ad2797e02705fc15957
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 62425177d92da9debe1b47babac4f1a634633a6e67c4665c0ed65093932515a35547e067c9f72a60b9abe674c2af367a23bf86204ff994adb6906762075461bd
|
|
7
|
+
data.tar.gz: 30a136f9742eefbd5c1499c4dae8c4d92e64541984f7caed36386e2aecda9b0a90ef1acbbd0c9c0fc5c98f9b4bb653cf685e2049ed76850f7b49f25446c7d6c7
|
data/CHANGELOG.md
CHANGED
|
@@ -4,10 +4,75 @@ All notable changes to this project are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog] and this project adheres to [Semantic Versioning].
|
|
6
6
|
|
|
7
|
-
Versions 0.11.0 and earlier were released as [`fasterer`](https://github.com/DamirSvrtan/fasterer),
|
|
7
|
+
Versions 0.11.0 and earlier were released as [`fasterer`](https://github.com/DamirSvrtan/fasterer),
|
|
8
|
+
prior to the fork. From 0.12.0 onward, this is `fastererer`, maintained by ExtractableMedia.
|
|
8
9
|
|
|
9
10
|
## [Unreleased]
|
|
10
11
|
|
|
12
|
+
## [1.1.0] - 2026-09-04
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- [#40]: A `-f`/`--format` flag with machine-readable output formats alongside the default `text`:
|
|
17
|
+
`json` (one document with a run `summary` and a flat `offenses` array) and `rdjsonl` (the
|
|
18
|
+
reviewdog Diagnostic Format, one record per offense, ready to pipe into reviewdog for inline PR
|
|
19
|
+
comments). Machine formats write only their payload to stdout and route diagnostics to stderr.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- [#77]: Parse errors and the "No such file or directory" message now go to STDERR instead of
|
|
24
|
+
STDOUT. STDOUT carries only offense lines and the statistics line, so it can be piped, grepped or
|
|
25
|
+
redirected on its own.
|
|
26
|
+
- [#84]: A path that does not exist now exits with status `2` instead of `0`. Status `1` still
|
|
27
|
+
means "offenses were found", so a CI job or wrapper script can tell a mistyped path from a real
|
|
28
|
+
finding. The exit codes are documented in the README.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- [#77]: The "N offenses detected" statistics line no longer counts offenses suppressed by
|
|
33
|
+
`speedups: <rule>: false` in `.fastererer.yml`. The total previously included suppressed
|
|
34
|
+
offenses, so it could exceed the number of offense lines printed; the two now agree.
|
|
35
|
+
- [#84]: An unknown flag now prints its message and exits `2` instead of dumping a Ruby backtrace
|
|
36
|
+
and exiting `1`. Exiting `1` made a mistyped flag indistinguishable from a real finding.
|
|
37
|
+
- [#84]: A directory whose name contains glob metacharacters (`pkg[1]`, `a{b}`) is now scanned.
|
|
38
|
+
The name was interpolated into the glob pattern, so such a directory silently reported
|
|
39
|
+
`0 files inspected` and exited `0`. Passing an absolute directory path no longer raises
|
|
40
|
+
`ArgumentError` either.
|
|
41
|
+
- [#84]: A missing path no longer reports a clean scan. `fastererer no_such_path` printed
|
|
42
|
+
`1 file inspected, 0 offenses detected` and exited `0`, so a renamed or mistyped directory in CI
|
|
43
|
+
stopped linting while the build stayed green. The statistics line now reads
|
|
44
|
+
`0 files inspected, 0 offenses detected` and the run fails.
|
|
45
|
+
|
|
46
|
+
## [1.0.0] - 2026-06-05
|
|
47
|
+
|
|
48
|
+
First stable release of `fastererer` after the fork.
|
|
49
|
+
|
|
50
|
+
### Changed
|
|
51
|
+
|
|
52
|
+
- [#9]: Migrate from `ruby_parser` to Ruby's native Prism parser. Prism ships with Ruby itself,
|
|
53
|
+
so this drops the `ruby_parser`, `sexp_processor`, and `racc` dependencies and keeps the
|
|
54
|
+
analyzer in step with current and future Ruby syntax, including Ruby 4.0.
|
|
55
|
+
|
|
56
|
+
### Added
|
|
57
|
+
|
|
58
|
+
- [#9]: Detect speedups reached through a safe-navigation chain (e.g. `arr&.first`,
|
|
59
|
+
`arr&.select { … }.first`), which the previous S-expression scanner skipped.
|
|
60
|
+
- [#9]: Detect speedups inside `rescue` bodies — getter-vs-`attr_reader`,
|
|
61
|
+
setter-vs-`attr_writer`, and proc-call-vs-`yield` offenses are now scanned in rescue clauses.
|
|
62
|
+
- [#15]: Offense output now mirrors RuboCop's format with a `Performance/RuleName` rule name and a
|
|
63
|
+
[fast-ruby](https://github.com/fastruby/fast-ruby) documentation link, e.g.
|
|
64
|
+
`path:line: W: Performance/ForLoopVsEach: For loop is slower than using each. (url)`.
|
|
65
|
+
|
|
66
|
+
### Fixed
|
|
67
|
+
|
|
68
|
+
- [#9]: Block calls inside a nested scope (a singleton class or a nested method definition) are
|
|
69
|
+
no longer misattributed to the enclosing method.
|
|
70
|
+
- [#9]: Unparseable source now raises a clear error instead of silently reporting no offenses.
|
|
71
|
+
- [#28]: `hash_merge_bang_vs_hash_brackets` now flags `Hash#update`, an alias of `Hash#merge!`, but
|
|
72
|
+
only when the receiver is provably a Hash (a `{}` literal, `Hash.new`, or `Hash[...]`). Because
|
|
73
|
+
`update` is also defined on `ActiveRecord`, `ActionController::Parameters`, and others, a bare
|
|
74
|
+
`obj.update(k => v)` is left untouched to avoid false positives.
|
|
75
|
+
|
|
11
76
|
## [0.12.0] - 2026-05-18
|
|
12
77
|
|
|
13
78
|
### Added
|
|
@@ -106,8 +171,16 @@ Versions 0.11.0 and earlier were released as [`fasterer`](https://github.com/Dam
|
|
|
106
171
|
|
|
107
172
|
[Keep a Changelog]: https://keepachangelog.com
|
|
108
173
|
[Semantic Versioning]: https://semver.org/spec/v2.0.0.html
|
|
109
|
-
[Unreleased]: https://github.com/ExtractableMedia/fastererer/compare/
|
|
174
|
+
[Unreleased]: https://github.com/ExtractableMedia/fastererer/compare/v1.1.0...HEAD
|
|
175
|
+
[1.1.0]: https://github.com/ExtractableMedia/fastererer/compare/v1.0.0...v1.1.0
|
|
176
|
+
[1.0.0]: https://github.com/ExtractableMedia/fastererer/compare/v0.12.0...v1.0.0
|
|
110
177
|
[0.12.0]: https://github.com/ExtractableMedia/fastererer/compare/v0.11.0...v0.12.0
|
|
111
178
|
[#1]: https://github.com/ExtractableMedia/fastererer/pull/1
|
|
112
179
|
[#2]: https://github.com/ExtractableMedia/fastererer/pull/2
|
|
180
|
+
[#9]: https://github.com/ExtractableMedia/fastererer/pull/9
|
|
181
|
+
[#15]: https://github.com/ExtractableMedia/fastererer/pull/15
|
|
182
|
+
[#28]: https://github.com/ExtractableMedia/fastererer/issues/28
|
|
183
|
+
[#40]: https://github.com/ExtractableMedia/fastererer/issues/40
|
|
113
184
|
[#42]: https://github.com/ExtractableMedia/fastererer/pull/42
|
|
185
|
+
[#77]: https://github.com/ExtractableMedia/fastererer/issues/77
|
|
186
|
+
[#84]: https://github.com/ExtractableMedia/fastererer/pull/84
|
data/README.md
CHANGED
|
@@ -49,22 +49,78 @@ bundle exec fastererer app/models
|
|
|
49
49
|
bundle exec fastererer app/models/post.rb
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
Fastererer exits with status `1` when offenses are found
|
|
52
|
+
Fastererer exits with status `1` when offenses are found and `2` when the path it was given does not
|
|
53
|
+
exist, making it suitable for CI. See [Exit codes](#exit-codes).
|
|
53
54
|
|
|
54
55
|
## Example output
|
|
55
56
|
|
|
57
|
+
Each offense is reported on a single line, following the same shape as
|
|
58
|
+
RuboCop and its plugins (`path:line: SEVERITY: Department/RuleName: message.
|
|
59
|
+
(url)`), so the rule name and a link to documentation are always visible:
|
|
60
|
+
|
|
56
61
|
```text
|
|
57
|
-
app/models/post.rb:57 Array#select.first is slower than Array#detect.
|
|
58
|
-
app/models/post.rb:61 Array#select.first is slower than Array#detect.
|
|
62
|
+
app/models/post.rb:57: W: Performance/SelectFirstVsDetect: Array#select.first is slower than Array#detect. (https://github.com/fastruby/fast-ruby#enumerabledetect-vs-enumerableselectfirst-code)
|
|
63
|
+
app/models/post.rb:61: W: Performance/SelectFirstVsDetect: Array#select.first is slower than Array#detect. (https://github.com/fastruby/fast-ruby#enumerabledetect-vs-enumerableselectfirst-code)
|
|
64
|
+
|
|
65
|
+
db/seeds/cities.rb:15: W: Performance/KeysEachVsEachKey: Hash#keys.each is slower than Hash#each_key. N.B. Hash#each_key cannot be used if the hash is modified during the each block. (https://github.com/fastruby/fast-ruby#hasheach_key-instead-of-hashkeyseach-code)
|
|
66
|
+
|
|
67
|
+
test/options_test.rb:84: W: Performance/HashMergeBangVsHashBrackets: Hash#merge! with one argument is slower than Hash#[]. (https://github.com/fastruby/fast-ruby#hashmerge-vs-hash-code)
|
|
68
|
+
|
|
69
|
+
test/module_test.rb:272: W: Performance/RescueVsRespondTo: Don't rescue NoMethodError, rather check with respond_to?. (https://github.com/fastruby/fast-ruby#beginrescue-vs-respond_to-for-control-flow-code)
|
|
70
|
+
|
|
71
|
+
spec/cache/mem_cache_store_spec.rb:161: W: Performance/GsubVsTr: Using tr is faster than gsub when replacing a single character in a string with another single character. (https://github.com/fastruby/fast-ruby#stringgsub-vs-stringtr-code)
|
|
72
|
+
```
|
|
59
73
|
|
|
60
|
-
|
|
61
|
-
|
|
74
|
+
The rule name (e.g. `Performance/SelectFirstVsDetect`) is derived from the
|
|
75
|
+
underlying snake_case rule key (e.g. `select_first_vs_detect`), which is what
|
|
76
|
+
you reference in `.fastererer.yml` under `speedups:` to disable a rule.
|
|
77
|
+
Descriptions and documentation URLs live in `config/locales/en.yml`.
|
|
78
|
+
|
|
79
|
+
## Output formats
|
|
80
|
+
|
|
81
|
+
By default fastererer prints the human-readable text shown above. Pass `-f`/`--format` to emit
|
|
82
|
+
machine-readable output for editors, log aggregators, or CI reporters instead:
|
|
83
|
+
|
|
84
|
+
```shell
|
|
85
|
+
fastererer --format=json # single JSON document, includes a run summary
|
|
86
|
+
fastererer --format=rdjsonl # reviewdog JSON Lines, one record per offense
|
|
87
|
+
fastererer --format=text # the default
|
|
88
|
+
```
|
|
62
89
|
|
|
63
|
-
|
|
90
|
+
Machine formats write only their payload to stdout; diagnostics (parse errors, a missing path)
|
|
91
|
+
go to stderr, so `fastererer --format=json > findings.json` captures clean JSON. An unrecognized
|
|
92
|
+
format name is a usage error: the name is reported on stderr and fastererer exits `2` without
|
|
93
|
+
scanning.
|
|
94
|
+
|
|
95
|
+
`--format=json` produces one document with a `summary` of run counts and a flat `offenses` array
|
|
96
|
+
(each offense carries `path`, `line`, `rule`, `rule_key`, `message`, and `url`). `rule` is the
|
|
97
|
+
display name; `rule_key` is the identifier to write under `speedups:` in `.fastererer.yml`, so a
|
|
98
|
+
consumer can offer to silence a rule without guessing at the name:
|
|
99
|
+
|
|
100
|
+
```json
|
|
101
|
+
{
|
|
102
|
+
"metadata": { "fastererer_version": "1.0.0" },
|
|
103
|
+
"summary": { "offense_count": 1, "inspected_file_count": 12, "unparsable_file_count": 0 },
|
|
104
|
+
"offenses": [
|
|
105
|
+
{
|
|
106
|
+
"path": "app/models/post.rb",
|
|
107
|
+
"line": 57,
|
|
108
|
+
"rule": "Performance/SelectFirstVsDetect",
|
|
109
|
+
"rule_key": "select_first_vs_detect",
|
|
110
|
+
"message": "Array#select.first is slower than Array#detect",
|
|
111
|
+
"url": "https://github.com/fastruby/fast-ruby#enumerabledetect-vs-enumerableselectfirst-code"
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
}
|
|
115
|
+
```
|
|
64
116
|
|
|
65
|
-
|
|
117
|
+
`--format=rdjsonl` follows the [reviewdog Diagnostic Format][rdf], one JSON object per line, ready
|
|
118
|
+
to pipe straight into reviewdog (see [CI integration](#ci-integration)). `code.value` carries the
|
|
119
|
+
rule key rather than the display name, because reviewdog renders it into the pull request comment
|
|
120
|
+
and the key is the name a reader can act on — the one to add under `speedups:` in `.fastererer.yml`:
|
|
66
121
|
|
|
67
|
-
|
|
122
|
+
```json
|
|
123
|
+
{"message":"Array#select.first is slower than Array#detect","location":{"path":"app/models/post.rb","range":{"start":{"line":57}}},"severity":"WARNING","code":{"value":"select_first_vs_detect","url":"https://github.com/fastruby/fast-ruby#enumerabledetect-vs-enumerableselectfirst-code"}}
|
|
68
124
|
```
|
|
69
125
|
|
|
70
126
|
## Configuration
|
|
@@ -114,6 +170,38 @@ step:
|
|
|
114
170
|
run: bundle exec fastererer
|
|
115
171
|
```
|
|
116
172
|
|
|
173
|
+
### Exit codes
|
|
174
|
+
|
|
175
|
+
| Status | Meaning |
|
|
176
|
+
| ------ | ------- |
|
|
177
|
+
| `0` | The scan completed and no offenses were found |
|
|
178
|
+
| `1` | The scan completed and offenses were found |
|
|
179
|
+
| `2` | Usage error — an unknown flag or format, or a path that does not exist, so nothing was scanned |
|
|
180
|
+
|
|
181
|
+
Status `2` is kept distinct from `1` so a wrapper script can tell "the tool ran and found problems"
|
|
182
|
+
from "you invoked me wrong". A renamed directory, a mistyped flag or an unknown `--format` value
|
|
183
|
+
fails the build instead of reporting a clean scan.
|
|
184
|
+
|
|
185
|
+
### Inline PR comments with reviewdog
|
|
186
|
+
|
|
187
|
+
The `rdjsonl` format is consumed natively by [reviewdog](https://github.com/reviewdog/reviewdog),
|
|
188
|
+
which can post findings as inline GitHub PR review comments:
|
|
189
|
+
|
|
190
|
+
```shell
|
|
191
|
+
bundle exec fastererer --format=rdjsonl \
|
|
192
|
+
| reviewdog -f=rdjsonl -name=fastererer -filter-mode=nofilter -reporter=github-pr-review
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
`-filter-mode=nofilter` reports every finding, not only those on lines the pull request touched,
|
|
196
|
+
which is what reviewdog does by default. Findings outside the diff cannot be posted as inline
|
|
197
|
+
review comments — GitHub's review API does not allow it — so reviewdog falls back to check
|
|
198
|
+
annotations for those. `-name=fastererer` attributes the comments, since the rdjsonl record carries
|
|
199
|
+
no tool name of its own. `-reporter=github-pr-review` needs `pull-requests: write`.
|
|
200
|
+
|
|
201
|
+
Run fastererer from the repository root and let the path default to `.`. reviewdog matches findings
|
|
202
|
+
against the pull request by repository-relative path, so passing an absolute directory emits
|
|
203
|
+
absolute paths that will not match — and puts the runner's directory layout in a public comment.
|
|
204
|
+
|
|
117
205
|
Color output is auto-disabled when STDOUT isn't a TTY, when `NO_COLOR` is set (see
|
|
118
206
|
[no-color.org](https://no-color.org/)), or when `--no-color` is passed — so CI logs, piped output
|
|
119
207
|
(`fastererer | grep`), and editor integrations get plain text without configuration.
|
|
@@ -165,5 +253,6 @@ for the idiom catalog that drives the speed checks.
|
|
|
165
253
|
[fasterer]: https://github.com/DamirSvrtan/fasterer
|
|
166
254
|
[issues]: https://github.com/ExtractableMedia/fastererer/issues
|
|
167
255
|
[prism]: https://github.com/ruby/prism
|
|
256
|
+
[rdf]: https://github.com/reviewdog/reviewdog/tree/master/proto/rdf
|
|
168
257
|
[roadmap-project]: https://github.com/orgs/ExtractableMedia/projects/1
|
|
169
258
|
[sferik-talk]: https://speakerdeck.com/sferik/writing-fast-ruby
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
en:
|
|
2
|
+
fastererer:
|
|
3
|
+
rules:
|
|
4
|
+
rescue_vs_respond_to:
|
|
5
|
+
description: "Don't rescue NoMethodError, rather check with respond_to?"
|
|
6
|
+
url: "https://github.com/fastruby/fast-ruby#beginrescue-vs-respond_to-for-control-flow-code"
|
|
7
|
+
module_eval:
|
|
8
|
+
description: "Using module_eval is slower than define_method"
|
|
9
|
+
url: "https://github.com/fastruby/fast-ruby#define_method-vs-module_eval-for-defining-methods-code"
|
|
10
|
+
shuffle_first_vs_sample:
|
|
11
|
+
description: "Array#shuffle.first is slower than Array#sample"
|
|
12
|
+
url: "https://github.com/fastruby/fast-ruby#arrayshufflefirst-vs-arraysample-code"
|
|
13
|
+
for_loop_vs_each:
|
|
14
|
+
description: "For loop is slower than using each"
|
|
15
|
+
url: "https://github.com/fastruby/fast-ruby#enumerableeach-vs-for-loop-code"
|
|
16
|
+
each_with_index_vs_while:
|
|
17
|
+
description: "Using each_with_index is slower than while loop"
|
|
18
|
+
url: "https://github.com/fastruby/fast-ruby#enumerableeach_with_index-vs-while-loop-code"
|
|
19
|
+
map_flatten_vs_flat_map:
|
|
20
|
+
description: "Array#map.flatten(1) is slower than Array#flat_map"
|
|
21
|
+
url: "https://github.com/fastruby/fast-ruby#enumerablemaparrayflatten-vs-enumerableflat_map-code"
|
|
22
|
+
reverse_each_vs_reverse_each:
|
|
23
|
+
description: "Array#reverse.each is slower than Array#reverse_each"
|
|
24
|
+
url: "https://github.com/fastruby/fast-ruby#enumerablereverseeach-vs-enumerablereverse_each-code"
|
|
25
|
+
select_first_vs_detect:
|
|
26
|
+
description: "Array#select.first is slower than Array#detect"
|
|
27
|
+
url: "https://github.com/fastruby/fast-ruby#enumerabledetect-vs-enumerableselectfirst-code"
|
|
28
|
+
sort_vs_sort_by:
|
|
29
|
+
description: "Enumerable#sort is slower than Enumerable#sort_by"
|
|
30
|
+
url: "https://github.com/fastruby/fast-ruby#enumerablesort-vs-enumerablesort_by-code"
|
|
31
|
+
fetch_with_argument_vs_block:
|
|
32
|
+
description: "Hash#fetch with second argument is slower than Hash#fetch with block"
|
|
33
|
+
url: "https://github.com/fastruby/fast-ruby#hashfetch-with-argument-vs-hashfetch--block-code"
|
|
34
|
+
keys_each_vs_each_key:
|
|
35
|
+
description: "Hash#keys.each is slower than Hash#each_key. N.B. Hash#each_key cannot be used if the hash is modified during the each block"
|
|
36
|
+
url: "https://github.com/fastruby/fast-ruby#hasheach_key-instead-of-hashkeyseach-code"
|
|
37
|
+
hash_merge_bang_vs_hash_brackets:
|
|
38
|
+
description: "Hash#merge!/#update with one argument is slower than Hash#[]"
|
|
39
|
+
url: "https://github.com/fastruby/fast-ruby#hashmerge-vs-hash-code"
|
|
40
|
+
block_vs_symbol_to_proc:
|
|
41
|
+
description: "Calling argumentless methods within blocks is slower than using symbol to proc"
|
|
42
|
+
url: "https://github.com/fastruby/fast-ruby#block-vs-symbolto_proc-code"
|
|
43
|
+
proc_call_vs_yield:
|
|
44
|
+
description: "Calling blocks with call is slower than yielding"
|
|
45
|
+
url: "https://github.com/fastruby/fast-ruby#proccall-and-block-arguments-vs-yield-code"
|
|
46
|
+
gsub_vs_tr:
|
|
47
|
+
description: "Using tr is faster than gsub when replacing a single character in a string with another single character"
|
|
48
|
+
url: "https://github.com/fastruby/fast-ruby#stringgsub-vs-stringtr-code"
|
|
49
|
+
select_last_vs_reverse_detect:
|
|
50
|
+
description: "Array#select.last is slower than Array#reverse.detect"
|
|
51
|
+
url: "https://github.com/fastruby/fast-ruby#enumerableselectlast-vs-enumerablereversedetect-code"
|
|
52
|
+
getter_vs_attr_reader:
|
|
53
|
+
description: "Use attr_reader for reading ivars"
|
|
54
|
+
url: "https://github.com/fastruby/fast-ruby#attr_accessor-vs-getter-and-setter-code"
|
|
55
|
+
setter_vs_attr_writer:
|
|
56
|
+
description: "Use attr_writer for writing to ivars"
|
|
57
|
+
url: "https://github.com/fastruby/fast-ruby#attr_accessor-vs-getter-and-setter-code"
|
|
58
|
+
include_vs_cover_on_range:
|
|
59
|
+
description: "Use #cover? instead of #include? on ranges"
|
|
60
|
+
url: "https://github.com/fastruby/fast-ruby#cover-vs-include-code"
|
data/lib/fastererer/analyzer.rb
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'prism'
|
|
3
4
|
require 'fastererer/method_definition'
|
|
4
5
|
require 'fastererer/method_call'
|
|
5
6
|
require 'fastererer/rescue_call'
|
|
7
|
+
require 'fastererer/offense'
|
|
6
8
|
require 'fastererer/offense_collector'
|
|
7
9
|
require 'fastererer/parser'
|
|
8
10
|
require 'fastererer/scanners/method_call_scanner'
|
|
9
|
-
require 'fastererer/scanners/rescue_call_scanner'
|
|
10
11
|
require 'fastererer/scanners/method_definition_scanner'
|
|
12
|
+
require 'fastererer/scanners/for_loop_scanner'
|
|
13
|
+
require 'fastererer/scanners/rescue_call_scanner'
|
|
11
14
|
|
|
12
15
|
module Fastererer
|
|
13
16
|
class Analyzer
|
|
@@ -20,75 +23,47 @@ module Fastererer
|
|
|
20
23
|
end
|
|
21
24
|
|
|
22
25
|
def scan
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
root_node = Fastererer::Parser.parse(@file_content)
|
|
27
|
+
root_node.accept(AnalyzerVisitor.new(errors))
|
|
25
28
|
end
|
|
26
29
|
|
|
27
30
|
def errors
|
|
28
31
|
@errors ||= Fastererer::OffenseCollector.new
|
|
29
32
|
end
|
|
33
|
+
end
|
|
30
34
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
token = sexp_tree.first
|
|
37
|
-
scan_by_token(token, sexp_tree)
|
|
38
|
-
|
|
39
|
-
if %i[call iter].include?(token)
|
|
40
|
-
descend_into_call(sexp_tree)
|
|
41
|
-
else
|
|
42
|
-
sexp_tree.each { |element| traverse_sexp_tree(element) }
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def descend_into_call(sexp_tree)
|
|
47
|
-
method_call = MethodCall.new(sexp_tree)
|
|
48
|
-
traverse_sexp_tree(method_call.receiver_element) if method_call.receiver_element
|
|
49
|
-
traverse_sexp_tree(method_call.arguments_element)
|
|
50
|
-
traverse_sexp_tree(method_call.block_body) if method_call.block?
|
|
35
|
+
class AnalyzerVisitor < Prism::Visitor
|
|
36
|
+
def initialize(offenses)
|
|
37
|
+
super()
|
|
38
|
+
@offenses = offenses
|
|
51
39
|
end
|
|
52
40
|
|
|
53
|
-
def
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
scan_method_definitions(element)
|
|
57
|
-
when :call, :iter
|
|
58
|
-
scan_method_calls(element)
|
|
59
|
-
when :for
|
|
60
|
-
scan_for_loop(element)
|
|
61
|
-
when :resbody
|
|
62
|
-
scan_rescue(element)
|
|
63
|
-
end
|
|
41
|
+
def visit_call_node(node)
|
|
42
|
+
collect(MethodCallScanner.new(node))
|
|
43
|
+
super
|
|
64
44
|
end
|
|
65
45
|
|
|
66
|
-
def
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return unless method_definition_scanner.offense_detected?
|
|
70
|
-
|
|
71
|
-
errors.push(method_definition_scanner.offense)
|
|
46
|
+
def visit_def_node(node)
|
|
47
|
+
collect(MethodDefinitionScanner.new(node))
|
|
48
|
+
super
|
|
72
49
|
end
|
|
73
50
|
|
|
74
|
-
def
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
return unless method_call_scanner.offense_detected?
|
|
78
|
-
|
|
79
|
-
errors.push(method_call_scanner.offense)
|
|
51
|
+
def visit_for_node(node)
|
|
52
|
+
collect(ForLoopScanner.new(node))
|
|
53
|
+
super
|
|
80
54
|
end
|
|
81
55
|
|
|
82
|
-
def
|
|
83
|
-
|
|
56
|
+
def visit_rescue_node(node)
|
|
57
|
+
collect(RescueCallScanner.new(node))
|
|
58
|
+
super
|
|
84
59
|
end
|
|
85
60
|
|
|
86
|
-
|
|
87
|
-
rescue_call_scanner = RescueCallScanner.new(element)
|
|
88
|
-
|
|
89
|
-
return unless rescue_call_scanner.offense_detected?
|
|
61
|
+
private
|
|
90
62
|
|
|
91
|
-
|
|
63
|
+
def collect(scanner)
|
|
64
|
+
@offenses.push(scanner.offense) if scanner.offense_detected?
|
|
92
65
|
end
|
|
93
66
|
end
|
|
67
|
+
|
|
68
|
+
private_constant :AnalyzerVisitor
|
|
94
69
|
end
|
data/lib/fastererer/cli.rb
CHANGED
|
@@ -2,17 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
require 'optparse'
|
|
4
4
|
require_relative 'file_traverser'
|
|
5
|
+
require_relative 'formatters'
|
|
5
6
|
require_relative 'painter'
|
|
6
7
|
require_relative 'version'
|
|
7
8
|
|
|
8
9
|
module Fastererer
|
|
9
10
|
class CLI
|
|
10
|
-
|
|
11
|
+
OFFENSES_FOUND_STATUS = 1
|
|
12
|
+
USAGE_ERROR_STATUS = 2
|
|
13
|
+
|
|
14
|
+
def self.execute(out: $stdout, err: $stderr)
|
|
11
15
|
options = parse_options(ARGV.dup)
|
|
16
|
+
formatter = Formatters.fetch(options[:format] || 'text').new(out:, err:)
|
|
17
|
+
rescue UnknownFormatError, OptionParser::ParseError => e
|
|
18
|
+
err.puts(e.message)
|
|
19
|
+
exit USAGE_ERROR_STATUS
|
|
20
|
+
else
|
|
12
21
|
Painter.disable! if options[:no_color]
|
|
13
|
-
file_traverser =
|
|
22
|
+
file_traverser = FileTraverser.new(options[:path], formatter:)
|
|
14
23
|
file_traverser.traverse
|
|
15
|
-
|
|
24
|
+
exit_with_status_for(file_traverser)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.exit_with_status_for(file_traverser)
|
|
28
|
+
exit USAGE_ERROR_STATUS if file_traverser.path_missing?
|
|
29
|
+
exit OFFENSES_FOUND_STATUS if file_traverser.offenses_found?
|
|
16
30
|
end
|
|
17
31
|
|
|
18
32
|
def self.parse_options(argv)
|
|
@@ -24,6 +38,9 @@ module Fastererer
|
|
|
24
38
|
def self.build_parser(options)
|
|
25
39
|
OptionParser.new do |opts|
|
|
26
40
|
opts.banner = 'Usage: fastererer [options] [path]'
|
|
41
|
+
opts.on('-f', '--format FORMAT', 'Output format: text (default), json, rdjsonl') do |format|
|
|
42
|
+
options[:format] = format
|
|
43
|
+
end
|
|
27
44
|
opts.on('--no-color', 'Disable ANSI color in output') { options[:no_color] = true }
|
|
28
45
|
opts.on('-h', '--help', 'Show this help') { show_help_and_exit(opts) }
|
|
29
46
|
opts.on('-v', '--version', 'Show version') { show_version_and_exit }
|
|
@@ -36,7 +53,7 @@ module Fastererer
|
|
|
36
53
|
end
|
|
37
54
|
|
|
38
55
|
def self.show_version_and_exit
|
|
39
|
-
puts
|
|
56
|
+
puts VERSION
|
|
40
57
|
exit
|
|
41
58
|
end
|
|
42
59
|
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'rule_catalog'
|
|
4
|
+
require_relative 'rule_name'
|
|
5
|
+
|
|
6
|
+
module Fastererer
|
|
7
|
+
class Explanation
|
|
8
|
+
def self.for(offense_name)
|
|
9
|
+
@instances ||= {}
|
|
10
|
+
@instances[offense_name.to_sym] ||= new(offense_name)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.format_line(source)
|
|
14
|
+
"#{source.rule_name}: #{source.description.delete_suffix('.')}. (#{source.url})"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
attr_reader :offense_name
|
|
18
|
+
|
|
19
|
+
def initialize(offense_name)
|
|
20
|
+
@offense_name = offense_name.to_sym
|
|
21
|
+
@row = RuleCatalog.fetch(@offense_name)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def description
|
|
25
|
+
row.fetch('description')
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def url
|
|
29
|
+
row.fetch('url')
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def rule_name
|
|
33
|
+
@rule_name ||= RuleName.from(offense_name)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def to_s
|
|
37
|
+
self.class.format_line(self)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
attr_reader :row
|
|
43
|
+
end
|
|
44
|
+
end
|