necropsy 0.2.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/CHANGELOG.md +26 -0
- data/MEASUREMENTS.md +27 -0
- data/README.md +182 -17
- 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 +69 -17
- data/lib/necropsy/analyzers/dynamic/coverage_importer.rb +99 -9
- data/lib/necropsy/analyzers/dynamic/coverband_importer.rb +69 -299
- 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 +133 -23
- 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 -73
- data/lib/necropsy/analyzers/static/name_resolution.rb +133 -12
- data/lib/necropsy/analyzers/static/rta.rb +237 -29
- data/lib/necropsy/ast_scanner/call_recording.rb +149 -30
- 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 +431 -41
- data/lib/necropsy/ast_scanner/method_definitions.rb +232 -50
- data/lib/necropsy/ast_scanner/references.rb +43 -9
- data/lib/necropsy/ast_scanner/ruby_semantics.rb +101 -15
- data/lib/necropsy/ast_scanner/traversal.rb +234 -71
- data/lib/necropsy/ast_scanner/value_definitions.rb +23 -13
- data/lib/necropsy/ast_scanner.rb +67 -6
- data/lib/necropsy/bench/candidate_union.rb +555 -0
- data/lib/necropsy/bench/claim_gate.rb +112 -0
- data/lib/necropsy/bench/evaluator.rb +329 -16
- 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 +85 -17
- data/lib/necropsy/call_site_identity.rb +54 -0
- data/lib/necropsy/cli.rb +220 -33
- data/lib/necropsy/clock.rb +40 -0
- data/lib/necropsy/confidence/scorer.rb +103 -58
- data/lib/necropsy/configuration.rb +224 -21
- 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 +176 -36
- data/lib/necropsy/embedded_ruby.rb +55 -0
- data/lib/necropsy/entry_points/plain.rb +111 -10
- data/lib/necropsy/entry_points/rails.rb +322 -41
- 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 +1099 -109
- 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 +350 -13
- data/lib/necropsy/guardrail/quarantine.rb +94 -9
- data/lib/necropsy/load_graph.rb +206 -0
- data/lib/necropsy/models.rb +878 -13
- data/lib/necropsy/performance_profiler.rb +108 -0
- data/lib/necropsy/project.rb +327 -25
- data/lib/necropsy/reachability/engine.rb +54 -13
- data/lib/necropsy/reference_barrier.rb +458 -0
- data/lib/necropsy/report.rb +113 -4
- data/lib/necropsy/reporter.rb +431 -15
- data/lib/necropsy/runner.rb +233 -18
- data/lib/necropsy/runtime_feedback.rb +136 -0
- data/lib/necropsy/semantics_matrix.rb +153 -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 +35 -2
- data/schema/necropsy-report-v2.schema.json +366 -0
- metadata +85 -1
|
@@ -8,121 +8,254 @@ module Necropsy
|
|
|
8
8
|
return false unless node.name == :define_method
|
|
9
9
|
return false unless context.owner
|
|
10
10
|
|
|
11
|
-
method_name =
|
|
12
|
-
|
|
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
|
|
13
23
|
|
|
14
24
|
kind, separator = method_kind_and_separator(context)
|
|
15
25
|
id = "#{context.owner}#{separator}#{method_name}"
|
|
16
|
-
|
|
17
|
-
|
|
26
|
+
definition = add_definition(
|
|
27
|
+
symbol_id: id,
|
|
18
28
|
kind: kind,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
end_line: node.location.end_line,
|
|
29
|
+
source_node: node,
|
|
30
|
+
context: context,
|
|
22
31
|
defined_via: :define_method,
|
|
23
32
|
owner: context.owner,
|
|
24
33
|
name: method_name,
|
|
25
|
-
test: context.test,
|
|
26
34
|
visibility: context.visibility
|
|
27
35
|
)
|
|
28
|
-
record_module_function_copy(node, context,
|
|
36
|
+
record_module_function_copy(node, context, definition) if context.module_function
|
|
29
37
|
|
|
30
38
|
if node.block
|
|
31
39
|
block_context = context.dup
|
|
32
|
-
block_context.current_caller_id =
|
|
40
|
+
block_context.current_caller_id = definition.graph_id
|
|
41
|
+
block_context.current_method_name = method_name
|
|
33
42
|
block_context.current_kind = kind
|
|
34
43
|
block_context.singleton_scope = false
|
|
35
44
|
block_context.visibility = :public
|
|
36
45
|
block_context.module_function = false
|
|
46
|
+
block_context.static_ancestry = false
|
|
37
47
|
visit(node.block.body, block_context)
|
|
38
48
|
end
|
|
39
|
-
|
|
49
|
+
handled_call
|
|
40
50
|
end
|
|
41
51
|
|
|
42
52
|
def handle_define_singleton_method(node, context)
|
|
43
53
|
return false unless node.name == :define_singleton_method
|
|
44
54
|
|
|
45
55
|
owner = definition_owner_for_call(node, context)
|
|
46
|
-
method_name =
|
|
47
|
-
|
|
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
|
|
48
70
|
|
|
49
71
|
id = "#{owner}.#{method_name}"
|
|
50
|
-
|
|
51
|
-
|
|
72
|
+
definition = add_definition(
|
|
73
|
+
symbol_id: id,
|
|
52
74
|
kind: :singleton_method,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
end_line: node.location.end_line,
|
|
75
|
+
source_node: node,
|
|
76
|
+
context: context,
|
|
56
77
|
defined_via: :define_singleton_method,
|
|
57
78
|
owner: owner,
|
|
58
79
|
name: method_name,
|
|
59
|
-
test: context.test,
|
|
60
80
|
visibility: :public
|
|
61
81
|
)
|
|
62
82
|
if node.block
|
|
63
83
|
block_context = context.dup
|
|
64
84
|
block_context.owner = owner
|
|
65
|
-
block_context.current_caller_id =
|
|
85
|
+
block_context.current_caller_id = definition.graph_id
|
|
86
|
+
block_context.current_method_name = method_name
|
|
66
87
|
block_context.current_kind = :singleton_method
|
|
67
88
|
block_context.singleton_scope = false
|
|
68
89
|
block_context.visibility = :public
|
|
69
90
|
block_context.module_function = false
|
|
91
|
+
block_context.static_ancestry = false
|
|
70
92
|
visit(node.block.body, block_context)
|
|
71
93
|
end
|
|
72
|
-
|
|
94
|
+
handled_call
|
|
73
95
|
end
|
|
74
96
|
|
|
75
97
|
def handle_eval(node, context)
|
|
76
|
-
return false unless %i[class_eval module_eval].include?(node.name)
|
|
77
|
-
return false unless node.block
|
|
98
|
+
return false unless %i[class_eval class_exec module_eval module_exec instance_eval instance_exec eval].include?(node.name)
|
|
78
99
|
|
|
79
100
|
owner = eval_owner(node, context)
|
|
80
|
-
|
|
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
|
|
81
115
|
|
|
82
116
|
block_context = context.dup
|
|
83
117
|
block_context.namespace = owner
|
|
84
118
|
block_context.owner = owner
|
|
85
|
-
block_context.singleton_scope =
|
|
119
|
+
block_context.singleton_scope = %i[instance_eval instance_exec].include?(node.name)
|
|
86
120
|
block_context.visibility = :public
|
|
87
121
|
block_context.module_function = false
|
|
122
|
+
block_context.static_ancestry = false
|
|
88
123
|
visit(node.block.body, block_context)
|
|
89
|
-
|
|
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
|
+
)
|
|
90
164
|
end
|
|
91
165
|
|
|
92
166
|
def handle_visibility(node, context)
|
|
93
167
|
return false unless VISIBILITY_MACROS.include?(node.name)
|
|
94
168
|
return false unless context.owner
|
|
95
169
|
|
|
96
|
-
|
|
170
|
+
modifier_definitions = arguments(node).grep(Prism::DefNode)
|
|
97
171
|
class_method = node.name.to_s.end_with?('_class_method')
|
|
98
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)
|
|
99
194
|
if names.empty?
|
|
100
195
|
unless class_method
|
|
101
196
|
context.visibility = visibility
|
|
102
197
|
context.module_function = false
|
|
103
198
|
end
|
|
104
199
|
else
|
|
105
|
-
names.each { |name| update_method_visibility(context, name, visibility, singleton: class_method) }
|
|
200
|
+
names.each { |name| update_method_visibility(context, name, visibility, node, singleton: class_method) }
|
|
106
201
|
end
|
|
107
|
-
|
|
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
|
+
)
|
|
108
228
|
end
|
|
109
229
|
|
|
110
230
|
def handle_module_function(node, context)
|
|
111
231
|
return false unless node.name == :module_function
|
|
112
232
|
return false unless context.owner
|
|
113
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
|
+
|
|
114
245
|
names = symbol_arguments(node)
|
|
115
246
|
if names.empty?
|
|
116
247
|
context.module_function = true
|
|
117
248
|
context.visibility = :private
|
|
118
249
|
else
|
|
119
|
-
names.each { |name|
|
|
250
|
+
names.each { |name| defer_module_function(context, name, node) }
|
|
120
251
|
end
|
|
121
|
-
|
|
252
|
+
handled_call
|
|
122
253
|
end
|
|
123
254
|
|
|
124
|
-
def record_module_function_copy(node, context,
|
|
125
|
-
|
|
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]
|
|
126
259
|
end
|
|
127
260
|
|
|
128
261
|
def handle_alias_method(node, context)
|
|
@@ -132,8 +265,47 @@ module Necropsy
|
|
|
132
265
|
new_name, old_name = symbol_arguments(node)
|
|
133
266
|
return false unless new_name && old_name
|
|
134
267
|
|
|
135
|
-
record_alias_method(context, node
|
|
136
|
-
|
|
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
|
|
137
309
|
end
|
|
138
310
|
|
|
139
311
|
def visit_alias_method_node(node, context)
|
|
@@ -141,36 +313,46 @@ module Necropsy
|
|
|
141
313
|
|
|
142
314
|
new_name = node.new_name.unescaped.to_s
|
|
143
315
|
old_name = node.old_name.unescaped.to_s
|
|
144
|
-
record_alias_method(context, node
|
|
316
|
+
record_alias_method(context, node, new_name, old_name)
|
|
145
317
|
end
|
|
146
318
|
|
|
147
|
-
def record_alias_method(context,
|
|
319
|
+
def record_alias_method(context, source_node, new_name, old_name)
|
|
148
320
|
kind = context.singleton_scope ? :singleton_method : :instance_method
|
|
149
321
|
separator = kind == :singleton_method ? '.' : '#'
|
|
150
322
|
id = "#{context.owner}#{separator}#{new_name}"
|
|
151
|
-
|
|
152
|
-
|
|
323
|
+
physical_source = physical_alias_source(context, source_node, old_name, kind)
|
|
324
|
+
definition = add_definition(
|
|
325
|
+
symbol_id: id,
|
|
153
326
|
kind: kind,
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
end_line: location.end_line,
|
|
327
|
+
source_node: source_node,
|
|
328
|
+
context: context,
|
|
157
329
|
defined_via: :alias_method,
|
|
158
330
|
owner: context.owner,
|
|
159
331
|
name: new_name,
|
|
160
|
-
|
|
161
|
-
visibility: context.visibility
|
|
332
|
+
visibility: physical_source&.visibility || context.visibility
|
|
162
333
|
)
|
|
163
|
-
|
|
164
|
-
|
|
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,
|
|
165
341
|
message: old_name,
|
|
166
342
|
receiver_kind: :self,
|
|
167
343
|
receiver_name: context.owner,
|
|
168
|
-
file: context.relative_file,
|
|
169
|
-
line: location.start_line,
|
|
170
|
-
test: context.test,
|
|
171
344
|
dynamic: false,
|
|
172
|
-
metadata:
|
|
345
|
+
metadata: metadata
|
|
173
346
|
)
|
|
174
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
|
|
175
357
|
end
|
|
176
358
|
end
|
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
module Necropsy
|
|
4
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
|
+
|
|
5
14
|
private
|
|
6
15
|
|
|
7
16
|
def callback_names(node)
|
|
@@ -25,11 +34,14 @@ module Necropsy
|
|
|
25
34
|
end
|
|
26
35
|
|
|
27
36
|
def record_delegate_target(caller_id, target, node, context)
|
|
28
|
-
record_reference_call(
|
|
37
|
+
record_reference_call(
|
|
38
|
+
caller_id, target.to_s.delete_prefix('@'), node, context,
|
|
39
|
+
role: :delegate_target, receiver_kind: :self
|
|
40
|
+
)
|
|
29
41
|
end
|
|
30
42
|
|
|
31
43
|
def record_delegated_message(caller_id, message, node, context)
|
|
32
|
-
record_reference_call(caller_id, message, node, context, receiver_kind: :unknown)
|
|
44
|
+
record_reference_call(caller_id, message, node, context, role: :delegate_message, receiver_kind: :unknown)
|
|
33
45
|
end
|
|
34
46
|
|
|
35
47
|
def record_symbol_reference(node, context)
|
|
@@ -44,9 +56,11 @@ module Necropsy
|
|
|
44
56
|
message,
|
|
45
57
|
node,
|
|
46
58
|
context,
|
|
59
|
+
role: :symbol_reference,
|
|
47
60
|
receiver_kind: receiver[:kind],
|
|
48
61
|
receiver_name: receiver[:name],
|
|
49
|
-
candidates: receiver[:candidates]
|
|
62
|
+
candidates: receiver[:candidates],
|
|
63
|
+
metadata: symbol_reference_metadata(node)
|
|
50
64
|
)
|
|
51
65
|
end
|
|
52
66
|
|
|
@@ -60,22 +74,42 @@ module Necropsy
|
|
|
60
74
|
block.expression.unescaped.to_s,
|
|
61
75
|
node,
|
|
62
76
|
context,
|
|
77
|
+
role: :symbol_to_proc,
|
|
63
78
|
receiver_kind: :unknown
|
|
64
79
|
)
|
|
65
80
|
end
|
|
66
81
|
|
|
67
|
-
def record_reference_call(caller_id, message, node, context, receiver_kind:, receiver_name: nil,
|
|
68
|
-
|
|
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,
|
|
69
88
|
caller_id: caller_id,
|
|
70
89
|
message: message.to_s,
|
|
71
90
|
receiver_kind: receiver_kind,
|
|
72
91
|
receiver_name: receiver_name,
|
|
73
|
-
file: context.relative_file,
|
|
74
|
-
line: node.location.start_line,
|
|
75
|
-
test: context.test,
|
|
76
92
|
dynamic: false,
|
|
77
|
-
metadata: { 'symbol_reference' => true, 'receiver_candidates' => Array(candidates) }
|
|
93
|
+
metadata: { 'symbol_reference' => true, 'receiver_candidates' => Array(candidates) }.merge(metadata)
|
|
78
94
|
)
|
|
79
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
|
|
80
114
|
end
|
|
81
115
|
end
|
|
@@ -5,20 +5,20 @@ module Necropsy
|
|
|
5
5
|
private
|
|
6
6
|
|
|
7
7
|
def classify_receiver(receiver, context)
|
|
8
|
-
return { kind: :implicit, name:
|
|
9
|
-
return { kind: :self, name: context.owner } if receiver.is_a?(Prism::SelfNode)
|
|
8
|
+
return { kind: :implicit, name: context.owner, candidates: [context.owner].compact } unless receiver
|
|
9
|
+
return { kind: :self, name: context.owner, candidates: [context.owner].compact } if receiver.is_a?(Prism::SelfNode)
|
|
10
10
|
|
|
11
11
|
constant = constant_name(receiver)
|
|
12
12
|
if constant
|
|
13
|
-
candidates = constant_candidates(constant, context.
|
|
14
|
-
return { kind: :constant, name: candidates
|
|
13
|
+
candidates = constant_candidates(constant, context.lexical_nesting)
|
|
14
|
+
return { kind: :constant, name: resolve_candidate_group(candidates), candidates: candidates }
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
if receiver.is_a?(Prism::CallNode) && receiver.name == :new
|
|
18
18
|
receiver_constant = constant_name(receiver.receiver)
|
|
19
19
|
if receiver_constant
|
|
20
|
-
candidates = constant_candidates(receiver_constant, context.
|
|
21
|
-
return { kind: :instance, name: candidates
|
|
20
|
+
candidates = constant_candidates(receiver_constant, context.lexical_nesting)
|
|
21
|
+
return { kind: :instance, name: resolve_candidate_group(candidates), candidates: candidates }
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
24
|
|
|
@@ -33,6 +33,17 @@ module Necropsy
|
|
|
33
33
|
arguments(node).find { |argument| argument.is_a?(Prism::StringNode) }&.unescaped
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
+
def literal_argument(node, index:)
|
|
37
|
+
argument = arguments(node)[index]
|
|
38
|
+
return unless literal_name?(argument)
|
|
39
|
+
|
|
40
|
+
argument.unescaped.to_s
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def literal_name?(node)
|
|
44
|
+
node.is_a?(Prism::SymbolNode) || node.is_a?(Prism::StringNode)
|
|
45
|
+
end
|
|
46
|
+
|
|
36
47
|
def symbol_arguments(node)
|
|
37
48
|
arguments(node).filter_map do |arg|
|
|
38
49
|
next unless arg.is_a?(Prism::SymbolNode)
|
|
@@ -71,6 +82,53 @@ module Necropsy
|
|
|
71
82
|
end
|
|
72
83
|
end
|
|
73
84
|
|
|
85
|
+
def method_signature(parameters)
|
|
86
|
+
return { 'complete' => false } unless parameters
|
|
87
|
+
|
|
88
|
+
forwarding = parameters.child_nodes.compact.any? { |parameter| parameter.type == :forwarding_parameter_node }
|
|
89
|
+
keywords = Array(parameters.keywords)
|
|
90
|
+
keyword_rest = parameters.keyword_rest
|
|
91
|
+
no_keywords = !keyword_rest.nil? && keyword_rest.type == :no_keywords_parameter_node
|
|
92
|
+
accepts_keywords = keywords.any? || (!keyword_rest.nil? && !no_keywords)
|
|
93
|
+
{
|
|
94
|
+
'complete' => !forwarding,
|
|
95
|
+
'minimum_positionals' => Array(parameters.requireds).length + Array(parameters.posts).length,
|
|
96
|
+
'maximum_positionals' => if parameters.rest
|
|
97
|
+
nil
|
|
98
|
+
else
|
|
99
|
+
Array(parameters.requireds).length +
|
|
100
|
+
Array(parameters.optionals).length + Array(parameters.posts).length
|
|
101
|
+
end,
|
|
102
|
+
'required_keywords' => keywords.select do |parameter|
|
|
103
|
+
parameter.type == :required_keyword_parameter_node
|
|
104
|
+
end.map { |parameter| parameter.name.to_s }.sort,
|
|
105
|
+
'accepted_keywords' => keywords.map { |parameter| parameter.name.to_s }.sort,
|
|
106
|
+
'accepts_keywords' => accepts_keywords,
|
|
107
|
+
'no_keywords' => no_keywords,
|
|
108
|
+
'keyword_rest' => !keyword_rest.nil? && !no_keywords
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def call_arguments(node, offset: 0)
|
|
113
|
+
values = Array(node.arguments&.arguments).drop(offset)
|
|
114
|
+
keyword_hash = values.find { |argument| argument.is_a?(Prism::KeywordHashNode) }
|
|
115
|
+
positional = values.grep_v(Prism::KeywordHashNode)
|
|
116
|
+
keyword_elements = Array(keyword_hash&.elements)
|
|
117
|
+
{
|
|
118
|
+
'complete' => positional.none? do |argument|
|
|
119
|
+
%i[splat_node forwarding_arguments_node].include?(argument.type)
|
|
120
|
+
end && keyword_elements.none? { |element| element.type == :assoc_splat_node },
|
|
121
|
+
'positional_count' => positional.length,
|
|
122
|
+
'keywords' => keyword_elements.filter_map do |element|
|
|
123
|
+
literal_value(element.key).to_s if element.is_a?(Prism::AssocNode)
|
|
124
|
+
end.sort
|
|
125
|
+
}
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def incomplete_arguments
|
|
129
|
+
{ 'complete' => false, 'positional_count' => 0, 'keywords' => [] }
|
|
130
|
+
end
|
|
131
|
+
|
|
74
132
|
def constant_name(node)
|
|
75
133
|
return nil unless node
|
|
76
134
|
|
|
@@ -100,8 +158,34 @@ module Necropsy
|
|
|
100
158
|
data[:line] = node.location.start_line
|
|
101
159
|
return unless node.respond_to?(:superclass)
|
|
102
160
|
|
|
161
|
+
unless node.superclass
|
|
162
|
+
data[:superclass_candidates] = implicit_superclass_candidates(namespace)
|
|
163
|
+
return
|
|
164
|
+
end
|
|
165
|
+
|
|
103
166
|
superclass = constant_name(node.superclass)
|
|
104
|
-
|
|
167
|
+
if superclass
|
|
168
|
+
data[:superclass_candidates] = constant_candidates(superclass, context.lexical_nesting)
|
|
169
|
+
return
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
data[:superclass_candidates] = []
|
|
173
|
+
data[:dynamic] = true
|
|
174
|
+
record_semantic_blocker(
|
|
175
|
+
:dynamic_ancestry,
|
|
176
|
+
node.superclass,
|
|
177
|
+
context,
|
|
178
|
+
"#{namespace} has a runtime-computed superclass",
|
|
179
|
+
suggested_action: :make_superclass_static,
|
|
180
|
+
scope_owner: namespace
|
|
181
|
+
)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def implicit_superclass_candidates(namespace)
|
|
185
|
+
return [] if namespace == 'BasicObject'
|
|
186
|
+
return ['BasicObject'] if namespace == 'Object'
|
|
187
|
+
|
|
188
|
+
['Object']
|
|
105
189
|
end
|
|
106
190
|
|
|
107
191
|
def class_record(namespace)
|
|
@@ -115,6 +199,8 @@ module Necropsy
|
|
|
115
199
|
includes: [],
|
|
116
200
|
prepends: [],
|
|
117
201
|
extends: [],
|
|
202
|
+
singleton_includes: [],
|
|
203
|
+
singleton_prepends: [],
|
|
118
204
|
dynamic: false
|
|
119
205
|
}
|
|
120
206
|
end
|
|
@@ -132,6 +218,8 @@ module Necropsy
|
|
|
132
218
|
includes: resolve_candidate_groups(data.fetch(:includes)),
|
|
133
219
|
prepends: resolve_candidate_groups(data.fetch(:prepends)),
|
|
134
220
|
extends: resolve_candidate_groups(data.fetch(:extends)),
|
|
221
|
+
singleton_includes: resolve_candidate_groups(data.fetch(:singleton_includes)),
|
|
222
|
+
singleton_prepends: resolve_candidate_groups(data.fetch(:singleton_prepends)),
|
|
135
223
|
dynamic: data.fetch(:dynamic)
|
|
136
224
|
)
|
|
137
225
|
end
|
|
@@ -145,18 +233,16 @@ module Necropsy
|
|
|
145
233
|
group.find { |candidate| class_data.key?(candidate) } || group.first
|
|
146
234
|
end
|
|
147
235
|
|
|
148
|
-
def constant_candidates(name,
|
|
236
|
+
def constant_candidates(name, lexical_nesting)
|
|
149
237
|
return [name.delete_prefix('::')] if name.start_with?('::')
|
|
150
|
-
|
|
238
|
+
|
|
239
|
+
nesting = Array(lexical_nesting).compact
|
|
240
|
+
return [name] if nesting.empty?
|
|
151
241
|
|
|
152
242
|
head, *rest = name.split('::')
|
|
153
243
|
suffix = rest.empty? ? '' : "::#{rest.join('::')}"
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
parts.length.downto(1) do |length|
|
|
157
|
-
candidates << "#{parts.first(length).join('::')}::#{head}#{suffix}"
|
|
158
|
-
end
|
|
159
|
-
inherited_namespaces(namespace).each do |ancestor|
|
|
244
|
+
candidates = nesting.map { |namespace| "#{namespace}::#{head}#{suffix}" }
|
|
245
|
+
inherited_namespaces(nesting.first).each do |ancestor|
|
|
160
246
|
candidates << "#{ancestor}::#{head}#{suffix}"
|
|
161
247
|
end
|
|
162
248
|
candidates << name
|