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.
- checksums.yaml +4 -4
- data/.rubocop.yml +33 -0
- data/CHANGELOG.md +44 -0
- data/MEASUREMENTS.md +112 -0
- data/README.md +233 -16
- data/Rakefile +3 -1
- data/bench/README.md +92 -0
- data/bench/audit.rb +113 -0
- data/bench/audits/0.2.1/audit.json +501 -0
- data/bench/audits/0.2.1/audit.md +55 -0
- data/bench/audits/0.2.1/baseline_performance.yml +16 -0
- data/bench/audits/0.2.1/config.yml +42 -0
- data/bench/audits/0.2.1/review.yml +56 -0
- data/bench/audits/0.2.1/review_queue.yml +3651 -0
- data/bench/corpora/v1/README.md +30 -0
- data/bench/corpora/v1/labels.yml +37 -0
- data/bench/corpora/v1/manifest.yml +63 -0
- data/bench/corpora/v1/rubocop.necropsy.yml +7 -0
- data/bench/corpora/v1/self.necropsy.yml +8 -0
- data/bench/corpora/v1/tools/debride.yml +42 -0
- data/bench/corpora/v1/tools/spoom.yml +12 -0
- data/bench/corpora/v1/tools/type_aware.yml +11 -0
- data/bench/golden/v1/candidate_union.json +21918 -0
- data/bench/golden/v1/metadata.json +12 -0
- data/bench/golden/v1/reports/dynamic_evidence.json +146 -0
- data/bench/golden/v1/reports/plain_ruby.json +135 -0
- data/bench/golden/v1/reports/rails.json +176 -0
- data/bench/golden/v1/reports/rubocop_1_75_0.json +26848 -0
- data/bench/golden/v1/reports/self.json +3449 -0
- data/bench/review_queue.rb +35 -0
- data/bench/run.rb +31 -0
- data/bench/schema/candidate-union-v1.schema.json +70 -0
- data/docs/impv_implementation_matrix.md +179 -0
- data/docs/migrations/0.2.1.md +57 -0
- data/docs/migrations/0.3.0.md +207 -0
- data/docs/migrations/0.4.0.md +13 -0
- data/docs/necropsy_performance_adr.md +43 -0
- data/docs/necropsy_scope_decisions.md +58 -0
- data/docs/necropsy_type_facts_adr.md +22 -0
- data/gemfiles/prism_min.gemfile +9 -0
- data/gemfiles/prism_min.gemfile.lock +50 -0
- data/lib/necropsy/analyzer.rb +121 -2
- data/lib/necropsy/analyzers/dynamic/coverage_collector.rb +81 -19
- data/lib/necropsy/analyzers/dynamic/coverage_importer.rb +104 -10
- data/lib/necropsy/analyzers/dynamic/coverband_importer.rb +74 -250
- data/lib/necropsy/analyzers/dynamic/coverband_payload_set.rb +149 -0
- data/lib/necropsy/analyzers/dynamic/observation_policy.rb +96 -0
- data/lib/necropsy/analyzers/dynamic/redis_input_limits.rb +121 -0
- data/lib/necropsy/analyzers/dynamic/redis_nonblocking_io.rb +94 -0
- data/lib/necropsy/analyzers/dynamic/redis_payload_loader.rb +165 -0
- data/lib/necropsy/analyzers/dynamic/redis_transport.rb +217 -0
- data/lib/necropsy/analyzers/dynamic/runtime_reference.rb +96 -0
- data/lib/necropsy/analyzers/dynamic/trace_point_collector.rb +225 -20
- data/lib/necropsy/analyzers/dynamic/trace_point_importer.rb +3 -1
- data/lib/necropsy/analyzers/legacy_result_adapter.rb +226 -0
- data/lib/necropsy/analyzers/static/cha.rb +33 -69
- data/lib/necropsy/analyzers/static/name_resolution.rb +151 -21
- data/lib/necropsy/analyzers/static/rta.rb +249 -25
- data/lib/necropsy/ast_scanner/call_recording.rb +275 -0
- data/lib/necropsy/ast_scanner/call_site_creation.rb +54 -0
- data/lib/necropsy/ast_scanner/definition_creation.rb +43 -0
- data/lib/necropsy/ast_scanner/dsl_macros.rb +532 -0
- data/lib/necropsy/ast_scanner/method_definitions.rb +358 -0
- data/lib/necropsy/ast_scanner/references.rb +115 -0
- data/lib/necropsy/ast_scanner/ruby_semantics.rb +263 -0
- data/lib/necropsy/ast_scanner/traversal.rb +347 -0
- data/lib/necropsy/ast_scanner/value_definitions.rb +78 -0
- data/lib/necropsy/ast_scanner.rb +89 -564
- data/lib/necropsy/bench/candidate_union.rb +555 -0
- data/lib/necropsy/bench/claim_gate.rb +112 -0
- data/lib/necropsy/bench/evaluator.rb +356 -24
- data/lib/necropsy/bench/finding_facts.rb +152 -0
- data/lib/necropsy/bench/precision_gate.rb +144 -0
- data/lib/necropsy/bench/release_audit/adversarial_runner.rb +56 -0
- data/lib/necropsy/bench/release_audit/artifact_writer.rb +112 -0
- data/lib/necropsy/bench/release_audit/config_validator.rb +112 -0
- data/lib/necropsy/bench/release_audit/git_snapshot.rb +36 -0
- data/lib/necropsy/bench/release_audit/performance_gate.rb +165 -0
- data/lib/necropsy/bench/release_audit/run_provenance.rb +133 -0
- data/lib/necropsy/bench/release_audit.rb +360 -0
- data/lib/necropsy/bench/report_normalizer.rb +140 -0
- data/lib/necropsy/bench/review_queue.rb +154 -0
- data/lib/necropsy/bench/safety_mutation_harness.rb +59 -0
- data/lib/necropsy/bench/seed_runner.rb +408 -0
- data/lib/necropsy/bounded_canonicalizer.rb +218 -0
- data/lib/necropsy/cache/scan_cache.rb +94 -18
- data/lib/necropsy/call_site_identity.rb +54 -0
- data/lib/necropsy/cli.rb +296 -53
- data/lib/necropsy/clock.rb +40 -0
- data/lib/necropsy/confidence/scorer.rb +181 -58
- data/lib/necropsy/configuration.rb +339 -18
- data/lib/necropsy/convention_rules.rb +138 -0
- data/lib/necropsy/definition_identity/canonical_digest.rb +278 -0
- data/lib/necropsy/definition_identity.rb +37 -0
- data/lib/necropsy/diagnostics.rb +342 -0
- data/lib/necropsy/embedded_ruby.rb +55 -0
- data/lib/necropsy/entry_points/plain.rb +125 -7
- data/lib/necropsy/entry_points/rails.rb +422 -105
- data/lib/necropsy/entry_points/test.rb +6 -1
- data/lib/necropsy/flow_interpreter.rb +460 -0
- data/lib/necropsy/graph/blocker_matching.rb +338 -0
- data/lib/necropsy/graph/call_graph.rb +1234 -62
- data/lib/necropsy/graph/definition_index.rb +149 -0
- data/lib/necropsy/graph/dynamic_evidence_tracking.rb +206 -0
- data/lib/necropsy/graph/evidence_store.rb +213 -0
- data/lib/necropsy/graph/resolution_store.rb +497 -0
- data/lib/necropsy/graph_self_check.rb +79 -0
- data/lib/necropsy/guardrail/baseline.rb +359 -13
- data/lib/necropsy/guardrail/diff.rb +5 -2
- data/lib/necropsy/guardrail/quarantine.rb +101 -10
- data/lib/necropsy/load_graph.rb +206 -0
- data/lib/necropsy/models.rb +894 -21
- data/lib/necropsy/performance_profiler.rb +108 -0
- data/lib/necropsy/project.rb +361 -13
- data/lib/necropsy/reachability/engine.rb +80 -15
- data/lib/necropsy/reference_barrier.rb +458 -0
- data/lib/necropsy/report.rb +153 -14
- data/lib/necropsy/reporter.rb +441 -19
- data/lib/necropsy/runner.rb +258 -14
- data/lib/necropsy/runtime_feedback.rb +136 -0
- data/lib/necropsy/semantics_matrix.rb +153 -0
- data/lib/necropsy/trace_point_runtime.rb +19 -0
- data/lib/necropsy/type_facts.rb +53 -0
- data/lib/necropsy/version.rb +1 -1
- data/lib/necropsy/why_not_explanation.rb +436 -0
- data/lib/necropsy/why_not_renderer.rb +197 -0
- data/lib/necropsy/world_policy.rb +90 -0
- data/lib/necropsy.rb +39 -2
- data/schema/necropsy-report-v2.schema.json +366 -0
- data/script/measure.rb +20 -0
- 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
|