fastererer 0.12.0 → 1.0.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 +35 -1
- data/README.md +15 -7
- data/config/locales/en.yml +60 -0
- data/lib/fastererer/analyzer.rb +26 -52
- data/lib/fastererer/explanation.rb +40 -0
- data/lib/fastererer/file_traverser.rb +12 -7
- data/lib/fastererer/method_call.rb +135 -75
- data/lib/fastererer/method_definition.rb +61 -48
- data/lib/fastererer/offense.rb +5 -63
- data/lib/fastererer/painter.rb +2 -1
- data/lib/fastererer/parser.rb +8 -4
- data/lib/fastererer/rescue_call.rb +10 -12
- data/lib/fastererer/rule_catalog.rb +78 -0
- data/lib/fastererer/rule_name.rb +17 -0
- data/lib/fastererer/scanners/method_call_scanner.rb +8 -4
- 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 +12 -4
- data/lib/fastererer/version.rb +1 -1
- metadata +8 -10
- 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: de024ad42cbd1fb10608607fd4b8f4bbe0c6ff58414f001fea232aee478d2c40
|
|
4
|
+
data.tar.gz: 2c2722e7fedbe2d4d8542b41db6ad37d61020bea79b5e437305651549cb8c6d5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c67fad403a643c864bc7989ca2bb0346802c143baef0bf5bd211a6b6a294abf81dcea1d031a8cf9adca452ee3d83cf58dc75f04297b2ab05fb87c23cfa73cd01
|
|
7
|
+
data.tar.gz: f034537ef76b425c479ff9dff2304859f869543f7bddad5c2596baa68d4d39525584b65f8c132ca7bc7918049408951c2f02446ae8181929e0c6b4f42695043f
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,36 @@ Versions 0.11.0 and earlier were released as [`fasterer`](https://github.com/Dam
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [1.0.0] - 2026-06-05
|
|
12
|
+
|
|
13
|
+
First stable release of `fastererer` after the fork.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- [#9]: Migrate from `ruby_parser` to Ruby's native Prism parser. Prism ships with Ruby itself,
|
|
18
|
+
so this drops the `ruby_parser`, `sexp_processor`, and `racc` dependencies and keeps the
|
|
19
|
+
analyzer in step with current and future Ruby syntax, including Ruby 4.0.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- [#9]: Detect speedups reached through a safe-navigation chain (e.g. `arr&.first`,
|
|
24
|
+
`arr&.select { … }.first`), which the previous S-expression scanner skipped.
|
|
25
|
+
- [#9]: Detect speedups inside `rescue` bodies — getter-vs-`attr_reader`,
|
|
26
|
+
setter-vs-`attr_writer`, and proc-call-vs-`yield` offenses are now scanned in rescue clauses.
|
|
27
|
+
- [#15]: Offense output now mirrors RuboCop's format with a `Performance/RuleName` rule name and a
|
|
28
|
+
[fast-ruby](https://github.com/fastruby/fast-ruby) documentation link, e.g.
|
|
29
|
+
`path:line: W: Performance/ForLoopVsEach: For loop is slower than using each. (url)`.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- [#9]: Block calls inside a nested scope (a singleton class or a nested method definition) are
|
|
34
|
+
no longer misattributed to the enclosing method.
|
|
35
|
+
- [#9]: Unparseable source now raises a clear error instead of silently reporting no offenses.
|
|
36
|
+
- [#28]: `hash_merge_bang_vs_hash_brackets` now flags `Hash#update`, an alias of `Hash#merge!`, but
|
|
37
|
+
only when the receiver is provably a Hash (a `{}` literal, `Hash.new`, or `Hash[...]`). Because
|
|
38
|
+
`update` is also defined on `ActiveRecord`, `ActionController::Parameters`, and others, a bare
|
|
39
|
+
`obj.update(k => v)` is left untouched to avoid false positives.
|
|
40
|
+
|
|
11
41
|
## [0.12.0] - 2026-05-18
|
|
12
42
|
|
|
13
43
|
### Added
|
|
@@ -106,8 +136,12 @@ Versions 0.11.0 and earlier were released as [`fasterer`](https://github.com/Dam
|
|
|
106
136
|
|
|
107
137
|
[Keep a Changelog]: https://keepachangelog.com
|
|
108
138
|
[Semantic Versioning]: https://semver.org/spec/v2.0.0.html
|
|
109
|
-
[Unreleased]: https://github.com/ExtractableMedia/fastererer/compare/
|
|
139
|
+
[Unreleased]: https://github.com/ExtractableMedia/fastererer/compare/v1.0.0...HEAD
|
|
140
|
+
[1.0.0]: https://github.com/ExtractableMedia/fastererer/compare/v0.12.0...v1.0.0
|
|
110
141
|
[0.12.0]: https://github.com/ExtractableMedia/fastererer/compare/v0.11.0...v0.12.0
|
|
111
142
|
[#1]: https://github.com/ExtractableMedia/fastererer/pull/1
|
|
112
143
|
[#2]: https://github.com/ExtractableMedia/fastererer/pull/2
|
|
144
|
+
[#9]: https://github.com/ExtractableMedia/fastererer/pull/9
|
|
145
|
+
[#15]: https://github.com/ExtractableMedia/fastererer/pull/15
|
|
146
|
+
[#28]: https://github.com/ExtractableMedia/fastererer/issues/28
|
|
113
147
|
[#42]: https://github.com/ExtractableMedia/fastererer/pull/42
|
data/README.md
CHANGED
|
@@ -53,20 +53,28 @@ Fastererer exits with status `1` when offenses are found, making it suitable for
|
|
|
53
53
|
|
|
54
54
|
## Example output
|
|
55
55
|
|
|
56
|
+
Each offense is reported on a single line, following the same shape as
|
|
57
|
+
RuboCop and its plugins (`path:line: SEVERITY: Department/RuleName: message.
|
|
58
|
+
(url)`), so the rule name and a link to documentation are always visible:
|
|
59
|
+
|
|
56
60
|
```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.
|
|
61
|
+
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)
|
|
62
|
+
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)
|
|
59
63
|
|
|
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.
|
|
64
|
+
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)
|
|
62
65
|
|
|
63
|
-
test/options_test.rb:84 Hash#merge! with one argument is slower than Hash#[].
|
|
66
|
+
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)
|
|
64
67
|
|
|
65
|
-
test/module_test.rb:272 Don't rescue NoMethodError, rather check with respond_to?.
|
|
68
|
+
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)
|
|
66
69
|
|
|
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.
|
|
70
|
+
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)
|
|
68
71
|
```
|
|
69
72
|
|
|
73
|
+
The rule name (e.g. `Performance/SelectFirstVsDetect`) is derived from the
|
|
74
|
+
underlying snake_case rule key (e.g. `select_first_vs_detect`), which is what
|
|
75
|
+
you reference in `.fastererer.yml` under `speedups:` to disable a rule.
|
|
76
|
+
Descriptions and documentation URLs live in `config/locales/en.yml`.
|
|
77
|
+
|
|
70
78
|
## Configuration
|
|
71
79
|
|
|
72
80
|
Configuration lives in a `.fastererer.yml` file at the root of your project (or any ancestor
|
|
@@ -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,8 +1,10 @@
|
|
|
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'
|
|
@@ -20,75 +22,47 @@ module Fastererer
|
|
|
20
22
|
end
|
|
21
23
|
|
|
22
24
|
def scan
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
root_node = Fastererer::Parser.parse(@file_content)
|
|
26
|
+
root_node.accept(AnalyzerVisitor.new(errors))
|
|
25
27
|
end
|
|
26
28
|
|
|
27
29
|
def errors
|
|
28
30
|
@errors ||= Fastererer::OffenseCollector.new
|
|
29
31
|
end
|
|
32
|
+
end
|
|
30
33
|
|
|
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?
|
|
34
|
+
class AnalyzerVisitor < Prism::Visitor
|
|
35
|
+
def initialize(offenses)
|
|
36
|
+
super()
|
|
37
|
+
@offenses = offenses
|
|
51
38
|
end
|
|
52
39
|
|
|
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
|
|
40
|
+
def visit_call_node(node)
|
|
41
|
+
collect(MethodCallScanner.new(node))
|
|
42
|
+
super
|
|
64
43
|
end
|
|
65
44
|
|
|
66
|
-
def
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return unless method_definition_scanner.offense_detected?
|
|
70
|
-
|
|
71
|
-
errors.push(method_definition_scanner.offense)
|
|
45
|
+
def visit_def_node(node)
|
|
46
|
+
collect(MethodDefinitionScanner.new(node))
|
|
47
|
+
super
|
|
72
48
|
end
|
|
73
49
|
|
|
74
|
-
def
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
return unless method_call_scanner.offense_detected?
|
|
78
|
-
|
|
79
|
-
errors.push(method_call_scanner.offense)
|
|
50
|
+
def visit_for_node(node)
|
|
51
|
+
@offenses.push(Fastererer::Offense.new(:for_loop_vs_each, node.location.start_line))
|
|
52
|
+
super
|
|
80
53
|
end
|
|
81
54
|
|
|
82
|
-
def
|
|
83
|
-
|
|
55
|
+
def visit_rescue_node(node)
|
|
56
|
+
collect(RescueCallScanner.new(node))
|
|
57
|
+
super
|
|
84
58
|
end
|
|
85
59
|
|
|
86
|
-
|
|
87
|
-
rescue_call_scanner = RescueCallScanner.new(element)
|
|
88
|
-
|
|
89
|
-
return unless rescue_call_scanner.offense_detected?
|
|
60
|
+
private
|
|
90
61
|
|
|
91
|
-
|
|
62
|
+
def collect(scanner)
|
|
63
|
+
@offenses.push(scanner.offense) if scanner.offense_detected?
|
|
92
64
|
end
|
|
93
65
|
end
|
|
66
|
+
|
|
67
|
+
private_constant :AnalyzerVisitor
|
|
94
68
|
end
|
|
@@ -0,0 +1,40 @@
|
|
|
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
|
+
attr_reader :offense_name
|
|
14
|
+
|
|
15
|
+
def initialize(offense_name)
|
|
16
|
+
@offense_name = offense_name.to_sym
|
|
17
|
+
@row = RuleCatalog.fetch(@offense_name)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def description
|
|
21
|
+
row.fetch('description')
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def url
|
|
25
|
+
row.fetch('url')
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def rule_name
|
|
29
|
+
@rule_name ||= RuleName.from(offense_name)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def to_s
|
|
33
|
+
"#{rule_name}: #{description.delete_suffix('.')}. (#{url})"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
attr_reader :row
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -5,6 +5,7 @@ require 'English'
|
|
|
5
5
|
|
|
6
6
|
require_relative 'analyzer'
|
|
7
7
|
require_relative 'config'
|
|
8
|
+
require_relative 'explanation'
|
|
8
9
|
require_relative 'painter'
|
|
9
10
|
|
|
10
11
|
module Fastererer
|
|
@@ -56,7 +57,7 @@ module Fastererer
|
|
|
56
57
|
def scan_file(path)
|
|
57
58
|
analyzer = Analyzer.new(path)
|
|
58
59
|
analyzer.scan
|
|
59
|
-
rescue
|
|
60
|
+
rescue Fastererer::ParseError, SystemCallError, SystemStackError, EncodingError => e
|
|
60
61
|
parse_error_paths.push(ErrorData.new(path, e.class, e.message).to_s)
|
|
61
62
|
else
|
|
62
63
|
if offenses_grouped_by_type(analyzer).any?
|
|
@@ -81,16 +82,22 @@ module Fastererer
|
|
|
81
82
|
end
|
|
82
83
|
|
|
83
84
|
def output(analyzer)
|
|
84
|
-
offenses_grouped_by_type(analyzer).
|
|
85
|
-
|
|
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|
|
|
86
89
|
file_and_line = "#{analyzer.file_path}:#{line}"
|
|
87
|
-
print "#{Painter.paint(file_and_line, :red)} #{
|
|
90
|
+
print "#{Painter.paint(file_and_line, :red)}: #{severity}: #{explanation}\n"
|
|
88
91
|
end
|
|
89
92
|
end
|
|
90
93
|
|
|
91
94
|
print "\n"
|
|
92
95
|
end
|
|
93
96
|
|
|
97
|
+
def severity
|
|
98
|
+
@severity ||= Painter.paint('W', :magenta)
|
|
99
|
+
end
|
|
100
|
+
|
|
94
101
|
def offenses_grouped_by_type(analyzer)
|
|
95
102
|
analyzer.errors.group_by(&:name).delete_if do |offense_name, _|
|
|
96
103
|
ignored_speedups.include?(offense_name)
|
|
@@ -100,9 +107,7 @@ module Fastererer
|
|
|
100
107
|
def output_parse_errors
|
|
101
108
|
return if parse_error_paths.none?
|
|
102
109
|
|
|
103
|
-
puts 'Fastererer was unable to process some files
|
|
104
|
-
puts 'internal parser is not able to read some characters or'
|
|
105
|
-
puts 'has timed out. Unprocessable files were:'
|
|
110
|
+
puts 'Fastererer was unable to process some files. Unprocessable files were:'
|
|
106
111
|
puts '-----------------------------------------------------'
|
|
107
112
|
puts parse_error_paths
|
|
108
113
|
puts
|
|
@@ -1,95 +1,130 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'prism'
|
|
4
|
+
|
|
3
5
|
module Fastererer
|
|
4
6
|
class MethodCall
|
|
5
|
-
attr_reader :element
|
|
7
|
+
attr_reader :element
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
def self.build(node)
|
|
10
|
+
node.is_a?(Prism::LambdaNode) ? LambdaCall.new(node) : new(node)
|
|
11
|
+
end
|
|
8
12
|
|
|
9
|
-
def initialize(
|
|
10
|
-
@element =
|
|
11
|
-
set_call_element
|
|
12
|
-
set_receiver
|
|
13
|
-
set_method_name
|
|
14
|
-
set_arguments
|
|
15
|
-
set_block_presence
|
|
16
|
-
set_block_body
|
|
17
|
-
set_block_argument_names
|
|
13
|
+
def initialize(node)
|
|
14
|
+
@element = node
|
|
18
15
|
end
|
|
19
16
|
|
|
20
|
-
def
|
|
21
|
-
@
|
|
17
|
+
def receiver
|
|
18
|
+
@receiver ||= ReceiverFactory.build(element.receiver)
|
|
22
19
|
end
|
|
23
20
|
|
|
24
|
-
def
|
|
25
|
-
|
|
21
|
+
def method_name
|
|
22
|
+
element.name
|
|
26
23
|
end
|
|
27
24
|
|
|
28
|
-
def
|
|
29
|
-
|
|
25
|
+
def name
|
|
26
|
+
method_name
|
|
30
27
|
end
|
|
31
28
|
|
|
32
|
-
def
|
|
33
|
-
|
|
29
|
+
def arguments
|
|
30
|
+
@arguments ||= argument_nodes.map { |argument| Argument.new(argument) }
|
|
34
31
|
end
|
|
35
32
|
|
|
36
|
-
|
|
33
|
+
def block_body
|
|
34
|
+
@block_body ||= block_statements
|
|
35
|
+
end
|
|
37
36
|
|
|
38
|
-
|
|
37
|
+
def block_argument_names
|
|
38
|
+
@block_argument_names ||= positional_block_parameter_names
|
|
39
|
+
end
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
@call_element = case element.sexp_type
|
|
43
|
-
when :call
|
|
44
|
-
@element
|
|
45
|
-
when :iter
|
|
46
|
-
@element[1]
|
|
47
|
-
end
|
|
41
|
+
def block?
|
|
42
|
+
!block_node.nil?
|
|
48
43
|
end
|
|
49
44
|
|
|
50
|
-
def
|
|
51
|
-
|
|
45
|
+
def lambda_literal?
|
|
46
|
+
false
|
|
52
47
|
end
|
|
53
48
|
|
|
54
|
-
|
|
55
|
-
|
|
49
|
+
# Provably a Hash: bare `Hash.new`/`Hash[...]` (any args — every `Hash[...]` yields a Hash),
|
|
50
|
+
# not qualified `Foo::Hash`. The is_a?(ConstantReadNode) check is load-bearing:
|
|
51
|
+
# ConstantPathNode#name is :Hash too, so the type (not the name) excludes qualified constants.
|
|
52
|
+
# unwrap_parentheses lets `(Hash).new` match, as the receiver factory normalizes its input.
|
|
53
|
+
def hash?
|
|
54
|
+
return false unless %i[new []].include?(method_name)
|
|
55
|
+
|
|
56
|
+
constant = ReceiverFactory.unwrap_parentheses(element.receiver)
|
|
57
|
+
constant.is_a?(Prism::ConstantReadNode) && constant.name == :Hash
|
|
56
58
|
end
|
|
57
59
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def block_node
|
|
63
|
+
@block_node ||= element.block
|
|
60
64
|
end
|
|
61
65
|
|
|
62
|
-
def
|
|
63
|
-
|
|
64
|
-
@block_present = true
|
|
65
|
-
end
|
|
66
|
+
def argument_nodes
|
|
67
|
+
element.arguments&.arguments || []
|
|
66
68
|
end
|
|
67
69
|
|
|
68
|
-
def
|
|
69
|
-
|
|
70
|
+
def block_statements
|
|
71
|
+
return unless block_node.is_a?(Prism::BlockNode)
|
|
72
|
+
|
|
73
|
+
body = block_node.body
|
|
74
|
+
body.body if body.is_a?(Prism::StatementsNode)
|
|
70
75
|
end
|
|
71
76
|
|
|
72
|
-
#
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
# Only single positional params convert to &:sym, so a splat or keyword param must not count
|
|
78
|
+
# toward block_argument_names.one?
|
|
79
|
+
def positional_block_parameter_names
|
|
80
|
+
return [] unless block_node.is_a?(Prism::BlockNode)
|
|
81
|
+
|
|
82
|
+
params = block_node.parameters
|
|
83
|
+
return [] unless params.is_a?(Prism::BlockParametersNode) && params.parameters
|
|
84
|
+
|
|
85
|
+
positional = params.parameters.requireds + params.parameters.optionals
|
|
86
|
+
positional.map { |param| param.is_a?(Prism::MultiTargetNode) ? nil : param.name }
|
|
77
87
|
end
|
|
78
88
|
end
|
|
79
89
|
|
|
80
|
-
#
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
# A `-> {}` lambda literal. Checks ignore it, so its call attributes are inert
|
|
91
|
+
class LambdaCall < MethodCall
|
|
92
|
+
def receiver = nil
|
|
93
|
+
def method_name = :lambda
|
|
94
|
+
def arguments = []
|
|
95
|
+
def block_body = nil
|
|
96
|
+
def block_argument_names = []
|
|
97
|
+
def block? = true
|
|
98
|
+
def lambda_literal? = true
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
module ReceiverFactory
|
|
102
|
+
PRIMITIVE_NODE_TYPES = [Prism::ArrayNode, Prism::RangeNode, Prism::IntegerNode,
|
|
103
|
+
Prism::FloatNode, Prism::SymbolNode, Prism::StringNode,
|
|
104
|
+
Prism::HashNode].freeze
|
|
105
|
+
|
|
106
|
+
def self.build(node)
|
|
107
|
+
return unless node
|
|
108
|
+
|
|
109
|
+
node = unwrap_parentheses(node)
|
|
110
|
+
case node
|
|
111
|
+
# A ConstantPathNode's #name is the unqualified tail only (Foo::Bar => :Bar), which is all
|
|
112
|
+
# the symbol-to-proc check needs; revisit if a check ever needs the fully-qualified path.
|
|
113
|
+
when Prism::LocalVariableReadNode, Prism::ConstantReadNode,
|
|
114
|
+
Prism::ConstantPathNode then VariableReference.new(node)
|
|
115
|
+
when Prism::CallNode then MethodCall.build(node)
|
|
116
|
+
when *PRIMITIVE_NODE_TYPES then Primitive.new(node)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Peels a single parenthesis level, so `((x))` stays wrapped — matches how callers normalize
|
|
121
|
+
def self.unwrap_parentheses(node)
|
|
122
|
+
if node.is_a?(Prism::ParenthesesNode) &&
|
|
123
|
+
node.body.is_a?(Prism::StatementsNode) &&
|
|
124
|
+
node.body.body.size == 1
|
|
125
|
+
node.body.body.first
|
|
126
|
+
else
|
|
127
|
+
node
|
|
93
128
|
end
|
|
94
129
|
end
|
|
95
130
|
end
|
|
@@ -97,45 +132,70 @@ module Fastererer
|
|
|
97
132
|
class VariableReference
|
|
98
133
|
attr_reader :name
|
|
99
134
|
|
|
100
|
-
def initialize(
|
|
101
|
-
@
|
|
102
|
-
|
|
135
|
+
def initialize(node)
|
|
136
|
+
@name = node.name
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# A bare local/constant reference can't be proven to hold a Hash
|
|
140
|
+
def hash?
|
|
141
|
+
false
|
|
103
142
|
end
|
|
104
143
|
end
|
|
105
144
|
|
|
106
145
|
class Argument
|
|
107
146
|
attr_reader :element
|
|
108
147
|
|
|
109
|
-
def initialize(
|
|
110
|
-
@element =
|
|
111
|
-
end
|
|
148
|
+
def initialize(node)
|
|
149
|
+
@element = node
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
TYPE_BY_NODE_CLASS = {
|
|
153
|
+
Prism::KeywordHashNode => :hash,
|
|
154
|
+
Prism::HashNode => :hash,
|
|
155
|
+
Prism::StringNode => :string,
|
|
156
|
+
Prism::IntegerNode => :integer,
|
|
157
|
+
Prism::SymbolNode => :symbol,
|
|
158
|
+
Prism::FloatNode => :float,
|
|
159
|
+
Prism::RegularExpressionNode => :regexp,
|
|
160
|
+
Prism::NilNode => :nil,
|
|
161
|
+
Prism::TrueNode => :boolean,
|
|
162
|
+
Prism::FalseNode => :boolean,
|
|
163
|
+
Prism::LocalVariableReadNode => :variable,
|
|
164
|
+
Prism::CallNode => :method_call
|
|
165
|
+
}.freeze
|
|
112
166
|
|
|
113
167
|
def type
|
|
114
|
-
@type ||=
|
|
168
|
+
@type ||= TYPE_BY_NODE_CLASS[element.class] || :unknown
|
|
115
169
|
end
|
|
116
170
|
|
|
117
171
|
def value
|
|
118
|
-
@value
|
|
172
|
+
return @value if defined?(@value)
|
|
173
|
+
|
|
174
|
+
@value = case element
|
|
175
|
+
when Prism::StringNode then element.unescaped
|
|
176
|
+
when Prism::IntegerNode, Prism::FloatNode then element.value
|
|
177
|
+
when Prism::SymbolNode then element.unescaped.to_sym
|
|
178
|
+
end
|
|
119
179
|
end
|
|
120
180
|
end
|
|
121
181
|
|
|
122
182
|
class Primitive
|
|
123
183
|
attr_reader :element
|
|
124
184
|
|
|
125
|
-
def initialize(
|
|
126
|
-
@element =
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
def type
|
|
130
|
-
@type ||= @element[0]
|
|
185
|
+
def initialize(node)
|
|
186
|
+
@element = node
|
|
131
187
|
end
|
|
132
188
|
|
|
133
189
|
def range?
|
|
134
|
-
|
|
190
|
+
element.is_a?(Prism::RangeNode)
|
|
135
191
|
end
|
|
136
192
|
|
|
137
193
|
def array?
|
|
138
|
-
|
|
194
|
+
element.is_a?(Prism::ArrayNode)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def hash?
|
|
198
|
+
element.is_a?(Prism::HashNode)
|
|
139
199
|
end
|
|
140
200
|
end
|
|
141
201
|
end
|