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,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hone
|
|
4
|
+
module Patterns
|
|
5
|
+
# Pattern: instance_variable_set("@#{name}", value)
|
|
6
|
+
#
|
|
7
|
+
# Dynamic instance variable creation causes object shape transitions.
|
|
8
|
+
# YJIT optimizes based on stable object shapes - when shapes change,
|
|
9
|
+
# it must deoptimize. Use a Hash for dynamic data instead.
|
|
10
|
+
#
|
|
11
|
+
# Impact: Significant with YJIT, none without
|
|
12
|
+
class DynamicIvar < Base
|
|
13
|
+
self.pattern_id = :dynamic_ivar
|
|
14
|
+
self.optimization_type = :jit
|
|
15
|
+
|
|
16
|
+
def visit_call_node(node)
|
|
17
|
+
super
|
|
18
|
+
|
|
19
|
+
return unless node.name == :instance_variable_set
|
|
20
|
+
|
|
21
|
+
first_arg = node.arguments&.arguments&.first
|
|
22
|
+
return unless first_arg
|
|
23
|
+
return unless dynamic_ivar_name?(first_arg)
|
|
24
|
+
|
|
25
|
+
add_finding(
|
|
26
|
+
node,
|
|
27
|
+
message: "Dynamic instance_variable_set causes object shape transitions, hurting YJIT. Use a Hash for dynamic data instead.",
|
|
28
|
+
speedup: "Enables better YJIT optimization"
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def dynamic_ivar_name?(node)
|
|
35
|
+
# Interpolated string like "@#{name}"
|
|
36
|
+
node.is_a?(Prism::InterpolatedStringNode) ||
|
|
37
|
+
# String concatenation
|
|
38
|
+
(node.is_a?(Prism::CallNode) && node.name == :+) ||
|
|
39
|
+
# Variable (not a literal symbol/string)
|
|
40
|
+
node.is_a?(Prism::LocalVariableReadNode)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hone
|
|
4
|
+
module Patterns
|
|
5
|
+
# Pattern: instance_variable_get("@#{name}")
|
|
6
|
+
#
|
|
7
|
+
# Often paired with dynamic instance_variable_set. Suggests the code
|
|
8
|
+
# is using ivars as a dynamic key-value store, which hurts JIT.
|
|
9
|
+
#
|
|
10
|
+
# Impact: Significant with YJIT, none without
|
|
11
|
+
class DynamicIvarGet < Base
|
|
12
|
+
self.pattern_id = :dynamic_ivar_get
|
|
13
|
+
self.optimization_type = :jit
|
|
14
|
+
|
|
15
|
+
def visit_call_node(node)
|
|
16
|
+
super
|
|
17
|
+
|
|
18
|
+
return unless node.name == :instance_variable_get
|
|
19
|
+
|
|
20
|
+
first_arg = node.arguments&.arguments&.first
|
|
21
|
+
return unless first_arg
|
|
22
|
+
return unless first_arg.is_a?(Prism::InterpolatedStringNode) ||
|
|
23
|
+
first_arg.is_a?(Prism::LocalVariableReadNode)
|
|
24
|
+
|
|
25
|
+
add_finding(
|
|
26
|
+
node,
|
|
27
|
+
message: "Dynamic instance_variable_get suggests ivars used as key-value store. Use a Hash instead for better YJIT performance.",
|
|
28
|
+
speedup: "Significant with YJIT, none without"
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
module Hone
|
|
6
|
+
module Patterns
|
|
7
|
+
# Pattern: array.each_with_index { |item, i| ... } when index or element unused
|
|
8
|
+
#
|
|
9
|
+
# When using each_with_index but only using the element (not the index),
|
|
10
|
+
# use plain .each instead to avoid index tracking overhead.
|
|
11
|
+
#
|
|
12
|
+
# When using each_with_index but only using the index (not the element),
|
|
13
|
+
# use array.size.times { |i| ... } instead for clarity and efficiency.
|
|
14
|
+
#
|
|
15
|
+
# Examples:
|
|
16
|
+
# # Bad: index `i` is never used
|
|
17
|
+
# array.each_with_index { |item, i| puts item }
|
|
18
|
+
# # Good: use plain each
|
|
19
|
+
# array.each { |item| puts item }
|
|
20
|
+
#
|
|
21
|
+
# # Bad: element `item` is never used
|
|
22
|
+
# array.each_with_index { |item, i| puts i }
|
|
23
|
+
# # Good: use times
|
|
24
|
+
# array.size.times { |i| puts i }
|
|
25
|
+
class EachWithIndex < Base
|
|
26
|
+
self.pattern_id = :each_with_index
|
|
27
|
+
self.optimization_type = :cpu
|
|
28
|
+
|
|
29
|
+
def visit_call_node(node)
|
|
30
|
+
super
|
|
31
|
+
return unless node.name == :each_with_index && node.block.is_a?(Prism::BlockNode)
|
|
32
|
+
|
|
33
|
+
check_block_parameter_usage(node)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def check_block_parameter_usage(node)
|
|
39
|
+
block = node.block
|
|
40
|
+
parameters = block.parameters
|
|
41
|
+
|
|
42
|
+
# If no parameters or not the expected structure, skip
|
|
43
|
+
return unless parameters.is_a?(Prism::BlockParametersNode)
|
|
44
|
+
return unless parameters.parameters.is_a?(Prism::ParametersNode)
|
|
45
|
+
|
|
46
|
+
params = parameters.parameters
|
|
47
|
+
requireds = params.requireds
|
|
48
|
+
|
|
49
|
+
# We expect two required parameters for each_with_index: |element, index|
|
|
50
|
+
return unless requireds.size == 2
|
|
51
|
+
|
|
52
|
+
element_param = requireds[0]
|
|
53
|
+
index_param = requireds[1]
|
|
54
|
+
|
|
55
|
+
# Get parameter names
|
|
56
|
+
element_name = extract_param_name(element_param)
|
|
57
|
+
index_name = extract_param_name(index_param)
|
|
58
|
+
|
|
59
|
+
return unless element_name && index_name
|
|
60
|
+
|
|
61
|
+
# Collect all local variable reads in the block body
|
|
62
|
+
used_locals = collect_local_variable_reads(block.body)
|
|
63
|
+
|
|
64
|
+
element_used = used_locals.include?(element_name)
|
|
65
|
+
index_used = used_locals.include?(index_name)
|
|
66
|
+
|
|
67
|
+
if !index_used && element_used
|
|
68
|
+
add_finding(
|
|
69
|
+
node,
|
|
70
|
+
message: "Index parameter `#{index_name}` is not used. Use `.each` instead of `.each_with_index` to avoid index tracking overhead.",
|
|
71
|
+
speedup: "Minor, avoids index tracking overhead"
|
|
72
|
+
)
|
|
73
|
+
elsif !element_used && index_used
|
|
74
|
+
add_finding(
|
|
75
|
+
node,
|
|
76
|
+
message: "Element parameter `#{element_name}` is not used. Consider using `.size.times { |#{index_name}| }` instead of `.each_with_index` for clarity.",
|
|
77
|
+
speedup: "Minor, avoids index tracking overhead"
|
|
78
|
+
)
|
|
79
|
+
elsif !element_used && !index_used
|
|
80
|
+
add_finding(
|
|
81
|
+
node,
|
|
82
|
+
message: "Neither element nor index parameters are used. Consider using `.size.times` or `.each` depending on intent.",
|
|
83
|
+
speedup: "Minor, avoids index tracking overhead"
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def extract_param_name(param)
|
|
89
|
+
case param
|
|
90
|
+
when Prism::RequiredParameterNode
|
|
91
|
+
param.name
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def collect_local_variable_reads(node)
|
|
96
|
+
collector = LocalVariableCollector.new
|
|
97
|
+
node&.accept(collector)
|
|
98
|
+
collector.used_names
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Helper visitor to collect all local variable read references
|
|
102
|
+
class LocalVariableCollector < Prism::Visitor
|
|
103
|
+
attr_reader :used_names
|
|
104
|
+
|
|
105
|
+
def initialize
|
|
106
|
+
@used_names = Set.new
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def visit_local_variable_read_node(node)
|
|
110
|
+
@used_names << node.name
|
|
111
|
+
super
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hone
|
|
4
|
+
module Patterns
|
|
5
|
+
# Pattern: inject({}) { |acc, x| ... ; acc } -> each_with_object({}) { |x, acc| }
|
|
6
|
+
#
|
|
7
|
+
# inject/reduce with a hash or array accumulator requires returning the
|
|
8
|
+
# accumulator at the end of each block iteration. each_with_object
|
|
9
|
+
# automatically passes the same object, making the code cleaner and
|
|
10
|
+
# avoiding the need to return the accumulator.
|
|
11
|
+
#
|
|
12
|
+
# Examples:
|
|
13
|
+
# # Bad: must return acc at end of block
|
|
14
|
+
# items.inject({}) { |acc, x| acc[x.id] = x; acc }
|
|
15
|
+
# # Good: acc is automatically passed
|
|
16
|
+
# items.each_with_object({}) { |x, acc| acc[x.id] = x }
|
|
17
|
+
class EachWithObject < Base
|
|
18
|
+
self.pattern_id = :each_with_object
|
|
19
|
+
self.optimization_type = :allocation
|
|
20
|
+
|
|
21
|
+
def visit_call_node(node)
|
|
22
|
+
super
|
|
23
|
+
|
|
24
|
+
return unless %i[inject reduce].include?(node.name)
|
|
25
|
+
return unless block_attached?(node)
|
|
26
|
+
return unless empty_collection_initial_value?(node)
|
|
27
|
+
|
|
28
|
+
add_finding(
|
|
29
|
+
node,
|
|
30
|
+
message: "Consider using `.each_with_object(#{initial_value_literal(node)})` instead of `.#{node.name}(#{initial_value_literal(node)})` for cleaner accumulator pattern",
|
|
31
|
+
speedup: "Cleaner, avoids returning accumulator each iteration"
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def empty_collection_initial_value?(node)
|
|
38
|
+
args = node.arguments&.arguments
|
|
39
|
+
return false unless args&.size == 1
|
|
40
|
+
|
|
41
|
+
arg = args.first
|
|
42
|
+
empty_hash?(arg) || empty_array?(arg)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def empty_hash?(arg)
|
|
46
|
+
arg.is_a?(Prism::HashNode) && arg.elements.empty?
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def empty_array?(arg)
|
|
50
|
+
arg.is_a?(Prism::ArrayNode) && arg.elements.empty?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def initial_value_literal(node)
|
|
54
|
+
arg = node.arguments.arguments.first
|
|
55
|
+
if arg.is_a?(Prism::HashNode)
|
|
56
|
+
"{}"
|
|
57
|
+
else
|
|
58
|
+
"[]"
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hone
|
|
4
|
+
module Patterns
|
|
5
|
+
# Pattern: array.flatten -> consider array.flatten(1)
|
|
6
|
+
#
|
|
7
|
+
# flatten without an argument flattens all levels recursively.
|
|
8
|
+
# If you only need one level, flatten(1) is faster.
|
|
9
|
+
class FlattenOnce < Base
|
|
10
|
+
self.pattern_id = :flatten_once
|
|
11
|
+
self.optimization_type = :cpu
|
|
12
|
+
|
|
13
|
+
def visit_call_node(node)
|
|
14
|
+
super
|
|
15
|
+
|
|
16
|
+
# Look for: .flatten without arguments
|
|
17
|
+
return unless node.name == :flatten
|
|
18
|
+
return unless node.arguments.nil?
|
|
19
|
+
|
|
20
|
+
add_finding(
|
|
21
|
+
node,
|
|
22
|
+
message: "Consider `.flatten(1)` if only one level of flattening is needed",
|
|
23
|
+
speedup: "Faster when you only need one level"
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hone
|
|
4
|
+
module Patterns
|
|
5
|
+
# Pattern: str.gsub('a', 'b') -> str.tr('a', 'b')
|
|
6
|
+
#
|
|
7
|
+
# For replacing single characters, tr is optimized and faster than gsub.
|
|
8
|
+
# gsub has regex overhead even for simple string patterns.
|
|
9
|
+
class GsubToTr < Base
|
|
10
|
+
self.pattern_id = :gsub_to_tr
|
|
11
|
+
self.optimization_type = :cpu
|
|
12
|
+
|
|
13
|
+
def visit_call_node(node)
|
|
14
|
+
super
|
|
15
|
+
|
|
16
|
+
return unless node.name == :gsub
|
|
17
|
+
|
|
18
|
+
args = node.arguments&.arguments
|
|
19
|
+
return unless args&.size == 2
|
|
20
|
+
|
|
21
|
+
first_arg = args[0]
|
|
22
|
+
second_arg = args[1]
|
|
23
|
+
|
|
24
|
+
# Check if both arguments are single-character strings
|
|
25
|
+
return unless single_char_string?(first_arg) && single_char_string?(second_arg)
|
|
26
|
+
|
|
27
|
+
add_finding(
|
|
28
|
+
node,
|
|
29
|
+
message: "Use `.tr('#{string_value(first_arg)}', '#{string_value(second_arg)}')` instead of `.gsub` for single character replacement",
|
|
30
|
+
speedup: "tr is optimized for character translation, avoids regex overhead"
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def single_char_string?(node)
|
|
37
|
+
return false unless node.is_a?(Prism::StringNode)
|
|
38
|
+
|
|
39
|
+
content = node.content
|
|
40
|
+
content.is_a?(String) && content.length == 1
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def string_value(node)
|
|
44
|
+
node.content
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hone
|
|
4
|
+
module Patterns
|
|
5
|
+
# Pattern: hash.keys.each { } -> hash.each_key { }
|
|
6
|
+
#
|
|
7
|
+
# keys.each creates an intermediate array of keys then iterates.
|
|
8
|
+
# each_key iterates keys directly without allocation.
|
|
9
|
+
#
|
|
10
|
+
# Also detects hash.values.each { } -> hash.each_value { }
|
|
11
|
+
class HashEachKey < Base
|
|
12
|
+
self.pattern_id = :hash_each_key
|
|
13
|
+
self.optimization_type = :allocation
|
|
14
|
+
|
|
15
|
+
def visit_call_node(node)
|
|
16
|
+
super
|
|
17
|
+
|
|
18
|
+
# Look for: .each { } where receiver is .keys or .values
|
|
19
|
+
return unless node.name == :each && block_attached?(node)
|
|
20
|
+
|
|
21
|
+
receiver = node.receiver
|
|
22
|
+
return unless receiver.is_a?(Prism::CallNode)
|
|
23
|
+
|
|
24
|
+
case receiver.name
|
|
25
|
+
when :keys
|
|
26
|
+
add_finding(
|
|
27
|
+
node,
|
|
28
|
+
message: "Use `.each_key { }` instead of `.keys.each { }` to avoid intermediate array",
|
|
29
|
+
speedup: "Iterates keys directly without allocating array"
|
|
30
|
+
)
|
|
31
|
+
when :values
|
|
32
|
+
add_finding(
|
|
33
|
+
node,
|
|
34
|
+
message: "Use `.each_value { }` instead of `.values.each { }` to avoid intermediate array",
|
|
35
|
+
speedup: "Iterates values directly without allocating array"
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hone
|
|
4
|
+
module Patterns
|
|
5
|
+
# Pattern: hash.values.each { } -> hash.each_value { }
|
|
6
|
+
#
|
|
7
|
+
# values.each creates an intermediate array of values then iterates.
|
|
8
|
+
# each_value iterates values directly without allocation.
|
|
9
|
+
class HashEachValue < Base
|
|
10
|
+
self.pattern_id = :hash_each_value
|
|
11
|
+
self.optimization_type = :allocation
|
|
12
|
+
|
|
13
|
+
def visit_call_node(node)
|
|
14
|
+
super
|
|
15
|
+
|
|
16
|
+
# Look for: .each { } where receiver is .values
|
|
17
|
+
return unless node.name == :each && block_attached?(node)
|
|
18
|
+
|
|
19
|
+
receiver = node.receiver
|
|
20
|
+
return unless receiver.is_a?(Prism::CallNode)
|
|
21
|
+
return unless receiver.name == :values
|
|
22
|
+
|
|
23
|
+
add_finding(
|
|
24
|
+
node,
|
|
25
|
+
message: "Use `.each_value { }` instead of `.values.each { }` to avoid intermediate array",
|
|
26
|
+
speedup: "Avoids creating intermediate array of values"
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hone
|
|
4
|
+
module Patterns
|
|
5
|
+
# Pattern: hash.keys.include?(key) -> hash.key?(key)
|
|
6
|
+
#
|
|
7
|
+
# keys.include? creates an array of all keys then searches it (O(n)).
|
|
8
|
+
# key? does a direct hash lookup (O(1)).
|
|
9
|
+
class HashKeysInclude < Base
|
|
10
|
+
self.pattern_id = :hash_keys_include
|
|
11
|
+
self.optimization_type = :allocation
|
|
12
|
+
|
|
13
|
+
def visit_call_node(node)
|
|
14
|
+
super
|
|
15
|
+
|
|
16
|
+
# Look for: .include?(x) where receiver is .keys
|
|
17
|
+
return unless node.name == :include? && node.arguments&.arguments&.size == 1
|
|
18
|
+
|
|
19
|
+
receiver = node.receiver
|
|
20
|
+
return unless receiver.is_a?(Prism::CallNode) && receiver.name == :keys
|
|
21
|
+
|
|
22
|
+
add_finding(
|
|
23
|
+
node,
|
|
24
|
+
message: "Use `.key?(k)` instead of `.keys.include?(k)` for O(1) lookup without array allocation",
|
|
25
|
+
speedup: "O(n) to O(1), avoids allocating array of all keys"
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hone
|
|
4
|
+
module Patterns
|
|
5
|
+
# Pattern: hash = hash.merge(other) -> hash.merge!(other)
|
|
6
|
+
#
|
|
7
|
+
# Reassigning to the same variable after merge creates a new hash.
|
|
8
|
+
# merge! mutates in place, avoiding allocation.
|
|
9
|
+
class HashMergeBang < Base
|
|
10
|
+
self.pattern_id = :hash_merge_bang
|
|
11
|
+
self.optimization_type = :allocation
|
|
12
|
+
|
|
13
|
+
def visit_local_variable_write_node(node)
|
|
14
|
+
super
|
|
15
|
+
|
|
16
|
+
# Look for: x = x.merge(...)
|
|
17
|
+
value = node.value
|
|
18
|
+
return unless value.is_a?(Prism::CallNode)
|
|
19
|
+
return unless value.name == :merge
|
|
20
|
+
|
|
21
|
+
receiver = value.receiver
|
|
22
|
+
return unless receiver.is_a?(Prism::LocalVariableReadNode)
|
|
23
|
+
return unless receiver.name == node.name
|
|
24
|
+
|
|
25
|
+
add_finding(
|
|
26
|
+
node,
|
|
27
|
+
message: "Use `.merge!` instead of `#{node.name} = #{node.name}.merge(...)` to avoid creating new hash",
|
|
28
|
+
speedup: "Avoids creating new hash, mutates in place"
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hone
|
|
4
|
+
module Patterns
|
|
5
|
+
# Pattern: hash.values.include?(x) -> hash.value?(x)
|
|
6
|
+
#
|
|
7
|
+
# values.include? creates an intermediate array of values then searches.
|
|
8
|
+
# value? checks directly without allocation.
|
|
9
|
+
class HashValuesInclude < Base
|
|
10
|
+
self.pattern_id = :hash_values_include
|
|
11
|
+
self.optimization_type = :allocation
|
|
12
|
+
|
|
13
|
+
def visit_call_node(node)
|
|
14
|
+
super
|
|
15
|
+
|
|
16
|
+
# Look for: .include?(arg) where receiver is .values
|
|
17
|
+
return unless node.name == :include? && node.arguments&.arguments&.size == 1
|
|
18
|
+
|
|
19
|
+
receiver = node.receiver
|
|
20
|
+
return unless receiver.is_a?(Prism::CallNode)
|
|
21
|
+
return unless receiver.name == :values
|
|
22
|
+
|
|
23
|
+
add_finding(
|
|
24
|
+
node,
|
|
25
|
+
message: "Use `.value?(x)` instead of `.values.include?(x)` to avoid intermediate array",
|
|
26
|
+
speedup: "Avoids creating intermediate array of values"
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hone
|
|
4
|
+
module Patterns
|
|
5
|
+
# Pattern: array.inject(0, :+) -> array.sum
|
|
6
|
+
#
|
|
7
|
+
# sum is optimized for numeric summation with native implementation.
|
|
8
|
+
# Also handles empty arrays correctly (returns 0).
|
|
9
|
+
class InjectSum < Base
|
|
10
|
+
self.pattern_id = :inject_sum
|
|
11
|
+
self.optimization_type = :cpu
|
|
12
|
+
|
|
13
|
+
def visit_call_node(node)
|
|
14
|
+
super
|
|
15
|
+
|
|
16
|
+
return unless %i[inject reduce].include?(node.name)
|
|
17
|
+
return unless sum_pattern?(node)
|
|
18
|
+
|
|
19
|
+
add_finding(
|
|
20
|
+
node,
|
|
21
|
+
message: "Use `.sum` instead of `.#{node.name}(:+)` for optimized numeric summation",
|
|
22
|
+
speedup: "Native C implementation, cleaner code"
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def sum_pattern?(node)
|
|
29
|
+
args = node.arguments&.arguments
|
|
30
|
+
return false unless args
|
|
31
|
+
|
|
32
|
+
# Pattern 1: inject(:+) or reduce(:+)
|
|
33
|
+
if args.size == 1
|
|
34
|
+
arg = args.first
|
|
35
|
+
return true if arg.is_a?(Prism::SymbolNode) && arg.value == "+"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Pattern 2: inject(0, :+) or reduce(0, :+)
|
|
39
|
+
if args.size == 2
|
|
40
|
+
second_arg = args[1]
|
|
41
|
+
return true if second_arg.is_a?(Prism::SymbolNode) && second_arg.value == "+"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
false
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hone
|
|
4
|
+
module Patterns
|
|
5
|
+
# Pattern: Kernel#loop { break if ... } -> while true ... end
|
|
6
|
+
#
|
|
7
|
+
# Kernel#loop has method call overhead vs the while construct.
|
|
8
|
+
#
|
|
9
|
+
# From sqids-ruby commit 8a74142
|
|
10
|
+
class KernelLoop < Base
|
|
11
|
+
self.pattern_id = :kernel_loop
|
|
12
|
+
self.optimization_type = :cpu
|
|
13
|
+
|
|
14
|
+
def visit_call_node(node)
|
|
15
|
+
super
|
|
16
|
+
# Look for: loop { ... } (implicit receiver, block present)
|
|
17
|
+
return unless node.name == :loop && node.receiver.nil? && node.block.is_a?(Prism::BlockNode)
|
|
18
|
+
|
|
19
|
+
add_finding(
|
|
20
|
+
node,
|
|
21
|
+
message: "Use `while true ... end` instead of `loop { }` 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,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hone
|
|
4
|
+
module Patterns
|
|
5
|
+
# Pattern: @ivar ||= value (outside initialize)
|
|
6
|
+
#
|
|
7
|
+
# When an ivar is first assigned outside initialize, it changes the
|
|
8
|
+
# object's shape at runtime. YJIT prefers stable shapes from creation.
|
|
9
|
+
#
|
|
10
|
+
# This pattern detects ||= with ivars outside of initialize methods.
|
|
11
|
+
#
|
|
12
|
+
# Impact: Moderate with YJIT, none without
|
|
13
|
+
class LazyIvar < Base
|
|
14
|
+
self.pattern_id = :lazy_ivar
|
|
15
|
+
self.optimization_type = :jit
|
|
16
|
+
|
|
17
|
+
def initialize(file_path)
|
|
18
|
+
super
|
|
19
|
+
@in_initialize = false
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def visit_def_node(node)
|
|
23
|
+
with_context(:@in_initialize, node.name == :initialize) { super }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def visit_instance_variable_or_write_node(node)
|
|
27
|
+
super
|
|
28
|
+
# @ivar ||= value pattern
|
|
29
|
+
return if @in_initialize
|
|
30
|
+
|
|
31
|
+
add_finding(
|
|
32
|
+
node,
|
|
33
|
+
message: "Lazy ivar initialization (||=) outside initialize causes shape transitions. Define #{node.name} = nil in initialize for better YJIT.",
|
|
34
|
+
speedup: "Moderate with YJIT, none without"
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hone
|
|
4
|
+
module Patterns
|
|
5
|
+
# Pattern: array.map { }.compact -> array.filter_map { }
|
|
6
|
+
#
|
|
7
|
+
# map.compact creates an intermediate array from map that may contain nils,
|
|
8
|
+
# then compacts it. filter_map combines both operations avoiding the
|
|
9
|
+
# intermediate allocation.
|
|
10
|
+
class MapCompact < Base
|
|
11
|
+
self.pattern_id = :map_compact
|
|
12
|
+
self.optimization_type = :allocation
|
|
13
|
+
|
|
14
|
+
def visit_call_node(node)
|
|
15
|
+
super
|
|
16
|
+
|
|
17
|
+
# Look for: .compact where receiver is .map with a block
|
|
18
|
+
return unless node.name == :compact
|
|
19
|
+
|
|
20
|
+
receiver = node.receiver
|
|
21
|
+
return unless receiver.is_a?(Prism::CallNode)
|
|
22
|
+
return unless receiver.name == :map && block_attached?(receiver)
|
|
23
|
+
|
|
24
|
+
add_finding(
|
|
25
|
+
node,
|
|
26
|
+
message: "Use `.filter_map { }` instead of `.map { }.compact` to avoid intermediate array with nils",
|
|
27
|
+
speedup: "Avoids intermediate array with nils"
|
|
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.map { }.flatten -> array.flat_map { }
|
|
6
|
+
#
|
|
7
|
+
# map.flatten creates an intermediate array from map, then flattens it.
|
|
8
|
+
# flat_map combines both operations avoiding the intermediate allocation.
|
|
9
|
+
class MapFlatten < Base
|
|
10
|
+
self.pattern_id = :map_flatten
|
|
11
|
+
self.optimization_type = :allocation
|
|
12
|
+
|
|
13
|
+
def visit_call_node(node)
|
|
14
|
+
super
|
|
15
|
+
|
|
16
|
+
# Look for: .flatten where receiver is .map with a block
|
|
17
|
+
return unless node.name == :flatten
|
|
18
|
+
|
|
19
|
+
receiver = node.receiver
|
|
20
|
+
return unless receiver.is_a?(Prism::CallNode)
|
|
21
|
+
return unless receiver.name == :map && block_attached?(receiver)
|
|
22
|
+
|
|
23
|
+
add_finding(
|
|
24
|
+
node,
|
|
25
|
+
message: "Use `.flat_map { }` instead of `.map { }.flatten` to avoid intermediate array",
|
|
26
|
+
speedup: "Single pass, no intermediate allocation"
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|