hone 0.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 +7 -0
- data/.standard.yml +8 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +201 -0
- data/Rakefile +10 -0
- data/examples/.hone/harness.rb +41 -0
- data/examples/README.md +22 -0
- data/examples/allocation_patterns.rb +66 -0
- data/examples/cpu_patterns.rb +50 -0
- data/examples/jit_patterns.rb +69 -0
- data/exe/hone +7 -0
- data/lib/hone/adapters/base.rb +35 -0
- data/lib/hone/adapters/fasterer.rb +38 -0
- data/lib/hone/adapters/rubocop_performance.rb +85 -0
- data/lib/hone/analyzer.rb +258 -0
- data/lib/hone/cli.rb +247 -0
- data/lib/hone/config.rb +93 -0
- data/lib/hone/correlator.rb +250 -0
- data/lib/hone/exit_codes.rb +10 -0
- data/lib/hone/finding.rb +64 -0
- data/lib/hone/finding_filter.rb +57 -0
- data/lib/hone/formatters/base.rb +25 -0
- data/lib/hone/formatters/filterable.rb +31 -0
- data/lib/hone/formatters/github.rb +71 -0
- data/lib/hone/formatters/json.rb +75 -0
- data/lib/hone/formatters/junit.rb +154 -0
- data/lib/hone/formatters/sarif.rb +179 -0
- data/lib/hone/formatters/tsv.rb +49 -0
- data/lib/hone/harness.rb +57 -0
- data/lib/hone/harness_generator.rb +128 -0
- data/lib/hone/harness_runner.rb +172 -0
- data/lib/hone/method_map.rb +140 -0
- data/lib/hone/patterns/README.md +174 -0
- data/lib/hone/patterns/array_compact.rb +105 -0
- data/lib/hone/patterns/array_include_set.rb +34 -0
- data/lib/hone/patterns/base.rb +90 -0
- data/lib/hone/patterns/block_to_proc.rb +109 -0
- data/lib/hone/patterns/bsearch_vs_find.rb +80 -0
- data/lib/hone/patterns/chars_map_ord.rb +42 -0
- data/lib/hone/patterns/chars_to_variable.rb +136 -0
- data/lib/hone/patterns/chars_to_variable_tainted.rb +136 -0
- data/lib/hone/patterns/constant_regexp.rb +74 -0
- data/lib/hone/patterns/count_vs_size.rb +35 -0
- data/lib/hone/patterns/divmod.rb +92 -0
- data/lib/hone/patterns/dynamic_ivar.rb +44 -0
- data/lib/hone/patterns/dynamic_ivar_get.rb +33 -0
- data/lib/hone/patterns/each_with_index.rb +116 -0
- data/lib/hone/patterns/each_with_object.rb +63 -0
- data/lib/hone/patterns/flatten_once.rb +28 -0
- data/lib/hone/patterns/gsub_to_tr.rb +48 -0
- data/lib/hone/patterns/hash_each_key.rb +41 -0
- data/lib/hone/patterns/hash_each_value.rb +31 -0
- data/lib/hone/patterns/hash_keys_include.rb +30 -0
- data/lib/hone/patterns/hash_merge_bang.rb +33 -0
- data/lib/hone/patterns/hash_values_include.rb +31 -0
- data/lib/hone/patterns/inject_sum.rb +48 -0
- data/lib/hone/patterns/kernel_loop.rb +27 -0
- data/lib/hone/patterns/lazy_ivar.rb +39 -0
- data/lib/hone/patterns/map_compact.rb +32 -0
- data/lib/hone/patterns/map_flatten.rb +31 -0
- data/lib/hone/patterns/map_select_chain.rb +32 -0
- data/lib/hone/patterns/parallel_assignment.rb +127 -0
- data/lib/hone/patterns/positive_predicate.rb +27 -0
- data/lib/hone/patterns/range_include.rb +34 -0
- data/lib/hone/patterns/redundant_string_chars.rb +82 -0
- data/lib/hone/patterns/regexp_match.rb +126 -0
- data/lib/hone/patterns/reverse_each.rb +30 -0
- data/lib/hone/patterns/reverse_first.rb +40 -0
- data/lib/hone/patterns/select_count.rb +32 -0
- data/lib/hone/patterns/select_first.rb +31 -0
- data/lib/hone/patterns/select_map.rb +32 -0
- data/lib/hone/patterns/shuffle_first.rb +30 -0
- data/lib/hone/patterns/slice_with_length.rb +48 -0
- data/lib/hone/patterns/sort_by_first.rb +31 -0
- data/lib/hone/patterns/sort_by_last.rb +31 -0
- data/lib/hone/patterns/sort_first.rb +52 -0
- data/lib/hone/patterns/sort_last.rb +30 -0
- data/lib/hone/patterns/sort_reverse.rb +53 -0
- data/lib/hone/patterns/string_casecmp.rb +54 -0
- data/lib/hone/patterns/string_chars_each.rb +56 -0
- data/lib/hone/patterns/string_concat_in_loop.rb +116 -0
- data/lib/hone/patterns/string_delete_prefix.rb +53 -0
- data/lib/hone/patterns/string_delete_suffix.rb +53 -0
- data/lib/hone/patterns/string_empty.rb +64 -0
- data/lib/hone/patterns/string_end_with.rb +81 -0
- data/lib/hone/patterns/string_shovel.rb +75 -0
- data/lib/hone/patterns/string_start_with.rb +80 -0
- data/lib/hone/patterns/taint_tracking_base.rb +230 -0
- data/lib/hone/patterns/times_map.rb +38 -0
- data/lib/hone/patterns/uniq_by.rb +32 -0
- data/lib/hone/patterns/yield_vs_block.rb +72 -0
- data/lib/hone/profilers/base.rb +162 -0
- data/lib/hone/profilers/factory.rb +31 -0
- data/lib/hone/profilers/memory_profiler.rb +213 -0
- data/lib/hone/profilers/stackprof.rb +99 -0
- data/lib/hone/profilers/vernier.rb +147 -0
- data/lib/hone/reporter.rb +371 -0
- data/lib/hone/scanner.rb +75 -0
- data/lib/hone/suggestion_generator.rb +23 -0
- data/lib/hone/version.rb +5 -0
- data/lib/hone.rb +108 -0
- data/logo.png +0 -0
- data/sig/hone.rbs +4 -0
- metadata +176 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hone
|
|
4
|
+
module Patterns
|
|
5
|
+
# Pattern: array.map { ... }.select { ... } -> array.filter_map { ... }
|
|
6
|
+
#
|
|
7
|
+
# Chaining .map and .select creates an intermediate array.
|
|
8
|
+
# Using .filter_map combines both operations in one pass.
|
|
9
|
+
#
|
|
10
|
+
# From sqids-ruby commit aa4e253
|
|
11
|
+
class MapSelectChain < Base
|
|
12
|
+
self.pattern_id = :map_select_chain
|
|
13
|
+
self.optimization_type = :allocation
|
|
14
|
+
|
|
15
|
+
def visit_call_node(node)
|
|
16
|
+
super
|
|
17
|
+
|
|
18
|
+
# Look for: .select { } where receiver is .map (with block or symbol arg)
|
|
19
|
+
return unless node.name == :select && block_attached?(node)
|
|
20
|
+
|
|
21
|
+
receiver = node.receiver
|
|
22
|
+
return unless receiver.is_a?(Prism::CallNode) && receiver.name == :map && block_attached?(receiver)
|
|
23
|
+
|
|
24
|
+
add_finding(
|
|
25
|
+
node,
|
|
26
|
+
message: "Use `.filter_map { }` instead of `.map { }.select { }` to avoid intermediate array",
|
|
27
|
+
speedup: "Fewer allocations"
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hone
|
|
4
|
+
module Patterns
|
|
5
|
+
# Pattern: Sequential array index assignments -> parallel assignment
|
|
6
|
+
#
|
|
7
|
+
# When assigning multiple variables from sequential array indices (0, 1, 2...),
|
|
8
|
+
# Ruby's parallel assignment syntax is more idiomatic and can be slightly faster
|
|
9
|
+
# as it avoids multiple array access operations.
|
|
10
|
+
#
|
|
11
|
+
# Example:
|
|
12
|
+
# # Before
|
|
13
|
+
# a = arr[0]
|
|
14
|
+
# b = arr[1]
|
|
15
|
+
# c = arr[2]
|
|
16
|
+
#
|
|
17
|
+
# # After
|
|
18
|
+
# a, b, c = arr
|
|
19
|
+
#
|
|
20
|
+
# Impact: Minor performance improvement, but more idiomatic Ruby
|
|
21
|
+
class ParallelAssignment < Base
|
|
22
|
+
self.pattern_id = :parallel_assignment
|
|
23
|
+
self.optimization_type = :cpu
|
|
24
|
+
|
|
25
|
+
def visit_statements_node(node)
|
|
26
|
+
check_sequential_assignments(node.body)
|
|
27
|
+
super
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def visit_begin_node(node)
|
|
31
|
+
check_sequential_assignments(node.statements&.body || [])
|
|
32
|
+
super
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def check_sequential_assignments(statements)
|
|
38
|
+
return if statements.nil? || statements.size < 2
|
|
39
|
+
|
|
40
|
+
i = 0
|
|
41
|
+
while i < statements.size
|
|
42
|
+
sequence = find_assignment_sequence(statements, i)
|
|
43
|
+
|
|
44
|
+
if sequence.size >= 2
|
|
45
|
+
report_sequence(sequence)
|
|
46
|
+
i += sequence.size
|
|
47
|
+
else
|
|
48
|
+
i += 1
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def find_assignment_sequence(statements, start_index)
|
|
54
|
+
sequence = []
|
|
55
|
+
array_name = nil
|
|
56
|
+
expected_index = 0
|
|
57
|
+
|
|
58
|
+
(start_index...statements.size).each do |i|
|
|
59
|
+
stmt = statements[i]
|
|
60
|
+
break unless stmt.is_a?(Prism::LocalVariableWriteNode)
|
|
61
|
+
|
|
62
|
+
array_access = extract_array_access(stmt.value)
|
|
63
|
+
break unless array_access
|
|
64
|
+
|
|
65
|
+
name, index = array_access
|
|
66
|
+
|
|
67
|
+
if sequence.empty?
|
|
68
|
+
array_name = name
|
|
69
|
+
expected_index = index
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
break unless name == array_name && index == expected_index
|
|
73
|
+
|
|
74
|
+
sequence << stmt
|
|
75
|
+
expected_index += 1
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
sequence
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def extract_array_access(node)
|
|
82
|
+
return nil unless node.is_a?(Prism::CallNode)
|
|
83
|
+
return nil unless node.name == :[]
|
|
84
|
+
|
|
85
|
+
args = node.arguments&.arguments
|
|
86
|
+
return nil unless args&.size == 1
|
|
87
|
+
|
|
88
|
+
index_node = args.first
|
|
89
|
+
return nil unless index_node.is_a?(Prism::IntegerNode)
|
|
90
|
+
|
|
91
|
+
index = index_node.value
|
|
92
|
+
return nil if index.negative?
|
|
93
|
+
|
|
94
|
+
receiver = node.receiver
|
|
95
|
+
array_name = extract_variable_name(receiver)
|
|
96
|
+
return nil unless array_name
|
|
97
|
+
|
|
98
|
+
[array_name, index]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def extract_variable_name(node)
|
|
102
|
+
case node
|
|
103
|
+
when Prism::LocalVariableReadNode
|
|
104
|
+
node.name
|
|
105
|
+
when Prism::InstanceVariableReadNode
|
|
106
|
+
node.name
|
|
107
|
+
when Prism::ClassVariableReadNode
|
|
108
|
+
node.name
|
|
109
|
+
when Prism::GlobalVariableReadNode
|
|
110
|
+
node.name
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def report_sequence(sequence)
|
|
115
|
+
var_names = sequence.map(&:name).join(", ")
|
|
116
|
+
array_access = sequence.first.value
|
|
117
|
+
array_name = extract_variable_name(array_access.receiver)
|
|
118
|
+
|
|
119
|
+
add_finding(
|
|
120
|
+
sequence.first,
|
|
121
|
+
message: "Use parallel assignment `#{var_names} = #{array_name}` instead of sequential array index assignments",
|
|
122
|
+
speedup: "Minor, but more idiomatic"
|
|
123
|
+
)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hone
|
|
4
|
+
module Patterns
|
|
5
|
+
# Pattern: (expr).positive? -> (expr) > 0
|
|
6
|
+
#
|
|
7
|
+
# In hot paths, the method call overhead of .positive? can add up.
|
|
8
|
+
# Direct comparison is faster.
|
|
9
|
+
#
|
|
10
|
+
# From sqids-ruby commit 8a74142
|
|
11
|
+
class PositivePredicate < Base
|
|
12
|
+
self.pattern_id = :positive_predicate
|
|
13
|
+
self.optimization_type = :cpu
|
|
14
|
+
|
|
15
|
+
def visit_call_node(node)
|
|
16
|
+
super
|
|
17
|
+
return unless node.name == :positive? && node.arguments.nil?
|
|
18
|
+
|
|
19
|
+
add_finding(
|
|
20
|
+
node,
|
|
21
|
+
message: "Use `> 0` instead of `.positive?` in hot paths to avoid method call overhead",
|
|
22
|
+
speedup: "Minor, but adds up in tight loops"
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hone
|
|
4
|
+
module Patterns
|
|
5
|
+
# Pattern: range.include?(x) -> range.cover?(x)
|
|
6
|
+
#
|
|
7
|
+
# include? iterates the range to check membership.
|
|
8
|
+
# cover? just checks if value is between bounds (O(1)).
|
|
9
|
+
#
|
|
10
|
+
# Note: Semantics differ for non-numeric ranges. cover? checks bounds only,
|
|
11
|
+
# include? checks actual membership. For numeric ranges they're equivalent.
|
|
12
|
+
class RangeInclude < Base
|
|
13
|
+
self.pattern_id = :range_include
|
|
14
|
+
self.optimization_type = :cpu
|
|
15
|
+
|
|
16
|
+
def visit_call_node(node)
|
|
17
|
+
super
|
|
18
|
+
|
|
19
|
+
# Look for: .include?(x) on a range literal
|
|
20
|
+
return unless node.name == :include?
|
|
21
|
+
return unless node.arguments&.arguments&.size == 1
|
|
22
|
+
|
|
23
|
+
receiver = node.receiver
|
|
24
|
+
return unless receiver.is_a?(Prism::RangeNode)
|
|
25
|
+
|
|
26
|
+
add_finding(
|
|
27
|
+
node,
|
|
28
|
+
message: "Use `.cover?` instead of `.include?` on ranges for O(1) bounds check",
|
|
29
|
+
speedup: "O(n) iteration to O(1) comparison (for numeric ranges)"
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hone
|
|
4
|
+
module Patterns
|
|
5
|
+
# Detects `str.chars[i]` which should be `str[i]`
|
|
6
|
+
#
|
|
7
|
+
# str.chars[n] creates an array of single-character strings, then indexes it.
|
|
8
|
+
# Direct string indexing is much faster.
|
|
9
|
+
#
|
|
10
|
+
# @example Bad
|
|
11
|
+
# str.chars[0]
|
|
12
|
+
# str.chars.first
|
|
13
|
+
# str.chars.last
|
|
14
|
+
#
|
|
15
|
+
# @example Good
|
|
16
|
+
# str[0]
|
|
17
|
+
# str[0]
|
|
18
|
+
# str[-1]
|
|
19
|
+
#
|
|
20
|
+
class RedundantStringChars < Base
|
|
21
|
+
self.pattern_id = :redundant_string_chars
|
|
22
|
+
self.optimization_type = :allocation
|
|
23
|
+
|
|
24
|
+
def visit_call_node(node)
|
|
25
|
+
if chained_chars_index?(node)
|
|
26
|
+
replacement = suggest_replacement(node)
|
|
27
|
+
add_finding(
|
|
28
|
+
node,
|
|
29
|
+
message: "Use `#{replacement}` instead of `chars[...]` to avoid array allocation",
|
|
30
|
+
speedup: "Significant - avoids creating array of all characters"
|
|
31
|
+
)
|
|
32
|
+
elsif chained_chars_first_last?(node)
|
|
33
|
+
replacement = suggest_first_last_replacement(node)
|
|
34
|
+
add_finding(
|
|
35
|
+
node,
|
|
36
|
+
message: "Use `#{replacement}` instead of `chars.#{node.name}` to avoid array allocation",
|
|
37
|
+
speedup: "Significant - avoids creating array of all characters"
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
super
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def chained_chars_index?(node)
|
|
47
|
+
return false unless node.name == :[]
|
|
48
|
+
|
|
49
|
+
receiver = node.receiver
|
|
50
|
+
return false unless receiver.is_a?(Prism::CallNode)
|
|
51
|
+
return false unless receiver.name == :chars
|
|
52
|
+
|
|
53
|
+
true
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def chained_chars_first_last?(node)
|
|
57
|
+
return false unless %i[first last].include?(node.name)
|
|
58
|
+
|
|
59
|
+
receiver = node.receiver
|
|
60
|
+
return false unless receiver.is_a?(Prism::CallNode)
|
|
61
|
+
return false unless receiver.name == :chars
|
|
62
|
+
|
|
63
|
+
true
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def suggest_replacement(node)
|
|
67
|
+
receiver = node.receiver.receiver
|
|
68
|
+
receiver_src = receiver ? receiver.slice : "str"
|
|
69
|
+
args = node.arguments&.arguments&.first
|
|
70
|
+
index = args ? args.slice : "i"
|
|
71
|
+
"#{receiver_src}[#{index}]"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def suggest_first_last_replacement(node)
|
|
75
|
+
receiver = node.receiver.receiver
|
|
76
|
+
receiver_src = receiver ? receiver.slice : "str"
|
|
77
|
+
index = (node.name == :first) ? "0" : "-1"
|
|
78
|
+
"#{receiver_src}[#{index}]"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hone
|
|
4
|
+
module Patterns
|
|
5
|
+
# Pattern: str =~ /pattern/ or /pattern/.match(str) for boolean -> str.match?(/pattern/)
|
|
6
|
+
#
|
|
7
|
+
# When the result of =~ or .match is only used for truthiness (in if/unless/ternary),
|
|
8
|
+
# using .match? avoids creating a MatchData object.
|
|
9
|
+
#
|
|
10
|
+
# Example:
|
|
11
|
+
# # Bad - creates MatchData object
|
|
12
|
+
# if str =~ /pattern/
|
|
13
|
+
# if /pattern/.match(str)
|
|
14
|
+
#
|
|
15
|
+
# # Good - returns boolean without allocation
|
|
16
|
+
# if str.match?(/pattern/)
|
|
17
|
+
class RegexpMatch < Base
|
|
18
|
+
self.pattern_id = :regexp_match
|
|
19
|
+
self.optimization_type = :allocation
|
|
20
|
+
|
|
21
|
+
def initialize(file_path)
|
|
22
|
+
super
|
|
23
|
+
@in_boolean_context = false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Track boolean contexts: if conditions
|
|
27
|
+
def visit_if_node(node)
|
|
28
|
+
visit_in_boolean_context(node.predicate)
|
|
29
|
+
node.statements&.accept(self)
|
|
30
|
+
node.subsequent&.accept(self)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Track boolean contexts: unless conditions
|
|
34
|
+
def visit_unless_node(node)
|
|
35
|
+
visit_in_boolean_context(node.predicate)
|
|
36
|
+
node.statements&.accept(self)
|
|
37
|
+
node.else_clause&.accept(self)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Track boolean contexts: while conditions
|
|
41
|
+
def visit_while_node(node)
|
|
42
|
+
visit_in_boolean_context(node.predicate)
|
|
43
|
+
node.statements&.accept(self)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Track boolean contexts: until conditions
|
|
47
|
+
def visit_until_node(node)
|
|
48
|
+
visit_in_boolean_context(node.predicate)
|
|
49
|
+
node.statements&.accept(self)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Track boolean contexts: ternary operator condition
|
|
53
|
+
def visit_ternary_node(node)
|
|
54
|
+
visit_in_boolean_context(node.predicate)
|
|
55
|
+
node.true_expression&.accept(self)
|
|
56
|
+
node.false_expression&.accept(self)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Track boolean contexts: && and || operators
|
|
60
|
+
def visit_and_node(node)
|
|
61
|
+
visit_in_boolean_context(node.left)
|
|
62
|
+
visit_in_boolean_context(node.right)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def visit_or_node(node)
|
|
66
|
+
visit_in_boolean_context(node.left)
|
|
67
|
+
visit_in_boolean_context(node.right)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def visit_call_node(node)
|
|
71
|
+
super
|
|
72
|
+
|
|
73
|
+
if @in_boolean_context
|
|
74
|
+
check_regexp_match(node)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Detect =~ operator (str =~ /pattern/ or /pattern/ =~ str)
|
|
79
|
+
def visit_match_last_line_node(node)
|
|
80
|
+
return unless @in_boolean_context
|
|
81
|
+
|
|
82
|
+
add_regexp_finding(node)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def visit_match_write_node(node)
|
|
86
|
+
return unless @in_boolean_context
|
|
87
|
+
|
|
88
|
+
add_regexp_finding(node)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def visit_in_boolean_context(node)
|
|
94
|
+
return unless node
|
|
95
|
+
|
|
96
|
+
with_context(:@in_boolean_context, true) do
|
|
97
|
+
node.accept(self)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def check_regexp_match(node)
|
|
102
|
+
# Check for =~ operator
|
|
103
|
+
if node.name == :=~
|
|
104
|
+
add_regexp_finding(node)
|
|
105
|
+
return
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Check for .match(str) without block
|
|
109
|
+
if node.name == :match && !block_attached?(node)
|
|
110
|
+
# Verify it has arguments (not just calling match on something)
|
|
111
|
+
return unless node.arguments&.arguments&.any?
|
|
112
|
+
|
|
113
|
+
add_regexp_finding(node)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def add_regexp_finding(node)
|
|
118
|
+
add_finding(
|
|
119
|
+
node,
|
|
120
|
+
message: "Use `.match?` instead of `=~` or `.match` when only checking for a match",
|
|
121
|
+
speedup: "Avoids creating MatchData object"
|
|
122
|
+
)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hone
|
|
4
|
+
module Patterns
|
|
5
|
+
# Pattern: array.reverse.each { } -> array.reverse_each { }
|
|
6
|
+
#
|
|
7
|
+
# Calling .reverse.each creates an intermediate reversed array.
|
|
8
|
+
# Using .reverse_each iterates in reverse order without allocation.
|
|
9
|
+
class ReverseEach < Base
|
|
10
|
+
self.pattern_id = :reverse_each
|
|
11
|
+
self.optimization_type = :allocation
|
|
12
|
+
|
|
13
|
+
def visit_call_node(node)
|
|
14
|
+
super
|
|
15
|
+
|
|
16
|
+
# Look for: .each { } where receiver is .reverse
|
|
17
|
+
return unless node.name == :each && block_attached?(node)
|
|
18
|
+
|
|
19
|
+
receiver = node.receiver
|
|
20
|
+
return unless receiver.is_a?(Prism::CallNode) && receiver.name == :reverse
|
|
21
|
+
|
|
22
|
+
add_finding(
|
|
23
|
+
node,
|
|
24
|
+
message: "Use `.reverse_each { }` instead of `.reverse.each { }` to avoid intermediate array",
|
|
25
|
+
speedup: "Avoids intermediate array allocation"
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hone
|
|
4
|
+
module Patterns
|
|
5
|
+
# Pattern: array.reverse.first -> array.last
|
|
6
|
+
# array.reverse.first(n) -> array.last(n).reverse
|
|
7
|
+
#
|
|
8
|
+
# reverse.first reverses the entire array then takes the first element(s).
|
|
9
|
+
# Using last directly accesses the end of the array without creating an
|
|
10
|
+
# intermediate reversed array.
|
|
11
|
+
class ReverseFirst < Base
|
|
12
|
+
self.pattern_id = :reverse_first
|
|
13
|
+
self.optimization_type = :allocation
|
|
14
|
+
|
|
15
|
+
def visit_call_node(node)
|
|
16
|
+
super
|
|
17
|
+
|
|
18
|
+
# Look for: .first or .first(n) where receiver is .reverse
|
|
19
|
+
return unless node.name == :first
|
|
20
|
+
|
|
21
|
+
receiver = node.receiver
|
|
22
|
+
return unless receiver.is_a?(Prism::CallNode) && receiver.name == :reverse
|
|
23
|
+
|
|
24
|
+
if node.arguments.nil?
|
|
25
|
+
add_finding(
|
|
26
|
+
node,
|
|
27
|
+
message: "Use `.last` instead of `.reverse.first` to avoid reversing entire array",
|
|
28
|
+
speedup: "Avoids reversing entire array"
|
|
29
|
+
)
|
|
30
|
+
else
|
|
31
|
+
add_finding(
|
|
32
|
+
node,
|
|
33
|
+
message: "Use `.last(n).reverse` instead of `.reverse.first(n)` to avoid reversing entire array",
|
|
34
|
+
speedup: "Avoids reversing entire array"
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hone
|
|
4
|
+
module Patterns
|
|
5
|
+
# Pattern: array.select { }.count -> array.count { }
|
|
6
|
+
#
|
|
7
|
+
# select { }.count creates a temporary array of matches then counts it.
|
|
8
|
+
# count { } counts matches directly without intermediate allocation.
|
|
9
|
+
class SelectCount < Base
|
|
10
|
+
self.pattern_id = :select_count
|
|
11
|
+
self.optimization_type = :allocation
|
|
12
|
+
|
|
13
|
+
def visit_call_node(node)
|
|
14
|
+
super
|
|
15
|
+
|
|
16
|
+
# Look for: .count/.size/.length (no args) where receiver is .select with block
|
|
17
|
+
return unless %i[count size length].include?(node.name)
|
|
18
|
+
return unless node.arguments.nil?
|
|
19
|
+
|
|
20
|
+
receiver = node.receiver
|
|
21
|
+
return unless receiver.is_a?(Prism::CallNode)
|
|
22
|
+
return unless receiver.name == :select && block_attached?(receiver)
|
|
23
|
+
|
|
24
|
+
add_finding(
|
|
25
|
+
node,
|
|
26
|
+
message: "Use `.count { }` instead of `.select { }.#{node.name}` to avoid intermediate array",
|
|
27
|
+
speedup: "Counts directly without allocating intermediate array"
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hone
|
|
4
|
+
module Patterns
|
|
5
|
+
# Pattern: array.select { }.first -> array.detect { } / array.find { }
|
|
6
|
+
#
|
|
7
|
+
# select.first iterates the entire array building a new array, then takes first.
|
|
8
|
+
# detect/find stops iteration as soon as a match is found.
|
|
9
|
+
class SelectFirst < Base
|
|
10
|
+
self.pattern_id = :select_first
|
|
11
|
+
self.optimization_type = :allocation
|
|
12
|
+
|
|
13
|
+
def visit_call_node(node)
|
|
14
|
+
super
|
|
15
|
+
|
|
16
|
+
# Look for: .first where receiver is .select with a block
|
|
17
|
+
return unless node.name == :first && node.arguments.nil?
|
|
18
|
+
|
|
19
|
+
receiver = node.receiver
|
|
20
|
+
return unless receiver.is_a?(Prism::CallNode)
|
|
21
|
+
return unless receiver.name == :select && block_attached?(receiver)
|
|
22
|
+
|
|
23
|
+
add_finding(
|
|
24
|
+
node,
|
|
25
|
+
message: "Use `.detect { }` or `.find { }` instead of `.select { }.first` to stop at first match",
|
|
26
|
+
speedup: "Avoids iterating entire array and intermediate array allocation"
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hone
|
|
4
|
+
module Patterns
|
|
5
|
+
# Pattern: array.select { }.map { } -> array.filter_map { }
|
|
6
|
+
#
|
|
7
|
+
# select.map creates an intermediate array from select, then maps over it.
|
|
8
|
+
# filter_map combines both operations in a single pass, avoiding the
|
|
9
|
+
# intermediate allocation.
|
|
10
|
+
class SelectMap < Base
|
|
11
|
+
self.pattern_id = :select_map
|
|
12
|
+
self.optimization_type = :allocation
|
|
13
|
+
|
|
14
|
+
def visit_call_node(node)
|
|
15
|
+
super
|
|
16
|
+
|
|
17
|
+
# Look for: .map { } where receiver is .select { }
|
|
18
|
+
return unless node.name == :map && block_attached?(node)
|
|
19
|
+
|
|
20
|
+
receiver = node.receiver
|
|
21
|
+
return unless receiver.is_a?(Prism::CallNode)
|
|
22
|
+
return unless receiver.name == :select && block_attached?(receiver)
|
|
23
|
+
|
|
24
|
+
add_finding(
|
|
25
|
+
node,
|
|
26
|
+
message: "Use `.filter_map { }` instead of `.select { }.map { }` to avoid intermediate array",
|
|
27
|
+
speedup: "Avoids intermediate array"
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hone
|
|
4
|
+
module Patterns
|
|
5
|
+
# Pattern: array.shuffle.first -> array.sample
|
|
6
|
+
#
|
|
7
|
+
# shuffle.first shuffles the entire array then takes the first element.
|
|
8
|
+
# sample directly selects a random element without creating intermediate array.
|
|
9
|
+
class ShuffleFirst < Base
|
|
10
|
+
self.pattern_id = :shuffle_first
|
|
11
|
+
self.optimization_type = :allocation
|
|
12
|
+
|
|
13
|
+
def visit_call_node(node)
|
|
14
|
+
super
|
|
15
|
+
|
|
16
|
+
# Look for: .first where receiver is .shuffle
|
|
17
|
+
return unless node.name == :first && node.arguments.nil?
|
|
18
|
+
|
|
19
|
+
receiver = node.receiver
|
|
20
|
+
return unless receiver.is_a?(Prism::CallNode) && receiver.name == :shuffle
|
|
21
|
+
|
|
22
|
+
add_finding(
|
|
23
|
+
node,
|
|
24
|
+
message: "Use `.sample` instead of `.shuffle.first` to avoid shuffling entire array",
|
|
25
|
+
speedup: "Avoids O(n) shuffle and intermediate array allocation"
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hone
|
|
4
|
+
module Patterns
|
|
5
|
+
# Pattern: str[n, str.length] -> str[n..]
|
|
6
|
+
#
|
|
7
|
+
# Endless range syntax avoids the length calculation.
|
|
8
|
+
#
|
|
9
|
+
# From sqids-ruby commit 9413b68
|
|
10
|
+
class SliceWithLength < Base
|
|
11
|
+
self.pattern_id = :slice_with_length
|
|
12
|
+
self.optimization_type = :cpu
|
|
13
|
+
|
|
14
|
+
def visit_call_node(node)
|
|
15
|
+
super
|
|
16
|
+
|
|
17
|
+
# Look for: receiver[offset, receiver.length] pattern
|
|
18
|
+
return unless node.name == :[] && node.arguments
|
|
19
|
+
|
|
20
|
+
args = node.arguments.arguments
|
|
21
|
+
return unless args&.length == 2
|
|
22
|
+
|
|
23
|
+
first_arg = args[0]
|
|
24
|
+
second_arg = args[1]
|
|
25
|
+
|
|
26
|
+
# Check if second arg is receiver.length or receiver.size
|
|
27
|
+
return unless second_arg.is_a?(Prism::CallNode)
|
|
28
|
+
return unless %i[length size].include?(second_arg.name)
|
|
29
|
+
return unless nodes_match?(node.receiver, second_arg.receiver)
|
|
30
|
+
|
|
31
|
+
add_finding(
|
|
32
|
+
node,
|
|
33
|
+
message: "Use endless range `[#{first_arg.location.slice}..]` instead of `[#{first_arg.location.slice}, #{second_arg.location.slice}]`",
|
|
34
|
+
speedup: "Minor, avoids length calculation"
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
# Simple check if two nodes represent the same variable/expression
|
|
41
|
+
def nodes_match?(node1, node2)
|
|
42
|
+
return false unless node1.instance_of?(node2.class)
|
|
43
|
+
|
|
44
|
+
node1.location.slice == node2.location.slice
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|