necropsy 0.1.0 → 0.3.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 (131) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +33 -0
  3. data/CHANGELOG.md +44 -0
  4. data/MEASUREMENTS.md +112 -0
  5. data/README.md +233 -16
  6. data/Rakefile +3 -1
  7. data/bench/README.md +92 -0
  8. data/bench/audit.rb +113 -0
  9. data/bench/audits/0.2.1/audit.json +501 -0
  10. data/bench/audits/0.2.1/audit.md +55 -0
  11. data/bench/audits/0.2.1/baseline_performance.yml +16 -0
  12. data/bench/audits/0.2.1/config.yml +42 -0
  13. data/bench/audits/0.2.1/review.yml +56 -0
  14. data/bench/audits/0.2.1/review_queue.yml +3651 -0
  15. data/bench/corpora/v1/README.md +30 -0
  16. data/bench/corpora/v1/labels.yml +37 -0
  17. data/bench/corpora/v1/manifest.yml +63 -0
  18. data/bench/corpora/v1/rubocop.necropsy.yml +7 -0
  19. data/bench/corpora/v1/self.necropsy.yml +8 -0
  20. data/bench/corpora/v1/tools/debride.yml +42 -0
  21. data/bench/corpora/v1/tools/spoom.yml +12 -0
  22. data/bench/corpora/v1/tools/type_aware.yml +11 -0
  23. data/bench/golden/v1/candidate_union.json +21918 -0
  24. data/bench/golden/v1/metadata.json +12 -0
  25. data/bench/golden/v1/reports/dynamic_evidence.json +146 -0
  26. data/bench/golden/v1/reports/plain_ruby.json +135 -0
  27. data/bench/golden/v1/reports/rails.json +176 -0
  28. data/bench/golden/v1/reports/rubocop_1_75_0.json +26848 -0
  29. data/bench/golden/v1/reports/self.json +3449 -0
  30. data/bench/review_queue.rb +35 -0
  31. data/bench/run.rb +31 -0
  32. data/bench/schema/candidate-union-v1.schema.json +70 -0
  33. data/docs/impv_implementation_matrix.md +179 -0
  34. data/docs/migrations/0.2.1.md +57 -0
  35. data/docs/migrations/0.3.0.md +207 -0
  36. data/docs/migrations/0.4.0.md +13 -0
  37. data/docs/necropsy_performance_adr.md +43 -0
  38. data/docs/necropsy_scope_decisions.md +58 -0
  39. data/docs/necropsy_type_facts_adr.md +22 -0
  40. data/gemfiles/prism_min.gemfile +9 -0
  41. data/gemfiles/prism_min.gemfile.lock +50 -0
  42. data/lib/necropsy/analyzer.rb +121 -2
  43. data/lib/necropsy/analyzers/dynamic/coverage_collector.rb +81 -19
  44. data/lib/necropsy/analyzers/dynamic/coverage_importer.rb +104 -10
  45. data/lib/necropsy/analyzers/dynamic/coverband_importer.rb +74 -250
  46. data/lib/necropsy/analyzers/dynamic/coverband_payload_set.rb +149 -0
  47. data/lib/necropsy/analyzers/dynamic/observation_policy.rb +96 -0
  48. data/lib/necropsy/analyzers/dynamic/redis_input_limits.rb +121 -0
  49. data/lib/necropsy/analyzers/dynamic/redis_nonblocking_io.rb +94 -0
  50. data/lib/necropsy/analyzers/dynamic/redis_payload_loader.rb +165 -0
  51. data/lib/necropsy/analyzers/dynamic/redis_transport.rb +217 -0
  52. data/lib/necropsy/analyzers/dynamic/runtime_reference.rb +96 -0
  53. data/lib/necropsy/analyzers/dynamic/trace_point_collector.rb +225 -20
  54. data/lib/necropsy/analyzers/dynamic/trace_point_importer.rb +3 -1
  55. data/lib/necropsy/analyzers/legacy_result_adapter.rb +226 -0
  56. data/lib/necropsy/analyzers/static/cha.rb +33 -69
  57. data/lib/necropsy/analyzers/static/name_resolution.rb +151 -21
  58. data/lib/necropsy/analyzers/static/rta.rb +249 -25
  59. data/lib/necropsy/ast_scanner/call_recording.rb +275 -0
  60. data/lib/necropsy/ast_scanner/call_site_creation.rb +54 -0
  61. data/lib/necropsy/ast_scanner/definition_creation.rb +43 -0
  62. data/lib/necropsy/ast_scanner/dsl_macros.rb +532 -0
  63. data/lib/necropsy/ast_scanner/method_definitions.rb +358 -0
  64. data/lib/necropsy/ast_scanner/references.rb +115 -0
  65. data/lib/necropsy/ast_scanner/ruby_semantics.rb +263 -0
  66. data/lib/necropsy/ast_scanner/traversal.rb +347 -0
  67. data/lib/necropsy/ast_scanner/value_definitions.rb +78 -0
  68. data/lib/necropsy/ast_scanner.rb +89 -564
  69. data/lib/necropsy/bench/candidate_union.rb +555 -0
  70. data/lib/necropsy/bench/claim_gate.rb +112 -0
  71. data/lib/necropsy/bench/evaluator.rb +356 -24
  72. data/lib/necropsy/bench/finding_facts.rb +152 -0
  73. data/lib/necropsy/bench/precision_gate.rb +144 -0
  74. data/lib/necropsy/bench/release_audit/adversarial_runner.rb +56 -0
  75. data/lib/necropsy/bench/release_audit/artifact_writer.rb +112 -0
  76. data/lib/necropsy/bench/release_audit/config_validator.rb +112 -0
  77. data/lib/necropsy/bench/release_audit/git_snapshot.rb +36 -0
  78. data/lib/necropsy/bench/release_audit/performance_gate.rb +165 -0
  79. data/lib/necropsy/bench/release_audit/run_provenance.rb +133 -0
  80. data/lib/necropsy/bench/release_audit.rb +360 -0
  81. data/lib/necropsy/bench/report_normalizer.rb +140 -0
  82. data/lib/necropsy/bench/review_queue.rb +154 -0
  83. data/lib/necropsy/bench/safety_mutation_harness.rb +59 -0
  84. data/lib/necropsy/bench/seed_runner.rb +408 -0
  85. data/lib/necropsy/bounded_canonicalizer.rb +218 -0
  86. data/lib/necropsy/cache/scan_cache.rb +94 -18
  87. data/lib/necropsy/call_site_identity.rb +54 -0
  88. data/lib/necropsy/cli.rb +296 -53
  89. data/lib/necropsy/clock.rb +40 -0
  90. data/lib/necropsy/confidence/scorer.rb +181 -58
  91. data/lib/necropsy/configuration.rb +339 -18
  92. data/lib/necropsy/convention_rules.rb +138 -0
  93. data/lib/necropsy/definition_identity/canonical_digest.rb +278 -0
  94. data/lib/necropsy/definition_identity.rb +37 -0
  95. data/lib/necropsy/diagnostics.rb +342 -0
  96. data/lib/necropsy/embedded_ruby.rb +55 -0
  97. data/lib/necropsy/entry_points/plain.rb +125 -7
  98. data/lib/necropsy/entry_points/rails.rb +422 -105
  99. data/lib/necropsy/entry_points/test.rb +6 -1
  100. data/lib/necropsy/flow_interpreter.rb +460 -0
  101. data/lib/necropsy/graph/blocker_matching.rb +338 -0
  102. data/lib/necropsy/graph/call_graph.rb +1234 -62
  103. data/lib/necropsy/graph/definition_index.rb +149 -0
  104. data/lib/necropsy/graph/dynamic_evidence_tracking.rb +206 -0
  105. data/lib/necropsy/graph/evidence_store.rb +213 -0
  106. data/lib/necropsy/graph/resolution_store.rb +497 -0
  107. data/lib/necropsy/graph_self_check.rb +79 -0
  108. data/lib/necropsy/guardrail/baseline.rb +359 -13
  109. data/lib/necropsy/guardrail/diff.rb +5 -2
  110. data/lib/necropsy/guardrail/quarantine.rb +101 -10
  111. data/lib/necropsy/load_graph.rb +206 -0
  112. data/lib/necropsy/models.rb +894 -21
  113. data/lib/necropsy/performance_profiler.rb +108 -0
  114. data/lib/necropsy/project.rb +361 -13
  115. data/lib/necropsy/reachability/engine.rb +80 -15
  116. data/lib/necropsy/reference_barrier.rb +458 -0
  117. data/lib/necropsy/report.rb +153 -14
  118. data/lib/necropsy/reporter.rb +441 -19
  119. data/lib/necropsy/runner.rb +258 -14
  120. data/lib/necropsy/runtime_feedback.rb +136 -0
  121. data/lib/necropsy/semantics_matrix.rb +153 -0
  122. data/lib/necropsy/trace_point_runtime.rb +19 -0
  123. data/lib/necropsy/type_facts.rb +53 -0
  124. data/lib/necropsy/version.rb +1 -1
  125. data/lib/necropsy/why_not_explanation.rb +436 -0
  126. data/lib/necropsy/why_not_renderer.rb +197 -0
  127. data/lib/necropsy/world_policy.rb +90 -0
  128. data/lib/necropsy.rb +39 -2
  129. data/schema/necropsy-report-v2.schema.json +366 -0
  130. data/script/measure.rb +20 -0
  131. metadata +106 -2
