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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +75 -2
  3. data/README.md +97 -8
  4. data/config/locales/en.yml +60 -0
  5. data/lib/fastererer/analyzer.rb +28 -53
  6. data/lib/fastererer/cli.rb +21 -4
  7. data/lib/fastererer/explanation.rb +44 -0
  8. data/lib/fastererer/file_traverser.rb +43 -112
  9. data/lib/fastererer/finding.rb +18 -0
  10. data/lib/fastererer/formatters/base.rb +32 -0
  11. data/lib/fastererer/formatters/json_formatter.rb +48 -0
  12. data/lib/fastererer/formatters/rdjsonl_formatter.rb +34 -0
  13. data/lib/fastererer/formatters/text_formatter.rb +63 -0
  14. data/lib/fastererer/formatters/text_statistics.rb +45 -0
  15. data/lib/fastererer/formatters.rb +24 -0
  16. data/lib/fastererer/method_call.rb +139 -75
  17. data/lib/fastererer/method_definition.rb +61 -48
  18. data/lib/fastererer/offense.rb +5 -63
  19. data/lib/fastererer/offense_collector.rb +1 -1
  20. data/lib/fastererer/painter.rb +2 -1
  21. data/lib/fastererer/parser.rb +8 -4
  22. data/lib/fastererer/receiver.rb +14 -0
  23. data/lib/fastererer/report.rb +10 -0
  24. data/lib/fastererer/rescue_call.rb +10 -12
  25. data/lib/fastererer/rule_catalog.rb +83 -0
  26. data/lib/fastererer/rule_name.rb +17 -0
  27. data/lib/fastererer/scanners/for_loop_scanner.rb +23 -0
  28. data/lib/fastererer/scanners/method_call_scanner.rb +13 -17
  29. data/lib/fastererer/scanners/method_definition_scanner.rb +57 -29
  30. data/lib/fastererer/scanners/offensive.rb +2 -6
  31. data/lib/fastererer/scanners/rescue_call_scanner.rb +1 -1
  32. data/lib/fastererer/scanners/symbol_to_proc_check.rb +21 -8
  33. data/lib/fastererer/version.rb +1 -1
  34. metadata +19 -11
  35. data/.fastererer.yml +0 -27
  36. data/.ruby-version +0 -1
  37. data/CODE_OF_CONDUCT.md +0 -18
  38. data/CONTRIBUTING.md +0 -63
  39. data/Rakefile +0 -12
  40. data/SECURITY.md +0 -20
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ee0a8fc9f430360ccef546a4f391fdb3112801f6fb2c56204900f0dca8214063
4
- data.tar.gz: a477c6a7b4b9f19011ea89bee00aa193d562f91981dd38e5136cd88fd7ebca14
3
+ metadata.gz: ba7c93c8bf5af26d77535e03595b6e56720bd31f865d23714a36e02f27c21a1a
4
+ data.tar.gz: 6eedb452c7ec6ac71ff7c9b145060cdccf9a5ab7b6837ad2797e02705fc15957
5
5
  SHA512:
6
- metadata.gz: 4d239a4233228c45cc0c0ea28ef00110a8d2ea6476c258ef8f68d3f5b443a66cc48724f47529bb6eb787857843a3c7a4d84aed4aeda09c3a2266880c65ccb14a
7
- data.tar.gz: 6ac88eca14ca854edc0e549b4ddbd6cd9b39796fb9658c10e3c16399b65a7341cea039c0d18fbb9e243406fe8503072ec25ba1a9671dba35d77f0549995bc7ea
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), prior to the fork. From 0.12.0 onward, this is `fastererer`, maintained by ExtractableMedia.
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/v0.12.0...HEAD
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, making it suitable for CI.
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
- db/seeds/cities.rb:15 Hash#keys.each is slower than Hash#each_key.
61
- db/seeds/cities.rb:33 Hash#keys.each is slower than Hash#each_key.
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
- test/options_test.rb:84 Hash#merge! with one argument is slower than Hash#[].
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
- test/module_test.rb:272 Don't rescue NoMethodError, rather check with respond_to?.
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
- spec/cache/mem_cache_store_spec.rb:161 Using tr is faster than gsub when replacing a single character in a string with another single character.
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"
@@ -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
- sexp_tree = Fastererer::Parser.parse(@file_content)
24
- traverse_sexp_tree(sexp_tree)
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
- private
32
-
33
- def traverse_sexp_tree(sexp_tree)
34
- return unless sexp_tree.is_a?(Sexp)
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 scan_by_token(token, element)
54
- case token
55
- when :defn
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 scan_method_definitions(element)
67
- method_definition_scanner = MethodDefinitionScanner.new(element)
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 scan_method_calls(element)
75
- method_call_scanner = MethodCallScanner.new(element)
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 scan_for_loop(element)
83
- errors.push(Fastererer::Offense.new(:for_loop_vs_each, element.line))
56
+ def visit_rescue_node(node)
57
+ collect(RescueCallScanner.new(node))
58
+ super
84
59
  end
85
60
 
86
- def scan_rescue(element)
87
- rescue_call_scanner = RescueCallScanner.new(element)
88
-
89
- return unless rescue_call_scanner.offense_detected?
61
+ private
90
62
 
91
- errors.push(rescue_call_scanner.offense)
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
@@ -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
- def self.execute
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 = Fastererer::FileTraverser.new(options[:path])
22
+ file_traverser = FileTraverser.new(options[:path], formatter:)
14
23
  file_traverser.traverse
15
- abort if file_traverser.offenses_found?
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 Fastererer::VERSION
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