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.
Files changed (105) hide show
  1. checksums.yaml +7 -0
  2. data/.standard.yml +8 -0
  3. data/CHANGELOG.md +5 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +201 -0
  6. data/Rakefile +10 -0
  7. data/examples/.hone/harness.rb +41 -0
  8. data/examples/README.md +22 -0
  9. data/examples/allocation_patterns.rb +66 -0
  10. data/examples/cpu_patterns.rb +50 -0
  11. data/examples/jit_patterns.rb +69 -0
  12. data/exe/hone +7 -0
  13. data/lib/hone/adapters/base.rb +35 -0
  14. data/lib/hone/adapters/fasterer.rb +38 -0
  15. data/lib/hone/adapters/rubocop_performance.rb +85 -0
  16. data/lib/hone/analyzer.rb +258 -0
  17. data/lib/hone/cli.rb +247 -0
  18. data/lib/hone/config.rb +93 -0
  19. data/lib/hone/correlator.rb +250 -0
  20. data/lib/hone/exit_codes.rb +10 -0
  21. data/lib/hone/finding.rb +64 -0
  22. data/lib/hone/finding_filter.rb +57 -0
  23. data/lib/hone/formatters/base.rb +25 -0
  24. data/lib/hone/formatters/filterable.rb +31 -0
  25. data/lib/hone/formatters/github.rb +71 -0
  26. data/lib/hone/formatters/json.rb +75 -0
  27. data/lib/hone/formatters/junit.rb +154 -0
  28. data/lib/hone/formatters/sarif.rb +179 -0
  29. data/lib/hone/formatters/tsv.rb +49 -0
  30. data/lib/hone/harness.rb +57 -0
  31. data/lib/hone/harness_generator.rb +128 -0
  32. data/lib/hone/harness_runner.rb +172 -0
  33. data/lib/hone/method_map.rb +140 -0
  34. data/lib/hone/patterns/README.md +174 -0
  35. data/lib/hone/patterns/array_compact.rb +105 -0
  36. data/lib/hone/patterns/array_include_set.rb +34 -0
  37. data/lib/hone/patterns/base.rb +90 -0
  38. data/lib/hone/patterns/block_to_proc.rb +109 -0
  39. data/lib/hone/patterns/bsearch_vs_find.rb +80 -0
  40. data/lib/hone/patterns/chars_map_ord.rb +42 -0
  41. data/lib/hone/patterns/chars_to_variable.rb +136 -0
  42. data/lib/hone/patterns/chars_to_variable_tainted.rb +136 -0
  43. data/lib/hone/patterns/constant_regexp.rb +74 -0
  44. data/lib/hone/patterns/count_vs_size.rb +35 -0
  45. data/lib/hone/patterns/divmod.rb +92 -0
  46. data/lib/hone/patterns/dynamic_ivar.rb +44 -0
  47. data/lib/hone/patterns/dynamic_ivar_get.rb +33 -0
  48. data/lib/hone/patterns/each_with_index.rb +116 -0
  49. data/lib/hone/patterns/each_with_object.rb +63 -0
  50. data/lib/hone/patterns/flatten_once.rb +28 -0
  51. data/lib/hone/patterns/gsub_to_tr.rb +48 -0
  52. data/lib/hone/patterns/hash_each_key.rb +41 -0
  53. data/lib/hone/patterns/hash_each_value.rb +31 -0
  54. data/lib/hone/patterns/hash_keys_include.rb +30 -0
  55. data/lib/hone/patterns/hash_merge_bang.rb +33 -0
  56. data/lib/hone/patterns/hash_values_include.rb +31 -0
  57. data/lib/hone/patterns/inject_sum.rb +48 -0
  58. data/lib/hone/patterns/kernel_loop.rb +27 -0
  59. data/lib/hone/patterns/lazy_ivar.rb +39 -0
  60. data/lib/hone/patterns/map_compact.rb +32 -0
  61. data/lib/hone/patterns/map_flatten.rb +31 -0
  62. data/lib/hone/patterns/map_select_chain.rb +32 -0
  63. data/lib/hone/patterns/parallel_assignment.rb +127 -0
  64. data/lib/hone/patterns/positive_predicate.rb +27 -0
  65. data/lib/hone/patterns/range_include.rb +34 -0
  66. data/lib/hone/patterns/redundant_string_chars.rb +82 -0
  67. data/lib/hone/patterns/regexp_match.rb +126 -0
  68. data/lib/hone/patterns/reverse_each.rb +30 -0
  69. data/lib/hone/patterns/reverse_first.rb +40 -0
  70. data/lib/hone/patterns/select_count.rb +32 -0
  71. data/lib/hone/patterns/select_first.rb +31 -0
  72. data/lib/hone/patterns/select_map.rb +32 -0
  73. data/lib/hone/patterns/shuffle_first.rb +30 -0
  74. data/lib/hone/patterns/slice_with_length.rb +48 -0
  75. data/lib/hone/patterns/sort_by_first.rb +31 -0
  76. data/lib/hone/patterns/sort_by_last.rb +31 -0
  77. data/lib/hone/patterns/sort_first.rb +52 -0
  78. data/lib/hone/patterns/sort_last.rb +30 -0
  79. data/lib/hone/patterns/sort_reverse.rb +53 -0
  80. data/lib/hone/patterns/string_casecmp.rb +54 -0
  81. data/lib/hone/patterns/string_chars_each.rb +56 -0
  82. data/lib/hone/patterns/string_concat_in_loop.rb +116 -0
  83. data/lib/hone/patterns/string_delete_prefix.rb +53 -0
  84. data/lib/hone/patterns/string_delete_suffix.rb +53 -0
  85. data/lib/hone/patterns/string_empty.rb +64 -0
  86. data/lib/hone/patterns/string_end_with.rb +81 -0
  87. data/lib/hone/patterns/string_shovel.rb +75 -0
  88. data/lib/hone/patterns/string_start_with.rb +80 -0
  89. data/lib/hone/patterns/taint_tracking_base.rb +230 -0
  90. data/lib/hone/patterns/times_map.rb +38 -0
  91. data/lib/hone/patterns/uniq_by.rb +32 -0
  92. data/lib/hone/patterns/yield_vs_block.rb +72 -0
  93. data/lib/hone/profilers/base.rb +162 -0
  94. data/lib/hone/profilers/factory.rb +31 -0
  95. data/lib/hone/profilers/memory_profiler.rb +213 -0
  96. data/lib/hone/profilers/stackprof.rb +99 -0
  97. data/lib/hone/profilers/vernier.rb +147 -0
  98. data/lib/hone/reporter.rb +371 -0
  99. data/lib/hone/scanner.rb +75 -0
  100. data/lib/hone/suggestion_generator.rb +23 -0
  101. data/lib/hone/version.rb +5 -0
  102. data/lib/hone.rb +108 -0
  103. data/logo.png +0 -0
  104. data/sig/hone.rbs +4 -0
  105. metadata +176 -0
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hone
4
+ module Patterns
5
+ # Pattern: str = str + other -> str << other
6
+ #
7
+ # When reassigning a local variable to itself plus another string,
8
+ # using the shovel operator (<<) avoids creating a new string object.
9
+ # The + operator always creates a new string, while << mutates in place.
10
+ #
11
+ # Example:
12
+ # # Before - creates new string
13
+ # str = str + other
14
+ # str = str + "suffix"
15
+ #
16
+ # # After - mutates in place
17
+ # str << other
18
+ # str << "suffix"
19
+ #
20
+ # Note: Only safe when the original string can be mutated (not frozen,
21
+ # not shared with other references that expect the original value).
22
+ #
23
+ # Impact: Avoids allocating a new string object
24
+ class StringShovel < Base
25
+ self.pattern_id = :string_shovel
26
+ self.optimization_type = :allocation
27
+
28
+ def visit_local_variable_write_node(node)
29
+ super
30
+
31
+ check_string_reassignment(node, node.name)
32
+ end
33
+
34
+ private
35
+
36
+ def check_string_reassignment(node, var_name)
37
+ value = node.value
38
+ return unless value.is_a?(Prism::CallNode)
39
+ return unless value.name == :+
40
+
41
+ receiver = value.receiver
42
+ return unless matches_variable?(receiver, var_name)
43
+
44
+ args = value.arguments&.arguments
45
+ return unless args&.size == 1
46
+ return unless likely_string?(args.first)
47
+
48
+ add_finding(
49
+ node,
50
+ message: "Use `#{var_name} << ...` instead of `#{var_name} = #{var_name} + ...` to avoid creating a new string",
51
+ speedup: "Avoids creating new string"
52
+ )
53
+ end
54
+
55
+ def matches_variable?(node, var_name)
56
+ node.is_a?(Prism::LocalVariableReadNode) && node.name == var_name
57
+ end
58
+
59
+ def likely_string?(node)
60
+ case node
61
+ when Prism::StringNode, Prism::InterpolatedStringNode
62
+ true
63
+ when Prism::CallNode
64
+ %i[to_s to_str inspect].include?(node.name)
65
+ when Prism::LocalVariableReadNode
66
+ # Could be a string, we'll suggest the optimization
67
+ # User can decide if it's appropriate
68
+ true
69
+ else
70
+ false
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hone
4
+ module Patterns
5
+ # Pattern: str[0] == 'x' or str.match?(/^x/) -> str.start_with?('x')
6
+ #
7
+ # Indexing at position 0 for comparison or using regex with ^ anchor
8
+ # is less clear and potentially slower than using start_with?.
9
+ #
10
+ # @example Bad
11
+ # str[0] == 'x'
12
+ # str.match?(/^foo/)
13
+ #
14
+ # @example Good
15
+ # str.start_with?('x')
16
+ # str.start_with?('foo')
17
+ class StringStartWith < Base
18
+ self.pattern_id = :string_start_with
19
+ self.optimization_type = :cpu
20
+
21
+ def visit_call_node(node)
22
+ super
23
+
24
+ if index_zero_comparison?(node)
25
+ add_finding(
26
+ node,
27
+ message: "Use `start_with?` instead of `str[0] == ...` for cleaner code",
28
+ speedup: "Cleaner and avoids substring/regex overhead"
29
+ )
30
+ elsif regex_start_anchor?(node)
31
+ add_finding(
32
+ node,
33
+ message: "Use `start_with?` instead of `match?(/^.../)` to avoid regex overhead",
34
+ speedup: "Cleaner and avoids substring/regex overhead"
35
+ )
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ # Detects: str[0] == 'x' or 'x' == str[0]
42
+ def index_zero_comparison?(node)
43
+ return false unless node.name == :==
44
+ return false unless node.arguments&.arguments&.size == 1
45
+
46
+ receiver = node.receiver
47
+ arg = node.arguments.arguments[0]
48
+
49
+ # Check receiver[0] == arg or arg == receiver[0]
50
+ index_at_zero?(receiver) || index_at_zero?(arg)
51
+ end
52
+
53
+ # Detects: str.match?(/^.../)
54
+ def regex_start_anchor?(node)
55
+ return false unless node.name == :match?
56
+ return false unless node.arguments&.arguments&.size == 1
57
+
58
+ arg = node.arguments.arguments[0]
59
+ return false unless arg.is_a?(Prism::RegularExpressionNode)
60
+
61
+ # Check if regex starts with ^ anchor
62
+ arg.content.start_with?("^")
63
+ end
64
+
65
+ # Check if node is a [] call with index 0
66
+ def index_at_zero?(node)
67
+ return false unless node.is_a?(Prism::CallNode)
68
+ return false unless node.name == :[]
69
+ return false unless node.arguments&.arguments&.size == 1
70
+
71
+ index_arg = node.arguments.arguments[0]
72
+ integer_zero?(index_arg)
73
+ end
74
+
75
+ def integer_zero?(node)
76
+ node.is_a?(Prism::IntegerNode) && node.value == 0
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,230 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hone
4
+ module Patterns
5
+ # Base class for patterns that need to track data flow through variables.
6
+ #
7
+ # Provides "taint tracking" - the ability to mark variables with metadata
8
+ # about their origin and propagate that information through assignments.
9
+ #
10
+ # Subclasses should:
11
+ # 1. Override `taint_from_call` to mark variables when assigned from specific calls
12
+ # 2. Override `check_tainted_usage` to detect problematic uses of tainted variables
13
+ #
14
+ # @example Tracking .chars allocations
15
+ # class CharsTracking < TaintTrackingBase
16
+ # def taint_from_call(call_node)
17
+ # return nil unless call_node.name == :chars
18
+ # { type: :chars, source: call_node.receiver&.slice }
19
+ # end
20
+ #
21
+ # def check_tainted_usage(call_node, taint_info)
22
+ # if call_node.name == :[] && taint_info[:type] == :chars
23
+ # report_finding(...)
24
+ # end
25
+ # end
26
+ # end
27
+ #
28
+ class TaintTrackingBase < Base
29
+ # Taint info structure
30
+ TaintInfo = Data.define(:type, :source, :source_code, :origin_line, :metadata) do
31
+ def initialize(type:, source: nil, source_code: nil, origin_line: nil, metadata: {})
32
+ super
33
+ end
34
+ end
35
+
36
+ def initialize(file_path)
37
+ super
38
+ @taint_scopes = []
39
+ end
40
+
41
+ # === Scope Management ===
42
+
43
+ def visit_def_node(node)
44
+ with_taint_scope { super }
45
+ end
46
+
47
+ def visit_block_node(node)
48
+ with_taint_scope(inherit: true) { super }
49
+ end
50
+
51
+ def visit_lambda_node(node)
52
+ with_taint_scope { super }
53
+ end
54
+
55
+ def visit_class_node(node)
56
+ with_taint_scope { super }
57
+ end
58
+
59
+ def visit_module_node(node)
60
+ with_taint_scope { super }
61
+ end
62
+
63
+ # === Variable Tracking ===
64
+
65
+ # Track local variable assignments
66
+ def visit_local_variable_write_node(node)
67
+ super
68
+ track_assignment(node.name, node.value, node)
69
+ end
70
+
71
+ # Track instance variable assignments
72
+ def visit_instance_variable_write_node(node)
73
+ super
74
+ track_assignment(node.name, node.value, node, scope: :instance)
75
+ end
76
+
77
+ # Track multiple assignment (a, b = x, y)
78
+ def visit_multi_write_node(node)
79
+ super
80
+ # For simplicity, clear taints for multi-assigned variables
81
+ node.lefts.each do |target|
82
+ case target
83
+ when Prism::LocalVariableTargetNode
84
+ clear_taint(target.name)
85
+ end
86
+ end
87
+ end
88
+
89
+ # Track call nodes for both tainting and usage checking
90
+ def visit_call_node(node)
91
+ super
92
+ check_tainted_call(node)
93
+ end
94
+
95
+ # Track variable reads (for propagation detection)
96
+ def visit_local_variable_read_node(node)
97
+ super
98
+ # Subclasses can override to track reads
99
+ end
100
+
101
+ protected
102
+
103
+ # === Override Points for Subclasses ===
104
+
105
+ # Override to define what calls create taints
106
+ # @param call_node [Prism::CallNode] The call being assigned
107
+ # @return [TaintInfo, nil] Taint info if this call should taint, nil otherwise
108
+ def taint_from_call(call_node)
109
+ nil
110
+ end
111
+
112
+ # Override to check if a tainted variable is being used problematically
113
+ # @param call_node [Prism::CallNode] The call on a tainted variable
114
+ # @param var_name [Symbol] The variable name
115
+ # @param taint_info [TaintInfo] Information about the taint
116
+ def check_tainted_usage(call_node, var_name, taint_info)
117
+ # Subclasses implement this
118
+ end
119
+
120
+ # === Taint Query Methods ===
121
+
122
+ # Get taint info for a variable
123
+ def get_taint(var_name, scope: :local)
124
+ case scope
125
+ when :local
126
+ current_taint_scope[:locals][var_name]
127
+ when :instance
128
+ current_taint_scope[:instance][var_name]
129
+ end
130
+ end
131
+
132
+ # Check if a variable is tainted
133
+ def tainted?(var_name, scope: :local)
134
+ !get_taint(var_name, scope: scope).nil?
135
+ end
136
+
137
+ # Set taint for a variable
138
+ def set_taint(var_name, taint_info, scope: :local)
139
+ case scope
140
+ when :local
141
+ current_taint_scope[:locals][var_name] = taint_info
142
+ when :instance
143
+ current_taint_scope[:instance][var_name] = taint_info
144
+ end
145
+ end
146
+
147
+ # Clear taint for a variable (e.g., on reassignment)
148
+ def clear_taint(var_name, scope: :local)
149
+ case scope
150
+ when :local
151
+ current_taint_scope[:locals].delete(var_name)
152
+ when :instance
153
+ current_taint_scope[:instance].delete(var_name)
154
+ end
155
+ end
156
+
157
+ # Get all tainted variables in current scope
158
+ def all_taints(scope: :local)
159
+ case scope
160
+ when :local
161
+ current_taint_scope[:locals].dup
162
+ when :instance
163
+ current_taint_scope[:instance].dup
164
+ end
165
+ end
166
+
167
+ private
168
+
169
+ def with_taint_scope(inherit: false)
170
+ new_scope = {
171
+ locals: {},
172
+ instance: (inherit && @taint_scopes.any?) ? current_taint_scope[:instance].dup : {}
173
+ }
174
+ @taint_scopes.push(new_scope)
175
+ yield
176
+ ensure
177
+ @taint_scopes.pop
178
+ end
179
+
180
+ def current_taint_scope
181
+ @taint_scopes.last || {locals: {}, instance: {}}
182
+ end
183
+
184
+ def track_assignment(var_name, value_node, assignment_node, scope: :local)
185
+ # If assigned from a call, check if it should be tainted
186
+ if value_node.is_a?(Prism::CallNode)
187
+ taint_info = taint_from_call(value_node)
188
+ if taint_info
189
+ # Ensure it's a TaintInfo
190
+ taint_info = if taint_info.is_a?(TaintInfo)
191
+ taint_info
192
+ else
193
+ TaintInfo.new(**taint_info)
194
+ end
195
+ set_taint(var_name, taint_info, scope: scope)
196
+ return
197
+ end
198
+ end
199
+
200
+ # If assigned from another variable, propagate taint
201
+ if value_node.is_a?(Prism::LocalVariableReadNode)
202
+ source_taint = get_taint(value_node.name)
203
+ if source_taint
204
+ set_taint(var_name, source_taint, scope: scope)
205
+ return
206
+ end
207
+ end
208
+
209
+ # Otherwise, clear any existing taint (variable reassigned to non-tainted value)
210
+ clear_taint(var_name, scope: scope)
211
+ end
212
+
213
+ def check_tainted_call(node)
214
+ # Check if this is a call on a tainted local variable
215
+ if node.receiver.is_a?(Prism::LocalVariableReadNode)
216
+ var_name = node.receiver.name
217
+ taint_info = get_taint(var_name)
218
+ check_tainted_usage(node, var_name, taint_info) if taint_info
219
+ end
220
+
221
+ # Check if this is a call on a tainted instance variable
222
+ if node.receiver.is_a?(Prism::InstanceVariableReadNode)
223
+ var_name = node.receiver.name
224
+ taint_info = get_taint(var_name, scope: :instance)
225
+ check_tainted_usage(node, var_name, taint_info) if taint_info
226
+ end
227
+ end
228
+ end
229
+ end
230
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hone
4
+ module Patterns
5
+ # Pattern: n.times.map { } -> Array.new(n) { }
6
+ #
7
+ # times.map creates an Enumerator then maps over it.
8
+ # Array.new(n) { } directly creates the array with the block values,
9
+ # avoiding the Enumerator overhead.
10
+ #
11
+ # Examples:
12
+ # # Bad: creates Enumerator then maps
13
+ # 5.times.map { |i| i * 2 }
14
+ # # Good: direct array creation
15
+ # Array.new(5) { |i| i * 2 }
16
+ class TimesMap < Base
17
+ self.pattern_id = :times_map
18
+ self.optimization_type = :allocation
19
+
20
+ def visit_call_node(node)
21
+ super
22
+
23
+ # Look for: .map { } where receiver is .times
24
+ return unless node.name == :map && block_attached?(node)
25
+
26
+ receiver = node.receiver
27
+ return unless receiver.is_a?(Prism::CallNode)
28
+ return unless receiver.name == :times
29
+
30
+ add_finding(
31
+ node,
32
+ message: "Use `Array.new(n) { }` instead of `n.times.map { }` to avoid Enumerator overhead",
33
+ speedup: "Avoids Enumerator overhead"
34
+ )
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hone
4
+ module Patterns
5
+ # Pattern: array.map { }.uniq -> consider array.uniq { }
6
+ #
7
+ # map { }.uniq creates an intermediate array from map, then deduplicates.
8
+ # If deduplicating on the transformed value, uniq { } avoids the intermediate.
9
+ class UniqBy < Base
10
+ self.pattern_id = :uniq_by
11
+ self.optimization_type = :allocation
12
+
13
+ def visit_call_node(node)
14
+ super
15
+
16
+ # Look for: .uniq where receiver is .map with a block
17
+ return unless node.name == :uniq
18
+ return unless node.arguments.nil? && !block_attached?(node)
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: "Consider `.uniq { }` instead of `.map { }.uniq` if deduping on transform",
27
+ speedup: "May avoid intermediate array if deduping on transform"
28
+ )
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hone
4
+ module Patterns
5
+ # Pattern: block.call -> yield when method has &block parameter
6
+ #
7
+ # When a method accepts a block with `&block`, calling `block.call` is slower
8
+ # than using `yield`. The `&block` syntax converts the block to a Proc object,
9
+ # which has overhead. Using `yield` avoids this Proc allocation when the block
10
+ # is only called (not stored or passed elsewhere).
11
+ #
12
+ # Example:
13
+ # # Before - allocates Proc
14
+ # def process(&block)
15
+ # block.call(value)
16
+ # end
17
+ #
18
+ # # After - no Proc allocation
19
+ # def process
20
+ # yield(value)
21
+ # end
22
+ #
23
+ # Impact: Avoids Proc allocation
24
+ class YieldVsBlock < Base
25
+ self.pattern_id = :yield_vs_block
26
+ self.optimization_type = :cpu
27
+
28
+ def initialize(file_path)
29
+ super
30
+ @block_param_name = nil
31
+ end
32
+
33
+ def visit_def_node(node)
34
+ block_param = find_block_parameter(node.parameters)
35
+
36
+ if block_param
37
+ with_context(:@block_param_name, block_param) { super }
38
+ else
39
+ super
40
+ end
41
+ end
42
+
43
+ def visit_call_node(node)
44
+ super
45
+
46
+ return unless @block_param_name
47
+ return unless node.name == :call
48
+
49
+ receiver = node.receiver
50
+ return unless receiver.is_a?(Prism::LocalVariableReadNode)
51
+ return unless receiver.name == @block_param_name
52
+
53
+ add_finding(
54
+ node,
55
+ message: "Use `yield` instead of `#{@block_param_name}.call` to avoid Proc allocation",
56
+ speedup: "Avoids Proc allocation"
57
+ )
58
+ end
59
+
60
+ private
61
+
62
+ def find_block_parameter(params)
63
+ return nil unless params
64
+
65
+ block = params.block
66
+ return nil unless block.is_a?(Prism::BlockParameterNode)
67
+
68
+ block.name
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Hone
6
+ module Profilers
7
+ # Shared data structure for hotspot information across all profilers
8
+ HotspotInfo = Data.define(:name, :file, :line, :cpu_percent, :samples)
9
+
10
+ # Shared method matching logic for profiler implementations.
11
+ # Provides utilities to find and match methods/frames by name and file.
12
+ module MethodMatching
13
+ private
14
+
15
+ def find_matching_frame(method_info)
16
+ name, file, line, end_line = extract_search_criteria(method_info)
17
+
18
+ # First try to match by line range (for allocation profilers)
19
+ if line && file
20
+ match = @frames.find do |frame|
21
+ next unless frame[:file] && frame[:line]
22
+ next unless file_matches?(frame[:file], file)
23
+
24
+ frame_line = frame[:line].to_i
25
+ if end_line
26
+ # Method has a range, check if frame line is within it
27
+ frame_line >= line && frame_line <= end_line
28
+ else
29
+ # Exact line match
30
+ frame_line == line
31
+ end
32
+ end
33
+ return match if match
34
+ end
35
+
36
+ # Fall back to name-based matching
37
+ @frames.find do |frame|
38
+ matches_method?(frame, name, file)
39
+ end
40
+ end
41
+
42
+ def extract_search_criteria(method_info)
43
+ case method_info
44
+ when String
45
+ [method_info, nil, nil, nil]
46
+ when Hash
47
+ extract_from_hash(method_info)
48
+ else
49
+ extract_from_object(method_info)
50
+ end
51
+ end
52
+
53
+ # Extracts name, file, line, and end_line from a Hash with symbol or string keys.
54
+ #
55
+ # @param hash [Hash] Hash containing :name/:file/:line/:end_line keys
56
+ # @return [Array] [name, file, line, end_line] tuple
57
+ #
58
+ def extract_from_hash(hash)
59
+ [
60
+ hash[:name] || hash["name"],
61
+ hash[:file] || hash["file"],
62
+ hash[:line] || hash["line"],
63
+ hash[:end_line] || hash["end_line"]
64
+ ]
65
+ end
66
+
67
+ # Extracts name, file, line, end_line from objects with method accessors (like MethodInfo).
68
+ #
69
+ # @param obj [Object] Object responding to :qualified_name/:name and optionally :file/:line/:end_line
70
+ # @return [Array] [name, file, line, end_line] tuple
71
+ #
72
+ def extract_from_object(obj)
73
+ name = if obj.respond_to?(:qualified_name)
74
+ obj.qualified_name
75
+ elsif obj.respond_to?(:name)
76
+ obj.name
77
+ else
78
+ obj.to_s
79
+ end
80
+
81
+ file = obj.respond_to?(:file) ? obj.file : nil
82
+ # Support both :line and :start_line for compatibility
83
+ line = if obj.respond_to?(:start_line)
84
+ obj.start_line
85
+ elsif obj.respond_to?(:line)
86
+ obj.line
87
+ end
88
+ end_line = obj.respond_to?(:end_line) ? obj.end_line : nil
89
+
90
+ [name, file, line, end_line]
91
+ end
92
+
93
+ def matches_method?(frame, name, file)
94
+ return false unless name
95
+
96
+ name_matches = method_name_matches?(frame[:name], name)
97
+ return name_matches unless file && name_matches
98
+
99
+ # If file is provided, also check file match
100
+ file_matches?(frame[:file], file)
101
+ end
102
+
103
+ def method_name_matches?(frame_name, search_name)
104
+ return false unless frame_name
105
+
106
+ # Exact match
107
+ return true if frame_name == search_name
108
+
109
+ # Match without class prefix (e.g., "method_name" matches "ClassName#method_name")
110
+ return true if frame_name.end_with?("##{search_name}")
111
+
112
+ # Match with class prefix when searching for just method name
113
+ frame_method = frame_name.split("#").last
114
+ frame_method == search_name
115
+ end
116
+
117
+ def file_matches?(frame_file, search_file)
118
+ return false unless frame_file && search_file
119
+
120
+ # Exact match or path suffix match
121
+ frame_file == search_file || frame_file.end_with?(search_file) || search_file.end_with?(File.basename(frame_file))
122
+ end
123
+
124
+ def build_hotspot_info(frame)
125
+ HotspotInfo.new(
126
+ name: frame[:name],
127
+ file: frame[:file],
128
+ line: frame[:line],
129
+ cpu_percent: frame[:cpu_percent],
130
+ samples: frame[:samples]
131
+ )
132
+ end
133
+ end
134
+
135
+ class Base
136
+ def initialize(profile_path)
137
+ @profile_path = profile_path
138
+ @data = load_profile(profile_path)
139
+ end
140
+
141
+ # Returns CPU percentage for a method (0.0-100.0)
142
+ def cpu_percent_for(method_info)
143
+ raise NotImplementedError
144
+ end
145
+
146
+ # Returns all hotspots above threshold
147
+ def hotspots(threshold: 1.0)
148
+ raise NotImplementedError
149
+ end
150
+
151
+ private
152
+
153
+ def load_profile(path)
154
+ raise Hone::Error, "Profile file not found: #{path}" unless File.exist?(path)
155
+
156
+ JSON.parse(File.read(path))
157
+ rescue JSON::ParserError => e
158
+ raise Hone::Error, "Invalid JSON in profile file #{path}: #{e.message}"
159
+ end
160
+ end
161
+ end
162
+ end