fastererer 1.0.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 +41 -2
- data/README.md +82 -1
- data/lib/fastererer/analyzer.rb +3 -2
- data/lib/fastererer/cli.rb +21 -4
- data/lib/fastererer/explanation.rb +5 -1
- data/lib/fastererer/file_traverser.rb +42 -116
- 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 +10 -6
- data/lib/fastererer/offense_collector.rb +1 -1
- data/lib/fastererer/receiver.rb +14 -0
- data/lib/fastererer/report.rb +10 -0
- data/lib/fastererer/rule_catalog.rb +5 -0
- data/lib/fastererer/scanners/for_loop_scanner.rb +23 -0
- data/lib/fastererer/scanners/method_call_scanner.rb +5 -13
- data/lib/fastererer/scanners/symbol_to_proc_check.rb +9 -4
- data/lib/fastererer/version.rb +1 -1
- metadata +12 -2
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,45 @@ 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
|
+
|
|
11
46
|
## [1.0.0] - 2026-06-05
|
|
12
47
|
|
|
13
48
|
First stable release of `fastererer` after the fork.
|
|
@@ -136,7 +171,8 @@ First stable release of `fastererer` after the fork.
|
|
|
136
171
|
|
|
137
172
|
[Keep a Changelog]: https://keepachangelog.com
|
|
138
173
|
[Semantic Versioning]: https://semver.org/spec/v2.0.0.html
|
|
139
|
-
[Unreleased]: https://github.com/ExtractableMedia/fastererer/compare/v1.
|
|
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
|
|
140
176
|
[1.0.0]: https://github.com/ExtractableMedia/fastererer/compare/v0.12.0...v1.0.0
|
|
141
177
|
[0.12.0]: https://github.com/ExtractableMedia/fastererer/compare/v0.11.0...v0.12.0
|
|
142
178
|
[#1]: https://github.com/ExtractableMedia/fastererer/pull/1
|
|
@@ -144,4 +180,7 @@ First stable release of `fastererer` after the fork.
|
|
|
144
180
|
[#9]: https://github.com/ExtractableMedia/fastererer/pull/9
|
|
145
181
|
[#15]: https://github.com/ExtractableMedia/fastererer/pull/15
|
|
146
182
|
[#28]: https://github.com/ExtractableMedia/fastererer/issues/28
|
|
183
|
+
[#40]: https://github.com/ExtractableMedia/fastererer/issues/40
|
|
147
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,7 +49,8 @@ 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
|
|
|
@@ -75,6 +76,53 @@ underlying snake_case rule key (e.g. `select_first_vs_detect`), which is what
|
|
|
75
76
|
you reference in `.fastererer.yml` under `speedups:` to disable a rule.
|
|
76
77
|
Descriptions and documentation URLs live in `config/locales/en.yml`.
|
|
77
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
|
+
```
|
|
89
|
+
|
|
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
|
+
```
|
|
116
|
+
|
|
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`:
|
|
121
|
+
|
|
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"}}
|
|
124
|
+
```
|
|
125
|
+
|
|
78
126
|
## Configuration
|
|
79
127
|
|
|
80
128
|
Configuration lives in a `.fastererer.yml` file at the root of your project (or any ancestor
|
|
@@ -122,6 +170,38 @@ step:
|
|
|
122
170
|
run: bundle exec fastererer
|
|
123
171
|
```
|
|
124
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
|
+
|
|
125
205
|
Color output is auto-disabled when STDOUT isn't a TTY, when `NO_COLOR` is set (see
|
|
126
206
|
[no-color.org](https://no-color.org/)), or when `--no-color` is passed — so CI logs, piped output
|
|
127
207
|
(`fastererer | grep`), and editor integrations get plain text without configuration.
|
|
@@ -173,5 +253,6 @@ for the idiom catalog that drives the speed checks.
|
|
|
173
253
|
[fasterer]: https://github.com/DamirSvrtan/fasterer
|
|
174
254
|
[issues]: https://github.com/ExtractableMedia/fastererer/issues
|
|
175
255
|
[prism]: https://github.com/ruby/prism
|
|
256
|
+
[rdf]: https://github.com/reviewdog/reviewdog/tree/master/proto/rdf
|
|
176
257
|
[roadmap-project]: https://github.com/orgs/ExtractableMedia/projects/1
|
|
177
258
|
[sferik-talk]: https://speakerdeck.com/sferik/writing-fast-ruby
|
data/lib/fastererer/analyzer.rb
CHANGED
|
@@ -8,8 +8,9 @@ require 'fastererer/offense'
|
|
|
8
8
|
require 'fastererer/offense_collector'
|
|
9
9
|
require 'fastererer/parser'
|
|
10
10
|
require 'fastererer/scanners/method_call_scanner'
|
|
11
|
-
require 'fastererer/scanners/rescue_call_scanner'
|
|
12
11
|
require 'fastererer/scanners/method_definition_scanner'
|
|
12
|
+
require 'fastererer/scanners/for_loop_scanner'
|
|
13
|
+
require 'fastererer/scanners/rescue_call_scanner'
|
|
13
14
|
|
|
14
15
|
module Fastererer
|
|
15
16
|
class Analyzer
|
|
@@ -48,7 +49,7 @@ module Fastererer
|
|
|
48
49
|
end
|
|
49
50
|
|
|
50
51
|
def visit_for_node(node)
|
|
51
|
-
|
|
52
|
+
collect(ForLoopScanner.new(node))
|
|
52
53
|
super
|
|
53
54
|
end
|
|
54
55
|
|
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
|
|
@@ -10,6 +10,10 @@ module Fastererer
|
|
|
10
10
|
@instances[offense_name.to_sym] ||= new(offense_name)
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
+
def self.format_line(source)
|
|
14
|
+
"#{source.rule_name}: #{source.description.delete_suffix('.')}. (#{source.url})"
|
|
15
|
+
end
|
|
16
|
+
|
|
13
17
|
attr_reader :offense_name
|
|
14
18
|
|
|
15
19
|
def initialize(offense_name)
|
|
@@ -30,7 +34,7 @@ module Fastererer
|
|
|
30
34
|
end
|
|
31
35
|
|
|
32
36
|
def to_s
|
|
33
|
-
|
|
37
|
+
self.class.format_line(self)
|
|
34
38
|
end
|
|
35
39
|
|
|
36
40
|
private
|
|
@@ -5,8 +5,9 @@ require 'English'
|
|
|
5
5
|
|
|
6
6
|
require_relative 'analyzer'
|
|
7
7
|
require_relative 'config'
|
|
8
|
-
require_relative '
|
|
9
|
-
require_relative '
|
|
8
|
+
require_relative 'finding'
|
|
9
|
+
require_relative 'report'
|
|
10
|
+
require_relative 'formatters/text_formatter'
|
|
10
11
|
|
|
11
12
|
module Fastererer
|
|
12
13
|
class FileTraverser
|
|
@@ -15,19 +16,17 @@ module Fastererer
|
|
|
15
16
|
EXCLUDE_PATHS_KEY = Config::EXCLUDE_PATHS_KEY
|
|
16
17
|
|
|
17
18
|
attr_reader :config, :parse_error_paths
|
|
18
|
-
attr_accessor :offenses_total_count
|
|
19
19
|
|
|
20
|
-
def initialize(path)
|
|
20
|
+
def initialize(path, formatter: Formatters::TextFormatter.new)
|
|
21
21
|
@path = Pathname(path || '.')
|
|
22
22
|
@parse_error_paths = []
|
|
23
23
|
@config = Config.new
|
|
24
|
-
@
|
|
24
|
+
@formatter = formatter
|
|
25
|
+
@findings = []
|
|
25
26
|
end
|
|
26
27
|
|
|
27
28
|
def traverse
|
|
28
|
-
|
|
29
|
-
output_parse_errors
|
|
30
|
-
output_statistics
|
|
29
|
+
@formatter.render(build_report)
|
|
31
30
|
end
|
|
32
31
|
|
|
33
32
|
def config_file
|
|
@@ -35,7 +34,13 @@ module Fastererer
|
|
|
35
34
|
end
|
|
36
35
|
|
|
37
36
|
def offenses_found?
|
|
38
|
-
|
|
37
|
+
findings.any?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def path_missing?
|
|
41
|
+
return @path_missing if defined?(@path_missing)
|
|
42
|
+
|
|
43
|
+
@path_missing = !@path.exist?
|
|
39
44
|
end
|
|
40
45
|
|
|
41
46
|
def scannable_files
|
|
@@ -44,83 +49,53 @@ module Fastererer
|
|
|
44
49
|
|
|
45
50
|
private
|
|
46
51
|
|
|
47
|
-
|
|
52
|
+
attr_reader :findings
|
|
48
53
|
|
|
49
|
-
def
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
def build_report
|
|
55
|
+
scan_files
|
|
56
|
+
|
|
57
|
+
Report.new(
|
|
58
|
+
findings:,
|
|
59
|
+
files_inspected_count: scannable_files.count,
|
|
60
|
+
unparsable_files: parse_error_paths,
|
|
61
|
+
missing_path: (@path.to_s if path_missing?)
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def scan_files
|
|
66
|
+
scannable_files.each { |ruby_file| scan_file(ruby_file) }
|
|
55
67
|
end
|
|
56
68
|
|
|
57
69
|
def scan_file(path)
|
|
58
70
|
analyzer = Analyzer.new(path)
|
|
59
71
|
analyzer.scan
|
|
60
72
|
rescue Fastererer::ParseError, SystemCallError, SystemStackError, EncodingError => e
|
|
61
|
-
parse_error_paths.push(ErrorData.new(path, e.class, e.message)
|
|
73
|
+
parse_error_paths.push(ErrorData.new(path, e.class, e.message))
|
|
62
74
|
else
|
|
63
|
-
|
|
64
|
-
output(analyzer)
|
|
65
|
-
self.offenses_found = true
|
|
66
|
-
self.offenses_total_count += analyzer.errors.count
|
|
67
|
-
end
|
|
75
|
+
collect_findings(analyzer)
|
|
68
76
|
end
|
|
69
77
|
|
|
70
|
-
def
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
Pathname(ruby_file_path).relative_path_from(root_dir).to_s
|
|
74
|
-
end
|
|
75
|
-
else
|
|
76
|
-
[@path.to_s]
|
|
78
|
+
def collect_findings(analyzer)
|
|
79
|
+
reported_offenses(analyzer).each do |offense|
|
|
80
|
+
findings.push(Finding.from(offense, analyzer.file_path))
|
|
77
81
|
end
|
|
78
82
|
end
|
|
79
83
|
|
|
80
|
-
def
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def output(analyzer)
|
|
85
|
-
offenses_grouped_by_type(analyzer).each_value do |error_occurrences|
|
|
86
|
-
explanation = error_occurrences.first.explanation
|
|
87
|
-
|
|
88
|
-
error_occurrences.map(&:line_number).each do |line|
|
|
89
|
-
file_and_line = "#{analyzer.file_path}:#{line}"
|
|
90
|
-
print "#{Painter.paint(file_and_line, :red)}: #{severity}: #{explanation}\n"
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
print "\n"
|
|
84
|
+
def reported_offenses(analyzer)
|
|
85
|
+
ignored = ignored_speedups
|
|
86
|
+
analyzer.errors.reject { |offense| ignored.include?(offense.name) }
|
|
95
87
|
end
|
|
96
88
|
|
|
97
|
-
def
|
|
98
|
-
|
|
99
|
-
|
|
89
|
+
def all_files
|
|
90
|
+
return [] if path_missing?
|
|
91
|
+
return [@path.to_s] unless @path.directory?
|
|
100
92
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
93
|
+
# base: takes the directory literally; interpolating it would read [ ] { } * ? as pattern
|
|
94
|
+
Dir.glob('**/*.rb', base: @path.to_s).map do |relative_path|
|
|
95
|
+
Pathname(File.join(@path, relative_path)).cleanpath.to_s
|
|
104
96
|
end
|
|
105
97
|
end
|
|
106
98
|
|
|
107
|
-
def output_parse_errors
|
|
108
|
-
return if parse_error_paths.none?
|
|
109
|
-
|
|
110
|
-
puts 'Fastererer was unable to process some files. Unprocessable files were:'
|
|
111
|
-
puts '-----------------------------------------------------'
|
|
112
|
-
puts parse_error_paths
|
|
113
|
-
puts
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
def output_statistics
|
|
117
|
-
puts Statistics.new(self)
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
def output_unable_to_find_file(path)
|
|
121
|
-
puts Painter.paint("No such file or directory - #{path}", :red)
|
|
122
|
-
end
|
|
123
|
-
|
|
124
99
|
def ignored_speedups
|
|
125
100
|
config.ignored_speedups
|
|
126
101
|
end
|
|
@@ -139,53 +114,4 @@ module Fastererer
|
|
|
139
114
|
"#{file_path} - #{error_class} - #{error_message}"
|
|
140
115
|
end
|
|
141
116
|
end
|
|
142
|
-
|
|
143
|
-
class Statistics
|
|
144
|
-
def initialize(traverser)
|
|
145
|
-
@files_inspected_count = traverser.scannable_files.count
|
|
146
|
-
@offenses_found_count = traverser.offenses_total_count
|
|
147
|
-
@unparsable_files_count = traverser.parse_error_paths.count
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
def to_s
|
|
151
|
-
[
|
|
152
|
-
inspected_files_output,
|
|
153
|
-
offenses_found_output,
|
|
154
|
-
unparsable_files_output
|
|
155
|
-
].compact.join(', ')
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
def inspected_files_output
|
|
159
|
-
Painter.paint(
|
|
160
|
-
"#{@files_inspected_count} #{pluralize(@files_inspected_count, 'file')} inspected", :green
|
|
161
|
-
)
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
def offenses_found_output
|
|
165
|
-
color = @offenses_found_count.zero? ? :green : :red
|
|
166
|
-
|
|
167
|
-
Painter.paint(
|
|
168
|
-
"#{@offenses_found_count} #{pluralize(@offenses_found_count, 'offense')} detected", color
|
|
169
|
-
)
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
def unparsable_files_output
|
|
173
|
-
return if @unparsable_files_count.zero?
|
|
174
|
-
|
|
175
|
-
Painter.paint(
|
|
176
|
-
"#{@unparsable_files_count} unparsable #{pluralize(@unparsable_files_count, 'file')} found",
|
|
177
|
-
:red
|
|
178
|
-
)
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
def pluralize(count, singular, plural = nil)
|
|
182
|
-
if count == 1
|
|
183
|
-
singular.to_s
|
|
184
|
-
elsif plural
|
|
185
|
-
plural.to_s
|
|
186
|
-
else
|
|
187
|
-
"#{singular}s"
|
|
188
|
-
end
|
|
189
|
-
end
|
|
190
|
-
end
|
|
191
117
|
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fastererer
|
|
4
|
+
Finding = Data.define(:path, :line, :rule_key, :rule_name, :description, :url) do
|
|
5
|
+
def self.from(offense, path)
|
|
6
|
+
explanation = offense.explanation
|
|
7
|
+
|
|
8
|
+
new(
|
|
9
|
+
path: path.to_s.scrub,
|
|
10
|
+
line: offense.line_number,
|
|
11
|
+
rule_key: offense.name.to_s,
|
|
12
|
+
rule_name: explanation.rule_name,
|
|
13
|
+
description: explanation.description,
|
|
14
|
+
url: explanation.url
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fastererer
|
|
4
|
+
module Formatters
|
|
5
|
+
class Base
|
|
6
|
+
# The gap at \x09 keeps tabs, which are printable, out of the escaping
|
|
7
|
+
UNSAFE_CHARS = /[\x00-\x08\x0A-\x1F\x7F]/
|
|
8
|
+
private_constant :UNSAFE_CHARS
|
|
9
|
+
|
|
10
|
+
def initialize(out: $stdout, err: $stderr)
|
|
11
|
+
@out = out
|
|
12
|
+
@err = err
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
attr_reader :out, :err
|
|
18
|
+
|
|
19
|
+
# Undecorated on purpose for the machine formats; TextFormatter overrides with color
|
|
20
|
+
def output_diagnostics(report)
|
|
21
|
+
missing_path = report.missing_path
|
|
22
|
+
|
|
23
|
+
err.puts("No such file or directory - #{sanitize(missing_path)}") if missing_path
|
|
24
|
+
report.unparsable_files.each { |unparsable_file| err.puts(sanitize(unparsable_file.to_s)) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def sanitize(text)
|
|
28
|
+
text.to_s.scrub.gsub(UNSAFE_CHARS) { |char| format('\\x%02X', char.ord) }
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require_relative 'base'
|
|
5
|
+
require_relative '../version'
|
|
6
|
+
|
|
7
|
+
module Fastererer
|
|
8
|
+
module Formatters
|
|
9
|
+
# Diagnostics go to stderr so stdout carries nothing but the JSON document
|
|
10
|
+
class JsonFormatter < Base
|
|
11
|
+
def render(report)
|
|
12
|
+
output_diagnostics(report)
|
|
13
|
+
out.puts JSON.pretty_generate(document(report))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def document(report)
|
|
19
|
+
{
|
|
20
|
+
'metadata' => { 'fastererer_version' => Fastererer::VERSION },
|
|
21
|
+
'summary' => summary(report),
|
|
22
|
+
'offenses' => offenses(report)
|
|
23
|
+
}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def summary(report)
|
|
27
|
+
{
|
|
28
|
+
'offense_count' => report.offenses_detected_count,
|
|
29
|
+
'inspected_file_count' => report.files_inspected_count,
|
|
30
|
+
'unparsable_file_count' => report.unparsable_files.count
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def offenses(report)
|
|
35
|
+
report.findings.sort_by { |finding| [finding.path, finding.line] }.map do |finding|
|
|
36
|
+
{
|
|
37
|
+
'path' => finding.path,
|
|
38
|
+
'line' => finding.line,
|
|
39
|
+
'rule' => finding.rule_name,
|
|
40
|
+
'rule_key' => finding.rule_key,
|
|
41
|
+
'message' => finding.description,
|
|
42
|
+
'url' => finding.url
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require_relative 'base'
|
|
5
|
+
|
|
6
|
+
module Fastererer
|
|
7
|
+
module Formatters
|
|
8
|
+
# Shape is pinned to reviewdog's Diagnostic schema: WARNING severity, start-only range
|
|
9
|
+
# code.value is the rule key, not the display name — the reader pastes it into .fastererer.yml
|
|
10
|
+
class RdjsonlFormatter < Base
|
|
11
|
+
SEVERITY = 'WARNING'
|
|
12
|
+
private_constant :SEVERITY
|
|
13
|
+
|
|
14
|
+
def render(report)
|
|
15
|
+
output_diagnostics(report)
|
|
16
|
+
report.findings.each { |finding| out.puts JSON.generate(diagnostic(finding)) }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def diagnostic(finding)
|
|
22
|
+
{
|
|
23
|
+
'message' => finding.description,
|
|
24
|
+
'location' => {
|
|
25
|
+
'path' => finding.path,
|
|
26
|
+
'range' => { 'start' => { 'line' => finding.line } }
|
|
27
|
+
},
|
|
28
|
+
'severity' => SEVERITY,
|
|
29
|
+
'code' => { 'value' => finding.rule_key, 'url' => finding.url }
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative '../painter'
|
|
5
|
+
require_relative 'text_statistics'
|
|
6
|
+
require_relative '../explanation'
|
|
7
|
+
|
|
8
|
+
module Fastererer
|
|
9
|
+
module Formatters
|
|
10
|
+
class TextFormatter < Base
|
|
11
|
+
def render(report)
|
|
12
|
+
output_offenses(report)
|
|
13
|
+
output_diagnostics(report)
|
|
14
|
+
output_statistics(report)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def output_offenses(report)
|
|
20
|
+
report.findings.group_by(&:path).each_value do |path_findings|
|
|
21
|
+
path_findings.group_by(&:rule_name).each_value { |group| output_group(group) }
|
|
22
|
+
out.puts
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def output_group(group)
|
|
27
|
+
group.each do |finding|
|
|
28
|
+
location = Painter.paint("#{sanitize(finding.path)}:#{finding.line}", :red)
|
|
29
|
+
out.puts "#{location}: #{severity}: #{Explanation.format_line(finding)}"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def output_diagnostics(report)
|
|
34
|
+
output_missing_path(report)
|
|
35
|
+
output_parse_errors(report)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def output_missing_path(report)
|
|
39
|
+
return unless report.missing_path
|
|
40
|
+
|
|
41
|
+
message = "No such file or directory - #{sanitize(report.missing_path)}"
|
|
42
|
+
err.puts Painter.paint(message, :red)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def output_parse_errors(report)
|
|
46
|
+
return if report.unparsable_files.none?
|
|
47
|
+
|
|
48
|
+
err.puts 'Fastererer was unable to process some files. Unprocessable files were:'
|
|
49
|
+
err.puts '-----------------------------------------------------'
|
|
50
|
+
report.unparsable_files.each { |unparsable_file| err.puts sanitize(unparsable_file.to_s) }
|
|
51
|
+
err.puts
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def output_statistics(report)
|
|
55
|
+
out.puts TextStatistics.new(report)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def severity
|
|
59
|
+
@severity ||= Painter.paint('W', :magenta)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../painter'
|
|
4
|
+
|
|
5
|
+
module Fastererer
|
|
6
|
+
module Formatters
|
|
7
|
+
class TextStatistics
|
|
8
|
+
def initialize(report)
|
|
9
|
+
@files_inspected_count = report.files_inspected_count
|
|
10
|
+
@offenses_detected_count = report.offenses_detected_count
|
|
11
|
+
@unparsable_files_count = report.unparsable_files.count
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def to_s
|
|
15
|
+
[inspected_files_output, offenses_detected_output, unparsable_files_output]
|
|
16
|
+
.compact.join(', ')
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def inspected_files_output
|
|
20
|
+
count = @files_inspected_count
|
|
21
|
+
|
|
22
|
+
Painter.paint("#{count} #{pluralize(count, 'file')} inspected", :green)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def offenses_detected_output
|
|
26
|
+
count = @offenses_detected_count
|
|
27
|
+
color = count.zero? ? :green : :red
|
|
28
|
+
|
|
29
|
+
Painter.paint("#{count} #{pluralize(count, 'offense')} detected", color)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def unparsable_files_output
|
|
33
|
+
return if @unparsable_files_count.zero?
|
|
34
|
+
|
|
35
|
+
count = @unparsable_files_count
|
|
36
|
+
|
|
37
|
+
Painter.paint("#{count} unparsable #{pluralize(count, 'file')} found", :red)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def pluralize(count, singular)
|
|
41
|
+
count == 1 ? singular : "#{singular}s"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'formatters/text_formatter'
|
|
4
|
+
require_relative 'formatters/json_formatter'
|
|
5
|
+
require_relative 'formatters/rdjsonl_formatter'
|
|
6
|
+
|
|
7
|
+
module Fastererer
|
|
8
|
+
class UnknownFormatError < StandardError; end
|
|
9
|
+
|
|
10
|
+
module Formatters
|
|
11
|
+
FORMATS = {
|
|
12
|
+
'text' => TextFormatter,
|
|
13
|
+
'json' => JsonFormatter,
|
|
14
|
+
'rdjsonl' => RdjsonlFormatter
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
def self.fetch(name)
|
|
18
|
+
FORMATS.fetch(name) do
|
|
19
|
+
raise UnknownFormatError,
|
|
20
|
+
"Unknown format: #{name.inspect}. Valid formats: #{FORMATS.keys.join(', ')}."
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'prism'
|
|
4
|
+
require 'fastererer/receiver'
|
|
4
5
|
|
|
5
6
|
module Fastererer
|
|
6
7
|
class MethodCall
|
|
8
|
+
include Receiver
|
|
9
|
+
|
|
7
10
|
attr_reader :element
|
|
8
11
|
|
|
9
12
|
def self.build(node)
|
|
@@ -26,6 +29,8 @@ module Fastererer
|
|
|
26
29
|
method_name
|
|
27
30
|
end
|
|
28
31
|
|
|
32
|
+
def call_name = method_name
|
|
33
|
+
|
|
29
34
|
def arguments
|
|
30
35
|
@arguments ||= argument_nodes.map { |argument| Argument.new(argument) }
|
|
31
36
|
end
|
|
@@ -75,7 +80,7 @@ module Fastererer
|
|
|
75
80
|
end
|
|
76
81
|
|
|
77
82
|
# Only single positional params convert to &:sym, so a splat or keyword param must not count
|
|
78
|
-
# toward
|
|
83
|
+
# toward the single-name check
|
|
79
84
|
def positional_block_parameter_names
|
|
80
85
|
return [] unless block_node.is_a?(Prism::BlockNode)
|
|
81
86
|
|
|
@@ -130,16 +135,13 @@ module Fastererer
|
|
|
130
135
|
end
|
|
131
136
|
|
|
132
137
|
class VariableReference
|
|
138
|
+
include Receiver
|
|
139
|
+
|
|
133
140
|
attr_reader :name
|
|
134
141
|
|
|
135
142
|
def initialize(node)
|
|
136
143
|
@name = node.name
|
|
137
144
|
end
|
|
138
|
-
|
|
139
|
-
# A bare local/constant reference can't be proven to hold a Hash
|
|
140
|
-
def hash?
|
|
141
|
-
false
|
|
142
|
-
end
|
|
143
145
|
end
|
|
144
146
|
|
|
145
147
|
class Argument
|
|
@@ -180,6 +182,8 @@ module Fastererer
|
|
|
180
182
|
end
|
|
181
183
|
|
|
182
184
|
class Primitive
|
|
185
|
+
include Receiver
|
|
186
|
+
|
|
183
187
|
attr_reader :element
|
|
184
188
|
|
|
185
189
|
def initialize(node)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fastererer
|
|
4
|
+
# Query surface every receiver answers; includers override only what is true for their type
|
|
5
|
+
module Receiver
|
|
6
|
+
def call_name = nil
|
|
7
|
+
def name = nil
|
|
8
|
+
def block? = false
|
|
9
|
+
def arguments = []
|
|
10
|
+
def range? = false
|
|
11
|
+
def array? = false
|
|
12
|
+
def hash? = false
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fastererer
|
|
4
|
+
# Findings arrive already filtered by ignored speedups; formatters must not filter again
|
|
5
|
+
Report = Data.define(:findings, :files_inspected_count, :unparsable_files, :missing_path) do
|
|
6
|
+
def offenses_detected_count
|
|
7
|
+
findings.count
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -8,6 +8,8 @@ module Fastererer
|
|
|
8
8
|
# Loads, validates, memoizes and looks up the rule catalog from the i18n locale.
|
|
9
9
|
module RuleCatalog
|
|
10
10
|
LOCALE_PATH = File.expand_path('../../config/locales/en.yml', __dir__)
|
|
11
|
+
KEY_SHAPE = /\A[a-z0-9_]+\z/
|
|
12
|
+
private_constant :KEY_SHAPE
|
|
11
13
|
|
|
12
14
|
class << self
|
|
13
15
|
def all
|
|
@@ -48,6 +50,9 @@ module Fastererer
|
|
|
48
50
|
|
|
49
51
|
def validate_row!(key, row)
|
|
50
52
|
raise "Fastererer rule #{key} is malformed: #{row.inspect}" unless row.is_a?(Hash)
|
|
53
|
+
unless key.is_a?(String) && key.match?(KEY_SHAPE)
|
|
54
|
+
raise "Fastererer rule #{key.inspect} is not a snake_case key"
|
|
55
|
+
end
|
|
51
56
|
|
|
52
57
|
validate_url!(key, row['url'])
|
|
53
58
|
validate_description!(key, row['description'])
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fastererer/offense'
|
|
4
|
+
require 'fastererer/scanners/offensive'
|
|
5
|
+
|
|
6
|
+
module Fastererer
|
|
7
|
+
class ForLoopScanner
|
|
8
|
+
include Fastererer::Offensive
|
|
9
|
+
|
|
10
|
+
attr_reader :element
|
|
11
|
+
|
|
12
|
+
def initialize(element)
|
|
13
|
+
@element = element
|
|
14
|
+
check_offense
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def check_offense
|
|
20
|
+
add_offense(:for_loop_vs_each) # for is always slower than each, so no guard
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -75,9 +75,7 @@ module Fastererer
|
|
|
75
75
|
end
|
|
76
76
|
|
|
77
77
|
def check_first_offense
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
case method_call.receiver.name
|
|
78
|
+
case method_call.receiver&.call_name
|
|
81
79
|
when :shuffle
|
|
82
80
|
add_offense(:shuffle_first_vs_sample)
|
|
83
81
|
when :select
|
|
@@ -89,9 +87,7 @@ module Fastererer
|
|
|
89
87
|
end
|
|
90
88
|
|
|
91
89
|
def check_each_offense
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
case method_call.receiver.name
|
|
90
|
+
case method_call.receiver&.call_name
|
|
95
91
|
when :reverse
|
|
96
92
|
add_offense(:reverse_each_vs_reverse_each)
|
|
97
93
|
when :keys
|
|
@@ -100,8 +96,7 @@ module Fastererer
|
|
|
100
96
|
end
|
|
101
97
|
|
|
102
98
|
def check_flatten_offense
|
|
103
|
-
return unless method_call.receiver.
|
|
104
|
-
return unless method_call.receiver.name == :map && method_call.arguments.one?
|
|
99
|
+
return unless method_call.receiver&.call_name == :map && method_call.arguments.one?
|
|
105
100
|
|
|
106
101
|
add_offense(:map_flatten_vs_flat_map) if method_call.arguments.first.value == 1
|
|
107
102
|
end
|
|
@@ -127,17 +122,14 @@ module Fastererer
|
|
|
127
122
|
end
|
|
128
123
|
|
|
129
124
|
def check_last_offense
|
|
130
|
-
return unless method_call.receiver
|
|
131
|
-
return unless method_call.receiver.name == :select
|
|
125
|
+
return unless method_call.receiver&.call_name == :select
|
|
132
126
|
return if method_call.arguments.any?
|
|
133
127
|
|
|
134
128
|
add_offense(:select_last_vs_reverse_detect)
|
|
135
129
|
end
|
|
136
130
|
|
|
137
131
|
def check_range_include_offense
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
add_offense(:include_vs_cover_on_range)
|
|
132
|
+
add_offense(:include_vs_cover_on_range) if method_call.receiver&.range?
|
|
141
133
|
end
|
|
142
134
|
end
|
|
143
135
|
end
|
|
@@ -19,12 +19,18 @@ module Fastererer
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def symbol_to_proc_candidate?
|
|
22
|
-
|
|
22
|
+
single_block_argument_name &&
|
|
23
23
|
single_call_body? &&
|
|
24
24
|
method_call.arguments.none? &&
|
|
25
25
|
!method_call.lambda_literal?
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
# A destructured param has no name, so |(a, b), c| must not read as a single-name block
|
|
29
|
+
def single_block_argument_name
|
|
30
|
+
names = method_call.block_argument_names
|
|
31
|
+
names.first if names.size == 1
|
|
32
|
+
end
|
|
33
|
+
|
|
28
34
|
# A safe-nav body (foo&.bar) is excluded: arr.map(&:bar) raises on a nil element, so the rewrite
|
|
29
35
|
# would not preserve behavior
|
|
30
36
|
def single_call_body?
|
|
@@ -32,12 +38,11 @@ module Fastererer
|
|
|
32
38
|
body&.size == 1 && body.first.is_a?(Prism::CallNode) && !body.first.safe_navigation?
|
|
33
39
|
end
|
|
34
40
|
|
|
41
|
+
# A primitive receiver's name is nil, which never matches the named param the gate requires
|
|
35
42
|
def symbol_to_proc_body?(body_call)
|
|
36
43
|
body_call.arguments.none? &&
|
|
37
44
|
!body_call.block? &&
|
|
38
|
-
|
|
39
|
-
!body_call.receiver.is_a?(Fastererer::Primitive) &&
|
|
40
|
-
body_call.receiver.name == method_call.block_argument_names.first
|
|
45
|
+
body_call.receiver&.name == single_block_argument_name
|
|
41
46
|
end
|
|
42
47
|
end
|
|
43
48
|
end
|
data/lib/fastererer/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fastererer
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Matt Menefee
|
|
@@ -46,15 +46,25 @@ files:
|
|
|
46
46
|
- lib/fastererer/config.rb
|
|
47
47
|
- lib/fastererer/explanation.rb
|
|
48
48
|
- lib/fastererer/file_traverser.rb
|
|
49
|
+
- lib/fastererer/finding.rb
|
|
50
|
+
- lib/fastererer/formatters.rb
|
|
51
|
+
- lib/fastererer/formatters/base.rb
|
|
52
|
+
- lib/fastererer/formatters/json_formatter.rb
|
|
53
|
+
- lib/fastererer/formatters/rdjsonl_formatter.rb
|
|
54
|
+
- lib/fastererer/formatters/text_formatter.rb
|
|
55
|
+
- lib/fastererer/formatters/text_statistics.rb
|
|
49
56
|
- lib/fastererer/method_call.rb
|
|
50
57
|
- lib/fastererer/method_definition.rb
|
|
51
58
|
- lib/fastererer/offense.rb
|
|
52
59
|
- lib/fastererer/offense_collector.rb
|
|
53
60
|
- lib/fastererer/painter.rb
|
|
54
61
|
- lib/fastererer/parser.rb
|
|
62
|
+
- lib/fastererer/receiver.rb
|
|
63
|
+
- lib/fastererer/report.rb
|
|
55
64
|
- lib/fastererer/rescue_call.rb
|
|
56
65
|
- lib/fastererer/rule_catalog.rb
|
|
57
66
|
- lib/fastererer/rule_name.rb
|
|
67
|
+
- lib/fastererer/scanners/for_loop_scanner.rb
|
|
58
68
|
- lib/fastererer/scanners/method_call_scanner.rb
|
|
59
69
|
- lib/fastererer/scanners/method_definition_scanner.rb
|
|
60
70
|
- lib/fastererer/scanners/offensive.rb
|
|
@@ -84,7 +94,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
84
94
|
- !ruby/object:Gem::Version
|
|
85
95
|
version: '0'
|
|
86
96
|
requirements: []
|
|
87
|
-
rubygems_version: 4.0.
|
|
97
|
+
rubygems_version: 4.0.16
|
|
88
98
|
specification_version: 4
|
|
89
99
|
summary: Run Ruby more than fast. Fastererer
|
|
90
100
|
test_files: []
|