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
|
@@ -1,22 +1,37 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'prism'
|
|
4
|
+
|
|
3
5
|
module Fastererer
|
|
4
6
|
class MethodDefinition
|
|
5
|
-
|
|
6
|
-
|
|
7
|
+
PARAMETER_CATEGORIES = %i[requireds optionals rest posts keywords keyword_rest block].freeze
|
|
8
|
+
private_constant :PARAMETER_CATEGORIES
|
|
9
|
+
|
|
10
|
+
attr_reader :element
|
|
7
11
|
|
|
12
|
+
def initialize(node)
|
|
13
|
+
@element = node
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def method_name
|
|
17
|
+
element.name
|
|
18
|
+
end
|
|
8
19
|
alias name method_name
|
|
9
20
|
|
|
10
|
-
def
|
|
11
|
-
@
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
21
|
+
def body
|
|
22
|
+
@body ||= statement_body
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def arguments
|
|
26
|
+
@arguments ||= parameter_nodes.map { |node| MethodDefinitionArgument.new(node) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def block_argument_name
|
|
30
|
+
block_parameter&.name
|
|
16
31
|
end
|
|
17
32
|
|
|
18
33
|
def block?
|
|
19
|
-
|
|
34
|
+
!block_parameter.nil?
|
|
20
35
|
end
|
|
21
36
|
|
|
22
37
|
def setter?
|
|
@@ -25,70 +40,68 @@ module Fastererer
|
|
|
25
40
|
|
|
26
41
|
private
|
|
27
42
|
|
|
28
|
-
def
|
|
29
|
-
element
|
|
43
|
+
def statement_body
|
|
44
|
+
body_node = element.body
|
|
45
|
+
# A rescue/ensure body is a BeginNode wrapping the statements; #statements may be nil
|
|
46
|
+
body_node = body_node.statements if body_node.is_a?(Prism::BeginNode)
|
|
47
|
+
body_node.is_a?(Prism::StatementsNode) ? body_node.body : []
|
|
30
48
|
end
|
|
31
49
|
|
|
32
|
-
def
|
|
33
|
-
|
|
34
|
-
|
|
50
|
+
def parameter_nodes
|
|
51
|
+
params = element.parameters
|
|
52
|
+
return [] unless params
|
|
35
53
|
|
|
36
|
-
|
|
37
|
-
@arguments = arguments_element.map do |argument_element|
|
|
38
|
-
MethodDefinitionArgument.new(argument_element)
|
|
39
|
-
end
|
|
54
|
+
PARAMETER_CATEGORIES.flat_map { |category| Array(params.public_send(category)) }
|
|
40
55
|
end
|
|
41
56
|
|
|
42
|
-
def
|
|
43
|
-
|
|
57
|
+
def block_parameter
|
|
58
|
+
params = element.parameters
|
|
59
|
+
return unless params&.block.is_a?(Prism::BlockParameterNode)
|
|
60
|
+
|
|
61
|
+
params.block
|
|
44
62
|
end
|
|
63
|
+
end
|
|
45
64
|
|
|
46
|
-
|
|
47
|
-
|
|
65
|
+
class MethodDefinitionArgument
|
|
66
|
+
attr_reader :element
|
|
48
67
|
|
|
49
|
-
|
|
68
|
+
def initialize(node)
|
|
69
|
+
@element = node
|
|
50
70
|
end
|
|
51
71
|
|
|
52
|
-
|
|
53
|
-
|
|
72
|
+
# Prism param nodes expose #name (nil when anonymous); MultiTargetNode has none
|
|
73
|
+
def name
|
|
74
|
+
element.respond_to?(:name) ? element.name : nil
|
|
54
75
|
end
|
|
55
|
-
end
|
|
56
76
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def initialize(element)
|
|
61
|
-
@element = element
|
|
62
|
-
set_name
|
|
63
|
-
set_argument_type
|
|
77
|
+
def type
|
|
78
|
+
@type ||= argument_type
|
|
64
79
|
end
|
|
65
80
|
|
|
66
81
|
def regular_argument?
|
|
67
|
-
|
|
82
|
+
type == :regular_argument
|
|
68
83
|
end
|
|
69
84
|
|
|
70
85
|
def default_argument?
|
|
71
|
-
|
|
86
|
+
type == :default_argument
|
|
72
87
|
end
|
|
73
88
|
|
|
74
89
|
def keyword_argument?
|
|
75
|
-
|
|
90
|
+
type == :keyword_argument
|
|
76
91
|
end
|
|
77
92
|
|
|
78
93
|
private
|
|
79
94
|
|
|
80
|
-
def
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
:keyword_argument
|
|
91
|
-
end
|
|
95
|
+
def argument_type
|
|
96
|
+
case element
|
|
97
|
+
when Prism::RequiredParameterNode
|
|
98
|
+
:regular_argument
|
|
99
|
+
when Prism::OptionalParameterNode
|
|
100
|
+
:default_argument
|
|
101
|
+
when Prism::RequiredKeywordParameterNode,
|
|
102
|
+
Prism::OptionalKeywordParameterNode
|
|
103
|
+
:keyword_argument
|
|
104
|
+
end
|
|
92
105
|
end
|
|
93
106
|
end
|
|
94
107
|
end
|
data/lib/fastererer/offense.rb
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'explanation'
|
|
4
|
+
require_relative 'rule_catalog'
|
|
5
|
+
|
|
3
6
|
module Fastererer
|
|
4
7
|
class Offense
|
|
5
8
|
attr_reader :offense_name, :line_number
|
|
@@ -10,72 +13,11 @@ module Fastererer
|
|
|
10
13
|
def initialize(offense_name, line_number)
|
|
11
14
|
@offense_name = offense_name
|
|
12
15
|
@line_number = line_number
|
|
13
|
-
|
|
16
|
+
RuleCatalog.validate!(offense_name)
|
|
14
17
|
end
|
|
15
18
|
|
|
16
19
|
def explanation
|
|
17
|
-
@explanation ||=
|
|
20
|
+
@explanation ||= Explanation.for(offense_name)
|
|
18
21
|
end
|
|
19
|
-
|
|
20
|
-
EXPLANATIONS = {
|
|
21
|
-
rescue_vs_respond_to:
|
|
22
|
-
'Don\'t rescue NoMethodError, rather check with respond_to?',
|
|
23
|
-
|
|
24
|
-
module_eval:
|
|
25
|
-
'Using module_eval is slower than define_method',
|
|
26
|
-
|
|
27
|
-
shuffle_first_vs_sample:
|
|
28
|
-
'Array#shuffle.first is slower than Array#sample',
|
|
29
|
-
|
|
30
|
-
for_loop_vs_each:
|
|
31
|
-
'For loop is slower than using each',
|
|
32
|
-
|
|
33
|
-
each_with_index_vs_while:
|
|
34
|
-
'Using each_with_index is slower than while loop',
|
|
35
|
-
|
|
36
|
-
map_flatten_vs_flat_map:
|
|
37
|
-
'Array#map.flatten(1) is slower than Array#flat_map',
|
|
38
|
-
|
|
39
|
-
reverse_each_vs_reverse_each:
|
|
40
|
-
'Array#reverse.each is slower than Array#reverse_each',
|
|
41
|
-
|
|
42
|
-
select_first_vs_detect:
|
|
43
|
-
'Array#select.first is slower than Array#detect',
|
|
44
|
-
|
|
45
|
-
sort_vs_sort_by:
|
|
46
|
-
'Enumerable#sort is slower than Enumerable#sort_by',
|
|
47
|
-
|
|
48
|
-
fetch_with_argument_vs_block:
|
|
49
|
-
'Hash#fetch with second argument is slower than Hash#fetch with block',
|
|
50
|
-
|
|
51
|
-
keys_each_vs_each_key:
|
|
52
|
-
'Hash#keys.each is slower than Hash#each_key. N.B. Hash#each_key cannot be used if ' \
|
|
53
|
-
'the hash is modified during the each block',
|
|
54
|
-
|
|
55
|
-
hash_merge_bang_vs_hash_brackets:
|
|
56
|
-
'Hash#merge! with one argument is slower than Hash#[]',
|
|
57
|
-
|
|
58
|
-
block_vs_symbol_to_proc:
|
|
59
|
-
'Calling argumentless methods within blocks is slower than using symbol to proc',
|
|
60
|
-
|
|
61
|
-
proc_call_vs_yield:
|
|
62
|
-
'Calling blocks with call is slower than yielding',
|
|
63
|
-
|
|
64
|
-
gsub_vs_tr:
|
|
65
|
-
'Using tr is faster than gsub when replacing a single character in a string with ' \
|
|
66
|
-
'another single character',
|
|
67
|
-
|
|
68
|
-
select_last_vs_reverse_detect:
|
|
69
|
-
'Array#select.last is slower than Array#reverse.detect',
|
|
70
|
-
|
|
71
|
-
getter_vs_attr_reader:
|
|
72
|
-
'Use attr_reader for reading ivars',
|
|
73
|
-
|
|
74
|
-
setter_vs_attr_writer:
|
|
75
|
-
'Use attr_writer for writing to ivars',
|
|
76
|
-
|
|
77
|
-
include_vs_cover_on_range:
|
|
78
|
-
'Use #cover? instead of #include? on ranges'
|
|
79
|
-
}.freeze
|
|
80
22
|
end
|
|
81
23
|
end
|
data/lib/fastererer/painter.rb
CHANGED
data/lib/fastererer/parser.rb
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require '
|
|
3
|
+
require 'prism'
|
|
4
4
|
|
|
5
5
|
module Fastererer
|
|
6
|
-
class
|
|
7
|
-
PARSER_CLASS = RubyParser
|
|
6
|
+
class ParseError < StandardError; end
|
|
8
7
|
|
|
8
|
+
# Single seam around Prism.parse: returns the AST root or raises ParseError
|
|
9
|
+
class Parser
|
|
9
10
|
def self.parse(ruby_code)
|
|
10
|
-
|
|
11
|
+
result = Prism.parse(ruby_code)
|
|
12
|
+
raise ParseError, result.errors.map(&:message).join('; ') if result.failure?
|
|
13
|
+
|
|
14
|
+
result.value
|
|
11
15
|
end
|
|
12
16
|
end
|
|
13
17
|
end
|
|
@@ -1,22 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'prism'
|
|
4
|
+
|
|
3
5
|
module Fastererer
|
|
4
6
|
class RescueCall
|
|
5
|
-
attr_reader :element
|
|
7
|
+
attr_reader :element
|
|
6
8
|
|
|
7
|
-
def initialize(
|
|
8
|
-
@element =
|
|
9
|
-
@rescue_classes = []
|
|
10
|
-
set_rescue_classes
|
|
9
|
+
def initialize(node)
|
|
10
|
+
@element = node
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
@rescue_classes = element[1].drop(1).filter_map do |rescue_reference|
|
|
19
|
-
rescue_reference[1] if rescue_reference.sexp_type == :const
|
|
13
|
+
# Only unqualified constants: a namespaced `Foo::NoMethodError` is not the core NoMethodError,
|
|
14
|
+
# so it must not match the rescue_vs_respond_to check.
|
|
15
|
+
def rescue_classes
|
|
16
|
+
@rescue_classes ||= element.exceptions.filter_map do |exception|
|
|
17
|
+
exception.name if exception.is_a?(Prism::ConstantReadNode)
|
|
20
18
|
end
|
|
21
19
|
end
|
|
22
20
|
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
|
|
5
|
+
module Fastererer
|
|
6
|
+
class UnknownRuleError < StandardError; end
|
|
7
|
+
|
|
8
|
+
# Loads, validates, memoizes and looks up the rule catalog from the i18n locale.
|
|
9
|
+
module RuleCatalog
|
|
10
|
+
LOCALE_PATH = File.expand_path('../../config/locales/en.yml', __dir__)
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
def all
|
|
14
|
+
@all ||= load
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def fetch(offense_name)
|
|
18
|
+
all.fetch(offense_name.to_s) do
|
|
19
|
+
raise UnknownRuleError, "Unknown rule: #{offense_name.inspect}"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Guard-only: raises for an unknown rule, returns nil otherwise.
|
|
24
|
+
def validate!(offense_name)
|
|
25
|
+
fetch(offense_name)
|
|
26
|
+
nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def load
|
|
32
|
+
rows = read_rows
|
|
33
|
+
rows.each { |key, row| validate_row!(key, row) }
|
|
34
|
+
rows.transform_values { |row| row.transform_values(&:freeze).freeze }.freeze
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def read_rows
|
|
38
|
+
loaded = YAML.safe_load_file(LOCALE_PATH)
|
|
39
|
+
rows = loaded.is_a?(Hash) ? loaded.dig('en', 'fastererer', 'rules') : nil
|
|
40
|
+
return rows if rows.is_a?(Hash)
|
|
41
|
+
|
|
42
|
+
raise "Fastererer locale at #{LOCALE_PATH} is missing the 'en.fastererer.rules' section"
|
|
43
|
+
rescue Errno::ENOENT
|
|
44
|
+
raise "Fastererer locale file not found at #{LOCALE_PATH}"
|
|
45
|
+
rescue Psych::SyntaxError => e
|
|
46
|
+
raise "Fastererer locale at #{LOCALE_PATH} is not valid YAML: #{e.message}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def validate_row!(key, row)
|
|
50
|
+
raise "Fastererer rule #{key} is malformed: #{row.inspect}" unless row.is_a?(Hash)
|
|
51
|
+
|
|
52
|
+
validate_url!(key, row['url'])
|
|
53
|
+
validate_description!(key, row['description'])
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def validate_url!(key, url)
|
|
57
|
+
unless url.is_a?(String) && url.start_with?('https://')
|
|
58
|
+
raise "Fastererer rule #{key} has a non-https url: #{url.inspect}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
return if url.match?(/\A[[:print:]]+\z/)
|
|
62
|
+
|
|
63
|
+
raise "Fastererer rule #{key} has a non-printable url: #{url.inspect}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def validate_description!(key, text)
|
|
67
|
+
return if text.is_a?(String) && text.match?(/\A[[:print:][:space:]]+\z/)
|
|
68
|
+
|
|
69
|
+
raise "Fastererer rule #{key} has a non-printable description: #{text.inspect}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Test-only hook: clears the memoized catalog so the YAML can be re-read.
|
|
73
|
+
def reset!
|
|
74
|
+
@all = nil
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fastererer
|
|
4
|
+
# Derives a rubocop-style "Performance/PascalCase" display name from a snake_case rule key.
|
|
5
|
+
module RuleName
|
|
6
|
+
DEPARTMENT = 'Performance'
|
|
7
|
+
|
|
8
|
+
def self.from(offense_name)
|
|
9
|
+
"#{DEPARTMENT}/#{pascal_case(offense_name)}"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.pascal_case(name)
|
|
13
|
+
name.to_s.split('_').filter_map { |part| part.capitalize unless part.empty? }.join
|
|
14
|
+
end
|
|
15
|
+
private_class_method :pascal_case
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -20,6 +20,7 @@ module Fastererer
|
|
|
20
20
|
flatten: :check_flatten_offense,
|
|
21
21
|
fetch: :check_fetch_offense,
|
|
22
22
|
merge!: :check_merge_bang_offense,
|
|
23
|
+
update: :check_update_offense,
|
|
23
24
|
last: :check_last_offense,
|
|
24
25
|
include?: :check_range_include_offense
|
|
25
26
|
}.freeze
|
|
@@ -33,7 +34,7 @@ module Fastererer
|
|
|
33
34
|
end
|
|
34
35
|
|
|
35
36
|
def method_call
|
|
36
|
-
@method_call ||= MethodCall.
|
|
37
|
+
@method_call ||= MethodCall.build(element)
|
|
37
38
|
end
|
|
38
39
|
|
|
39
40
|
private
|
|
@@ -115,13 +116,16 @@ module Fastererer
|
|
|
115
116
|
return unless method_call.arguments.one?
|
|
116
117
|
|
|
117
118
|
first_argument = method_call.arguments.first
|
|
118
|
-
return unless first_argument.type == :hash
|
|
119
|
-
# each key and value is an item by itself.
|
|
120
|
-
return unless first_argument.element.drop(1).count == 2
|
|
119
|
+
return unless first_argument.type == :hash && first_argument.element.elements.one?
|
|
121
120
|
|
|
122
121
|
add_offense(:hash_merge_bang_vs_hash_brackets)
|
|
123
122
|
end
|
|
124
123
|
|
|
124
|
+
# Hash#update is a merge! alias but heavily overloaded, so require a provably-Hash receiver
|
|
125
|
+
def check_update_offense
|
|
126
|
+
check_merge_bang_offense if method_call.receiver&.hash?
|
|
127
|
+
end
|
|
128
|
+
|
|
125
129
|
def check_last_offense
|
|
126
130
|
return unless method_call.receiver.is_a?(MethodCall)
|
|
127
131
|
return unless method_call.receiver.name == :select
|
|
@@ -1,5 +1,6 @@
|
|
|
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/offense'
|
|
@@ -27,33 +28,15 @@ module Fastererer
|
|
|
27
28
|
end
|
|
28
29
|
|
|
29
30
|
def scan_block_call_offense
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
method_call = MethodCall.new(element)
|
|
34
|
-
|
|
35
|
-
if method_call.receiver.is_a?(Fastererer::VariableReference) &&
|
|
36
|
-
method_call.receiver.name == method_definition.block_argument_name &&
|
|
37
|
-
method_call.method_name == :call
|
|
38
|
-
|
|
39
|
-
add_offense(:proc_call_vs_yield) && return
|
|
40
|
-
end
|
|
41
|
-
end
|
|
31
|
+
visitor = ProcCallVisitor.new(method_definition.block_argument_name)
|
|
32
|
+
method_definition.body.each { |node| node.accept(visitor) }
|
|
33
|
+
add_offense(:proc_call_vs_yield) if visitor.proc_call_found?
|
|
42
34
|
end
|
|
43
35
|
|
|
44
36
|
def method_definition
|
|
45
37
|
@method_definition ||= MethodDefinition.new(element)
|
|
46
38
|
end
|
|
47
39
|
|
|
48
|
-
def traverse_tree(sexp_tree, &block)
|
|
49
|
-
sexp_tree.each do |element|
|
|
50
|
-
next unless element.is_a?(Array)
|
|
51
|
-
|
|
52
|
-
yield element
|
|
53
|
-
traverse_tree(element, &block)
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
|
|
57
40
|
def scan_getter_and_setter_offense
|
|
58
41
|
method_definition.setter? ? scan_setter_offense : scan_getter_offense
|
|
59
42
|
end
|
|
@@ -69,12 +52,12 @@ module Fastererer
|
|
|
69
52
|
end
|
|
70
53
|
|
|
71
54
|
def trivial_setter?(first_argument)
|
|
72
|
-
|
|
73
|
-
|
|
55
|
+
body_node = method_definition.body.first
|
|
56
|
+
return false unless body_node.is_a?(Prism::InstanceVariableWriteNode)
|
|
57
|
+
return false unless body_node.value.is_a?(Prism::LocalVariableReadNode)
|
|
74
58
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
body_first[2][1] == first_argument.name
|
|
59
|
+
expected_ivar = :"@#{method_definition.name.to_s.delete_suffix('=')}"
|
|
60
|
+
body_node.name == expected_ivar && body_node.value.name == first_argument.name
|
|
78
61
|
end
|
|
79
62
|
|
|
80
63
|
def scan_getter_offense
|
|
@@ -85,10 +68,55 @@ module Fastererer
|
|
|
85
68
|
end
|
|
86
69
|
|
|
87
70
|
def trivial_getter?
|
|
88
|
-
|
|
71
|
+
body_node = method_definition.body.first
|
|
72
|
+
expected_ivar = :"@#{method_definition.name}"
|
|
89
73
|
|
|
90
|
-
|
|
91
|
-
body_first[1].to_s == "@#{method_definition.name}"
|
|
74
|
+
body_node.is_a?(Prism::InstanceVariableReadNode) && body_node.name == expected_ivar
|
|
92
75
|
end
|
|
93
76
|
end
|
|
77
|
+
|
|
78
|
+
# Finds `block_name.call` in a method body, without descending into nested def/class/module scopes
|
|
79
|
+
# where the block parameter is no longer in scope
|
|
80
|
+
class ProcCallVisitor < Prism::Visitor
|
|
81
|
+
def initialize(block_name)
|
|
82
|
+
super()
|
|
83
|
+
@block_name = block_name
|
|
84
|
+
@proc_call_found = false
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def proc_call_found?
|
|
88
|
+
@proc_call_found
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def visit_call_node(node)
|
|
92
|
+
@proc_call_found = true if proc_call?(node)
|
|
93
|
+
super
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# No super on purpose: these open a new scope where the block param is unbound, so we stop
|
|
97
|
+
# descending rather than attribute their inner `name.call`s to the enclosing method.
|
|
98
|
+
def visit_def_node(_node)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def visit_class_node(_node)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def visit_module_node(_node)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def visit_singleton_class_node(_node)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
def proc_call?(node)
|
|
113
|
+
return false if @block_name.nil? # an anonymous block param (&) is never invoked by name
|
|
114
|
+
|
|
115
|
+
node.receiver.is_a?(Prism::LocalVariableReadNode) &&
|
|
116
|
+
node.receiver.name == @block_name &&
|
|
117
|
+
node.name == :call
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private_constant :ProcCallVisitor
|
|
94
122
|
end
|
|
@@ -14,12 +14,8 @@ module Fastererer
|
|
|
14
14
|
|
|
15
15
|
private
|
|
16
16
|
|
|
17
|
-
def add_offense(offense_name
|
|
18
|
-
self.offense = Fastererer::Offense.new(offense_name,
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def check_offense
|
|
22
|
-
raise NotImplementedError
|
|
17
|
+
def add_offense(offense_name)
|
|
18
|
+
self.offense = Fastererer::Offense.new(offense_name, element.location.start_line)
|
|
23
19
|
end
|
|
24
20
|
end
|
|
25
21
|
end
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'prism'
|
|
3
4
|
require 'fastererer/method_call'
|
|
4
5
|
|
|
5
6
|
module Fastererer
|
|
6
|
-
# Includer must respond to #method_call (returning a MethodCall) and #add_offense(symbol)
|
|
7
|
+
# Includer must respond to #method_call (returning a MethodCall) and #add_offense(symbol)
|
|
8
|
+
# (the latter via the Offensive mixin).
|
|
7
9
|
module SymbolToProcCheck
|
|
8
10
|
private
|
|
9
11
|
|
|
10
12
|
def check_symbol_to_proc
|
|
11
13
|
return unless symbol_to_proc_candidate?
|
|
12
14
|
|
|
13
|
-
body_call = MethodCall.
|
|
15
|
+
body_call = MethodCall.build(method_call.block_body.first)
|
|
14
16
|
return unless symbol_to_proc_body?(body_call)
|
|
15
17
|
|
|
16
18
|
add_offense(:block_vs_symbol_to_proc)
|
|
@@ -18,12 +20,18 @@ module Fastererer
|
|
|
18
20
|
|
|
19
21
|
def symbol_to_proc_candidate?
|
|
20
22
|
method_call.block_argument_names.one? &&
|
|
21
|
-
|
|
22
|
-
method_call.block_body.sexp_type == :call &&
|
|
23
|
+
single_call_body? &&
|
|
23
24
|
method_call.arguments.none? &&
|
|
24
25
|
!method_call.lambda_literal?
|
|
25
26
|
end
|
|
26
27
|
|
|
28
|
+
# A safe-nav body (foo&.bar) is excluded: arr.map(&:bar) raises on a nil element, so the rewrite
|
|
29
|
+
# would not preserve behavior
|
|
30
|
+
def single_call_body?
|
|
31
|
+
body = method_call.block_body
|
|
32
|
+
body&.size == 1 && body.first.is_a?(Prism::CallNode) && !body.first.safe_navigation?
|
|
33
|
+
end
|
|
34
|
+
|
|
27
35
|
def symbol_to_proc_body?(body_call)
|
|
28
36
|
body_call.arguments.none? &&
|
|
29
37
|
!body_call.block? &&
|
data/lib/fastererer/version.rb
CHANGED