@@ -0,0 +1,358 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Necropsy
4
+ class AstScanner
5
+ private
6
+
7
+ def handle_define_method(node, context)
8
+ return false unless node.name == :define_method
9
+ return false unless context.owner
10
+
11
+ method_name = literal_argument(node, index: 0)
12
+ unless method_name
13
+ record_semantic_blocker(
14
+ :dynamic_definition,
15
+ node,
16
+ context,
17
+ 'define_method name is not statically bounded',
18
+ suggested_action: :make_method_name_literal
19
+ )
20
+ visit_synthetic_body(node.block&.body, context, :dynamic_define_method)
21
+ return handled_call
22
+ end
23
+
24
+ kind, separator = method_kind_and_separator(context)
25
+ id = "#{context.owner}#{separator}#{method_name}"
26
+ definition = add_definition(
27
+ symbol_id: id,
28
+ kind: kind,
29
+ source_node: node,
30
+ context: context,
31
+ defined_via: :define_method,
32
+ owner: context.owner,
33
+ name: method_name,
34
+ visibility: context.visibility
35
+ )
36
+ record_module_function_copy(node, context, definition) if context.module_function
37
+
38
+ if node.block
39
+ block_context = context.dup
40
+ block_context.current_caller_id = definition.graph_id
41
+ block_context.current_method_name = method_name
42
+ block_context.current_kind = kind
43
+ block_context.singleton_scope = false
44
+ block_context.visibility = :public
45
+ block_context.module_function = false
46
+ block_context.static_ancestry = false
47
+ visit(node.block.body, block_context)
48
+ end
49
+ handled_call
50
+ end
51
+
52
+ def handle_define_singleton_method(node, context)
53
+ return false unless node.name == :define_singleton_method
54
+
55
+ owner = definition_owner_for_call(node, context)
56
+ method_name = literal_argument(node, index: 0)
57
+ unless owner && method_name
58
+ record_semantic_blocker(
59
+ :dynamic_singleton_definition,
60
+ node,
61
+ context,
62
+ 'define_singleton_method receiver or name is not statically bounded',
63
+ suggested_action: :make_singleton_definition_static,
64
+ scope_owner: owner,
65
+ force_global: !owner
66
+ )
67
+ visit_synthetic_body(node.block&.body, context, :dynamic_define_singleton_method)
68
+ return handled_call
69
+ end
70
+
71
+ id = "#{owner}.#{method_name}"
72
+ definition = add_definition(
73
+ symbol_id: id,
74
+ kind: :singleton_method,
75
+ source_node: node,
76
+ context: context,
77
+ defined_via: :define_singleton_method,
78
+ owner: owner,
79
+ name: method_name,
80
+ visibility: :public
81
+ )
82
+ if node.block
83
+ block_context = context.dup
84
+ block_context.owner = owner
85
+ block_context.current_caller_id = definition.graph_id
86
+ block_context.current_method_name = method_name
87
+ block_context.current_kind = :singleton_method
88
+ block_context.singleton_scope = false
89
+ block_context.visibility = :public
90
+ block_context.module_function = false
91
+ block_context.static_ancestry = false
92
+ visit(node.block.body, block_context)
93
+ end
94
+ handled_call
95
+ end
96
+
97
+ def handle_eval(node, context)
98
+ return false unless %i[class_eval class_exec module_eval module_exec instance_eval instance_exec eval].include?(node.name)
99
+
100
+ owner = eval_owner(node, context)
101
+ unless node.block && owner && node.name != :eval
102
+ record_semantic_blocker(
103
+ :variable_eval,
104
+ node,
105
+ context,
106
+ 'Eval receiver or source is not statically bounded',
107
+ suggested_action: :replace_variable_eval,
108
+ scope_owner: node.name == :eval ? nil : owner,
109
+ receiver_kind: %i[instance_eval instance_exec].include?(node.name) ? :constant : nil,
110
+ force_global: node.name == :eval
111
+ )
112
+ visit_synthetic_body(node.block&.body, context, :dynamic_eval)
113
+ return handled_call
114
+ end
115
+
116
+ block_context = context.dup
117
+ block_context.namespace = owner
118
+ block_context.owner = owner
119
+ block_context.singleton_scope = %i[instance_eval instance_exec].include?(node.name)
120
+ block_context.visibility = :public
121
+ block_context.module_function = false
122
+ block_context.static_ancestry = false
123
+ visit(node.block.body, block_context)
124
+ handled_call
125
+ end
126
+
127
+ def handle_unsupported_semantics(node, context)
128
+ return false unless %i[refine using].include?(node.name)
129
+
130
+ record_semantic_blocker(
131
+ :unsupported_refinement,
132
+ node,
133
+ context,
134
+ "#{node.name} changes lexical method lookup and is not resolved",
135
+ suggested_action: :review_refinement_scope
136
+ )
137
+ handled_call
138
+ end
139
+
140
+ def record_semantic_blocker(kind, node, context, reason, suggested_action: :review,
141
+ scope_owner: context.owner, receiver_kind: nil, force_global: false)
142
+ global = force_global || !scope_owner
143
+ scope_kind = global ? :global : :owner
144
+ scope_value = global ? '*' : scope_owner
145
+ metadata = {
146
+ 'caller_domain' => context.test ? 'test' : 'runtime',
147
+ 'caller_id' => context.current_caller_id,
148
+ 'semantic_operation' => (node.respond_to?(:name) ? node.name : node.type).to_s,
149
+ 'owner_scope' => Array(scope_owner).compact,
150
+ 'file' => context.relative_file,
151
+ 'line' => node.location.start_line,
152
+ 'reason_code' => kind.to_s
153
+ }
154
+ metadata['receiver_kind'] = receiver_kind.to_s if receiver_kind
155
+ semantic_blockers << Blocker.new(
156
+ kind: kind,
157
+ scope_kind: scope_kind,
158
+ scope_value: scope_value,
159
+ source: 'ast_scanner',
160
+ reason: reason,
161
+ suggested_action: suggested_action,
162
+ metadata: metadata
163
+ )
164
+ end
165
+
166
+ def handle_visibility(node, context)
167
+ return false unless VISIBILITY_MACROS.include?(node.name)
168
+ return false unless context.owner
169
+
170
+ modifier_definitions = arguments(node).grep(Prism::DefNode)
171
+ class_method = node.name.to_s.end_with?('_class_method')
172
+ visibility = node.name.to_s.delete_suffix('_class_method').to_sym
173
+ unless modifier_definitions.empty?
174
+ modifier_definitions.each do |definition|
175
+ definition_context = context.dup
176
+ definition_context.visibility = visibility
177
+ visit_def(definition, definition_context)
178
+ end
179
+ return handled_call(arguments: false)
180
+ end
181
+
182
+ modifier_calls = arguments(node).grep(Prism::CallNode)
183
+ unless modifier_calls.empty?
184
+ modifier_calls.each { |call| visit(call, context) }
185
+ names = [*symbol_arguments(node), *modifier_calls.flat_map { |call| visibility_result_names(call) }].uniq
186
+ names.each { |name| update_method_visibility(context, name, visibility, node, singleton: class_method) }
187
+ record_unknown_visibility_result(node, context, modifier_calls, visibility) if names.empty?
188
+ arguments(node).reject { |argument| argument.is_a?(Prism::CallNode) || argument.is_a?(Prism::DefNode) }
189
+ .each { |argument| visit(argument, context) }
190
+ return handled_call(receiver: false, arguments: false)
191
+ end
192
+
193
+ names = symbol_arguments(node)
194
+ if names.empty?
195
+ unless class_method
196
+ context.visibility = visibility
197
+ context.module_function = false
198
+ end
199
+ else
200
+ names.each { |name| update_method_visibility(context, name, visibility, node, singleton: class_method) }
201
+ end
202
+ handled_call
203
+ end
204
+
205
+ def visibility_result_names(call)
206
+ names = symbol_arguments(call)
207
+ case call.name
208
+ when :attr_reader
209
+ names
210
+ when :attr_writer
211
+ names.map { |name| "#{name}=" }
212
+ when :attr_accessor
213
+ names.flat_map { |name| [name, "#{name}="] }
214
+ else
215
+ []
216
+ end
217
+ end
218
+
219
+ def record_unknown_visibility_result(node, context, modifier_calls, visibility)
220
+ operations = modifier_calls.map(&:name).compact.map(&:to_s).uniq.sort
221
+ record_semantic_blocker(
222
+ :visibility_activation,
223
+ node,
224
+ context,
225
+ "#{visibility} receives runtime-computed method names from #{operations.join(', ')}",
226
+ suggested_action: :review_load_order
227
+ )
228
+ end
229
+
230
+ def handle_module_function(node, context)
231
+ return false unless node.name == :module_function
232
+ return false unless context.owner
233
+
234
+ modifier_definitions = arguments(node).grep(Prism::DefNode)
235
+ unless modifier_definitions.empty?
236
+ modifier_definitions.each do |definition|
237
+ definition_context = context.dup
238
+ definition_context.module_function = true
239
+ definition_context.visibility = :private
240
+ visit_def(definition, definition_context)
241
+ end
242
+ return handled_call(arguments: false)
243
+ end
244
+
245
+ names = symbol_arguments(node)
246
+ if names.empty?
247
+ context.module_function = true
248
+ context.visibility = :private
249
+ else
250
+ names.each { |name| defer_module_function(context, name, node) }
251
+ end
252
+ handled_call
253
+ end
254
+
255
+ def record_module_function_copy(node, context, instance_node)
256
+ copy = add_module_function_definition(context, instance_node.name, node)
257
+ module_function_sources[copy.graph_id] = [instance_node.graph_id]
258
+ method_signatures[copy.graph_id] = method_signatures[instance_node.graph_id] if method_signatures[instance_node.graph_id]
259
+ end
260
+
261
+ def handle_alias_method(node, context)
262
+ return false unless node.name == :alias_method
263
+ return false unless context.owner
264
+
265
+ new_name, old_name = symbol_arguments(node)
266
+ return false unless new_name && old_name
267
+
268
+ record_alias_method(context, node, new_name, old_name)
269
+ handled_call
270
+ end
271
+
272
+ def handle_method_removal(node, context)
273
+ return false unless %i[remove_method undef_method].include?(node.name)
274
+ return false unless context.owner
275
+
276
+ names = symbol_arguments(node)
277
+ if names.empty?
278
+ record_semantic_blocker(
279
+ :dynamic_method_removal,
280
+ node,
281
+ context,
282
+ "#{node.name} target is not statically bounded",
283
+ suggested_action: :make_method_name_literal,
284
+ force_global: true
285
+ )
286
+ return handled_call
287
+ end
288
+
289
+ names.each do |name|
290
+ semantic_blockers << Blocker.new(
291
+ kind: node.name,
292
+ scope_kind: :message,
293
+ scope_value: name,
294
+ source: 'ast_scanner',
295
+ reason: "#{context.owner} #{node.name}s #{name}; activation order is not closed",
296
+ suggested_action: :review_method_activation,
297
+ metadata: {
298
+ 'caller_domain' => context.test ? 'test' : 'runtime',
299
+ 'caller_id' => context.current_caller_id,
300
+ 'owner_scope' => [context.owner],
301
+ 'file' => context.relative_file,
302
+ 'line' => node.location.start_line,
303
+ 'message' => name,
304
+ 'reason_code' => node.name.to_s
305
+ }
306
+ )
307
+ end
308
+ handled_call
309
+ end
310
+
311
+ def visit_alias_method_node(node, context)
312
+ return visit_children(node, context) unless context.owner
313
+
314
+ new_name = node.new_name.unescaped.to_s
315
+ old_name = node.old_name.unescaped.to_s
316
+ record_alias_method(context, node, new_name, old_name)
317
+ end
318
+
319
+ def record_alias_method(context, source_node, new_name, old_name)
320
+ kind = context.singleton_scope ? :singleton_method : :instance_method
321
+ separator = kind == :singleton_method ? '.' : '#'
322
+ id = "#{context.owner}#{separator}#{new_name}"
323
+ physical_source = physical_alias_source(context, source_node, old_name, kind)
324
+ definition = add_definition(
325
+ symbol_id: id,
326
+ kind: kind,
327
+ source_node: source_node,
328
+ context: context,
329
+ defined_via: :alias_method,
330
+ owner: context.owner,
331
+ name: new_name,
332
+ visibility: physical_source&.visibility || context.visibility
333
+ )
334
+ metadata = { 'original_message' => old_name, 'alias_method' => true }
335
+ metadata['physical_target_definition_id'] = physical_source.graph_id if physical_source
336
+ add_scanned_call_site(
337
+ source_node: source_node,
338
+ context: context,
339
+ role: :alias_target,
340
+ caller_id: definition.graph_id,
341
+ message: old_name,
342
+ receiver_kind: :self,
343
+ receiver_name: context.owner,
344
+ dynamic: false,
345
+ metadata: metadata
346
+ )
347
+ end
348
+
349
+ def physical_alias_source(context, source_node, old_name, kind)
350
+ return unless context.static_ancestry
351
+
352
+ nodes.select do |definition|
353
+ definition.kind == kind && definition.owner == context.owner && definition.name == old_name &&
354
+ definition.file == context.relative_file && definition.line <= source_node.location.start_line
355
+ end.max_by { |definition| [definition.line, definition.ordinal] }
356
+ end
357
+ end
358
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Necropsy
4
+ class AstScanner
5
+ RESPOND_TO_TRUTHY_LITERAL_TYPES = %i[
6
+ array_node float_node hash_node imaginary_node integer_node
7
+ interpolated_match_last_line_node interpolated_regular_expression_node
8
+ interpolated_string_node interpolated_symbol_node interpolated_x_string_node
9
+ keyword_hash_node match_last_line_node range_node rational_node
10
+ regular_expression_node source_encoding_node source_file_node source_line_node
11
+ string_node symbol_node true_node x_string_node
12
+ ].freeze
13
+
14
+ private
15
+
16
+ def callback_names(node)
17
+ names = symbol_arguments(node)
18
+ with = keyword_value(node, 'with')
19
+ names << with if with.is_a?(String)
20
+ names.uniq
21
+ end
22
+
23
+ def custom_validator_names(node)
24
+ keyword_keys(node).reject { |name| RAILS_BUILTIN_VALIDATORS.include?(name) }.map do |name|
25
+ name.split('_').map(&:capitalize).join
26
+ end
27
+ end
28
+
29
+ def delegated_method_name(name, target, prefix)
30
+ return name unless prefix
31
+
32
+ prefix_name = prefix == true ? target.to_s.delete_prefix('@') : prefix.to_s
33
+ "#{prefix_name}_#{name}"
34
+ end
35
+
36
+ def record_delegate_target(caller_id, target, node, context)
37
+ record_reference_call(
38
+ caller_id, target.to_s.delete_prefix('@'), node, context,
39
+ role: :delegate_target, receiver_kind: :self
40
+ )
41
+ end
42
+
43
+ def record_delegated_message(caller_id, message, node, context)
44
+ record_reference_call(caller_id, message, node, context, role: :delegate_message, receiver_kind: :unknown)
45
+ end
46
+
47
+ def record_symbol_reference(node, context)
48
+ return unless SYMBOL_REFERENCE_CALLS.include?(node.name)
49
+
50
+ message = first_symbol_argument(node) || first_string_argument(node)
51
+ return unless message
52
+
53
+ receiver = classify_receiver(node.receiver, context)
54
+ record_reference_call(
55
+ context.current_caller_id,
56
+ message,
57
+ node,
58
+ context,
59
+ role: :symbol_reference,
60
+ receiver_kind: receiver[:kind],
61
+ receiver_name: receiver[:name],
62
+ candidates: receiver[:candidates],
63
+ metadata: symbol_reference_metadata(node)
64
+ )
65
+ end
66
+
67
+ def record_symbol_to_proc(node, context)
68
+ block = node.block
69
+ return unless block.is_a?(Prism::BlockArgumentNode)
70
+ return unless block.expression.is_a?(Prism::SymbolNode)
71
+
72
+ record_reference_call(
73
+ context.current_caller_id,
74
+ block.expression.unescaped.to_s,
75
+ node,
76
+ context,
77
+ role: :symbol_to_proc,
78
+ receiver_kind: :unknown
79
+ )
80
+ end
81
+
82
+ def record_reference_call(caller_id, message, node, context, role:, receiver_kind:, receiver_name: nil,
83
+ candidates: [], metadata: {})
84
+ add_scanned_call_site(
85
+ source_node: node,
86
+ context: context,
87
+ role: role,
88
+ caller_id: caller_id,
89
+ message: message.to_s,
90
+ receiver_kind: receiver_kind,
91
+ receiver_name: receiver_name,
92
+ dynamic: false,
93
+ metadata: { 'symbol_reference' => true, 'receiver_candidates' => Array(candidates) }.merge(metadata)
94
+ )
95
+ end
96
+
97
+ def symbol_reference_metadata(node)
98
+ metadata = { 'original_message' => node.name.to_s }
99
+ return metadata unless node.name == :respond_to?
100
+
101
+ metadata['include_private'] = respond_to_include_private(arguments(node))
102
+ metadata
103
+ end
104
+
105
+ def respond_to_include_private(call_arguments)
106
+ return false unless call_arguments.length > 1
107
+
108
+ argument = call_arguments[1]
109
+ return false if %i[false_node nil_node].include?(argument.type)
110
+ return true if RESPOND_TO_TRUTHY_LITERAL_TYPES.include?(argument.type)
111
+
112
+ 'unknown'
113
+ end
114
+ end
115
+ end