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
|
@@ -4,12 +4,25 @@ module Necropsy
|
|
|
4
4
|
class AstScanner
|
|
5
5
|
private
|
|
6
6
|
|
|
7
|
+
def resolve_deferred_module_function_sources
|
|
8
|
+
deferred_module_functions.each do |copy_id, instance_id|
|
|
9
|
+
module_function_sources[copy_id] = nodes.filter_map do |definition|
|
|
10
|
+
definition.graph_id if definition.kind == :instance_method && definition.symbol_id == instance_id
|
|
11
|
+
end
|
|
12
|
+
signatures = module_function_sources.fetch(copy_id).filter_map { |source_id| method_signatures[source_id] }.uniq
|
|
13
|
+
method_signatures[copy_id] = signatures.first if signatures.one?
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
7
17
|
def copy_module_function_call_sites
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
18
|
+
copies = module_function_sources.flat_map do |copy_id, source_ids|
|
|
19
|
+
source_ids.flat_map do |source_id|
|
|
20
|
+
call_sites.select { |site| site.caller_id == source_id }.map do |site|
|
|
21
|
+
derived_call_site(
|
|
22
|
+
site, derivation: :module_function, caller_id: copy_id,
|
|
23
|
+
metadata: { 'module_function' => true }
|
|
24
|
+
)
|
|
25
|
+
end
|
|
13
26
|
end
|
|
14
27
|
end
|
|
15
28
|
call_sites.concat(copies)
|
|
@@ -17,42 +30,49 @@ module Necropsy
|
|
|
17
30
|
|
|
18
31
|
def scan_file(file)
|
|
19
32
|
relative = project.relative_path(file)
|
|
20
|
-
root_id = "file:#{relative}"
|
|
21
33
|
test = project.test_file?(file)
|
|
22
|
-
|
|
23
|
-
|
|
34
|
+
context = Context.new(
|
|
35
|
+
namespace: nil,
|
|
36
|
+
lexical_nesting: [],
|
|
37
|
+
owner: nil,
|
|
38
|
+
current_caller_id: nil,
|
|
39
|
+
current_method_name: nil,
|
|
40
|
+
current_kind: :block_entry,
|
|
41
|
+
root_id: nil,
|
|
42
|
+
file: file,
|
|
43
|
+
relative_file: relative,
|
|
44
|
+
test: test,
|
|
45
|
+
singleton_scope: false,
|
|
46
|
+
visibility: :public,
|
|
47
|
+
module_function: false,
|
|
48
|
+
static_ancestry: true,
|
|
49
|
+
flow_result: nil
|
|
50
|
+
)
|
|
51
|
+
result = Prism.parse(File.read(file))
|
|
52
|
+
root = add_definition(
|
|
53
|
+
symbol_id: "file:#{relative}",
|
|
24
54
|
kind: :block_entry,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
end_line: 1,
|
|
55
|
+
source_node: result.value,
|
|
56
|
+
context: context,
|
|
28
57
|
defined_via: :file,
|
|
29
58
|
owner: nil,
|
|
30
59
|
name: relative,
|
|
31
|
-
test: test,
|
|
32
60
|
visibility: :public
|
|
33
61
|
)
|
|
62
|
+
root_id = root.graph_id
|
|
63
|
+
context.current_caller_id = root_id
|
|
64
|
+
context.root_id = root_id
|
|
34
65
|
|
|
35
|
-
result
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
root_id: root_id,
|
|
46
|
-
file: file,
|
|
47
|
-
relative_file: relative,
|
|
48
|
-
test: test,
|
|
49
|
-
singleton_scope: false,
|
|
50
|
-
visibility: :public,
|
|
51
|
-
module_function: false
|
|
52
|
-
)
|
|
53
|
-
)
|
|
54
|
-
rescue SystemCallError, EncodingError => e
|
|
55
|
-
uncertainties[root_id] << "Could not parse #{relative}: #{e.message}"
|
|
66
|
+
if result.failure?
|
|
67
|
+
file_statuses[relative] = :recovered
|
|
68
|
+
record_parse_errors(root_id, relative, result)
|
|
69
|
+
else
|
|
70
|
+
file_statuses[relative] = :complete
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
visit(result.value, context)
|
|
74
|
+
rescue DefinitionIdentity::CanonicalizationError, SystemStackError, SystemCallError, EncodingError => e
|
|
75
|
+
record_source_failure(root_id || "file:#{relative}", relative, e)
|
|
56
76
|
end
|
|
57
77
|
|
|
58
78
|
def visit(node, context)
|
|
@@ -74,15 +94,25 @@ module Necropsy
|
|
|
74
94
|
when Prism::ConstantWriteNode
|
|
75
95
|
visit_constant_write(node, context)
|
|
76
96
|
else
|
|
77
|
-
node
|
|
97
|
+
visit_generic_node(node, context)
|
|
78
98
|
end
|
|
79
99
|
end
|
|
80
100
|
|
|
101
|
+
def visit_generic_node(node, context)
|
|
102
|
+
return visit_children(node, context) unless ANCESTRY_CONTROL_FLOW_TYPES.include?(node.type)
|
|
103
|
+
|
|
104
|
+
nested_context = context.dup
|
|
105
|
+
nested_context.static_ancestry = false
|
|
106
|
+
visit_children(node, nested_context)
|
|
107
|
+
end
|
|
108
|
+
|
|
81
109
|
def visit_namespace(node, context)
|
|
82
110
|
namespace = qualify_constant(constant_name(node.constant_path), context.namespace)
|
|
83
111
|
record_class_info(node, namespace, context)
|
|
112
|
+
visit(node.superclass, context) if node.respond_to?(:superclass) && node.superclass
|
|
84
113
|
child_context = context.dup
|
|
85
114
|
child_context.namespace = namespace
|
|
115
|
+
child_context.lexical_nesting = [namespace, *context.lexical_nesting].compact.uniq
|
|
86
116
|
child_context.owner = namespace
|
|
87
117
|
child_context.singleton_scope = false
|
|
88
118
|
child_context.visibility = :public
|
|
@@ -92,93 +122,226 @@ module Necropsy
|
|
|
92
122
|
|
|
93
123
|
def visit_def(node, context)
|
|
94
124
|
owner = definition_owner(node, context)
|
|
95
|
-
|
|
125
|
+
unless owner
|
|
126
|
+
visit(node.receiver, context) if node.receiver
|
|
127
|
+
record_semantic_blocker(
|
|
128
|
+
:dynamic_singleton_definition,
|
|
129
|
+
node,
|
|
130
|
+
context,
|
|
131
|
+
'singleton method receiver is not statically bounded',
|
|
132
|
+
suggested_action: :make_singleton_receiver_static,
|
|
133
|
+
force_global: true
|
|
134
|
+
)
|
|
135
|
+
visit_synthetic_body(node.body, context, :dynamic_singleton_method)
|
|
136
|
+
return
|
|
137
|
+
end
|
|
96
138
|
|
|
97
139
|
kind = node.receiver || context.singleton_scope ? :singleton_method : :instance_method
|
|
98
140
|
separator = kind == :singleton_method ? '.' : '#'
|
|
99
141
|
id = "#{owner}#{separator}#{node.name}"
|
|
100
|
-
|
|
101
|
-
|
|
142
|
+
definition = add_definition(
|
|
143
|
+
symbol_id: id,
|
|
102
144
|
kind: kind,
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
end_line: node.location.end_line,
|
|
145
|
+
source_node: node,
|
|
146
|
+
context: context,
|
|
106
147
|
defined_via: :def,
|
|
107
148
|
owner: owner,
|
|
108
149
|
name: node.name.to_s,
|
|
109
|
-
test: context.test,
|
|
110
150
|
visibility: node.receiver ? :public : context.visibility
|
|
111
151
|
)
|
|
112
|
-
|
|
113
|
-
if
|
|
114
|
-
|
|
152
|
+
method_signatures[definition.graph_id] = method_signature(node.parameters)
|
|
153
|
+
record_module_function_copy(node, context, definition) if kind == :instance_method && context.module_function
|
|
154
|
+
if %i[method_missing respond_to_missing?].include?(node.name)
|
|
155
|
+
uncertainties[definition.graph_id] << "#{owner} defines #{node.name}"
|
|
115
156
|
class_record(owner)[:dynamic] = true
|
|
116
157
|
end
|
|
158
|
+
rule_hit = convention_rules.method_hit(
|
|
159
|
+
owner: owner,
|
|
160
|
+
method_name: node.name,
|
|
161
|
+
ancestors: convention_ancestors(owner),
|
|
162
|
+
frameworks: project.config.frameworks
|
|
163
|
+
)
|
|
164
|
+
if rule_hit
|
|
165
|
+
entrypoint_hints << EntryPoint.new(
|
|
166
|
+
node_id: definition.graph_id,
|
|
167
|
+
reason: :convention_callback,
|
|
168
|
+
evidence: { 'type' => 'convention_rule', **rule_hit }
|
|
169
|
+
)
|
|
170
|
+
end
|
|
117
171
|
|
|
118
172
|
method_context = context.dup
|
|
119
173
|
method_context.owner = owner
|
|
120
|
-
method_context.current_caller_id =
|
|
174
|
+
method_context.current_caller_id = definition.graph_id
|
|
175
|
+
method_context.current_method_name = node.name.to_s
|
|
121
176
|
method_context.current_kind = kind
|
|
122
177
|
method_context.singleton_scope = false
|
|
123
178
|
method_context.visibility = :public
|
|
124
179
|
method_context.module_function = false
|
|
180
|
+
method_context.static_ancestry = false
|
|
181
|
+
method_context.flow_result = FlowInterpreter.new(
|
|
182
|
+
constant_resolver: lambda do |constant|
|
|
183
|
+
resolve_candidate_group(constant_candidates(constant, method_context.lexical_nesting))
|
|
184
|
+
end
|
|
185
|
+
).analyze(node.body)
|
|
186
|
+
method_context.flow_result.issues.each do |issue|
|
|
187
|
+
uncertainties[definition.graph_id] << "Forward value analysis stopped at #{issue}; affected calls use conservative lookup"
|
|
188
|
+
end
|
|
189
|
+
visit_default_parameters(node.parameters, method_context)
|
|
125
190
|
visit(node.body, method_context)
|
|
126
191
|
end
|
|
127
192
|
|
|
193
|
+
def visit_default_parameters(parameters, context)
|
|
194
|
+
return unless parameters
|
|
195
|
+
|
|
196
|
+
defaults = [*Array(parameters.optionals), *Array(parameters.keywords)].filter_map do |parameter|
|
|
197
|
+
parameter.value if parameter.respond_to?(:value)
|
|
198
|
+
end
|
|
199
|
+
defaults.each { |default| visit(default, context) }
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def convention_ancestors(owner)
|
|
203
|
+
ancestors = []
|
|
204
|
+
pending = convention_parent_candidates(class_data[owner])
|
|
205
|
+
until pending.empty?
|
|
206
|
+
current = pending.shift
|
|
207
|
+
next if ancestors.include?(current)
|
|
208
|
+
|
|
209
|
+
ancestors << current
|
|
210
|
+
pending.concat(convention_parent_candidates(class_data[current]))
|
|
211
|
+
end
|
|
212
|
+
ancestors
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def convention_parent_candidates(data)
|
|
216
|
+
return [] unless data
|
|
217
|
+
|
|
218
|
+
Array(data[:superclass_candidates]) +
|
|
219
|
+
%i[includes prepends extends singleton_includes singleton_prepends].flat_map do |relation|
|
|
220
|
+
Array(data[relation]).flatten
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
128
224
|
def visit_call(node, context)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
225
|
+
handlers = %i[
|
|
226
|
+
handle_unsupported_semantics handle_visibility handle_module_function handle_eval
|
|
227
|
+
handle_define_singleton_method handle_define_method handle_attr_macro handle_delegate
|
|
228
|
+
handle_forwardable handle_alias_method handle_method_removal
|
|
229
|
+
]
|
|
230
|
+
handled = handlers.lazy.map { |handler| send(handler, node, context) }.find(&:itself)
|
|
231
|
+
if handled
|
|
232
|
+
raise TypeError, "semantic call handler must return AstScanner::CallTraversal, got #{handled.class}" unless
|
|
233
|
+
handled.is_a?(CallTraversal)
|
|
234
|
+
|
|
235
|
+
visit_call_children(
|
|
236
|
+
node,
|
|
237
|
+
context,
|
|
238
|
+
visit_receiver: handled.receiver,
|
|
239
|
+
visit_arguments: handled.arguments,
|
|
240
|
+
visit_block: handled.block
|
|
241
|
+
)
|
|
242
|
+
return
|
|
243
|
+
end
|
|
138
244
|
|
|
139
245
|
handle_module_relation(node, context)
|
|
140
|
-
handle_rails_callback(node, context)
|
|
246
|
+
callback_block_consumed = handle_rails_callback(node, context)
|
|
247
|
+
handle_graphql_field(node, context)
|
|
248
|
+
if handle_generated_rails_methods(node, context)
|
|
249
|
+
visit_call_children(node, context, visit_block: false)
|
|
250
|
+
return
|
|
251
|
+
end
|
|
141
252
|
|
|
142
253
|
record_instantiation(node, context)
|
|
143
254
|
record_symbol_reference(node, context)
|
|
144
255
|
record_symbol_to_proc(node, context)
|
|
145
|
-
|
|
146
|
-
call_sites
|
|
147
|
-
if site
|
|
148
|
-
|
|
149
|
-
elsif unresolved_dynamic_dispatch?(node)
|
|
150
|
-
record_uncertainty_at(node, context)
|
|
151
|
-
end
|
|
256
|
+
sites = build_call_sites(node, context).compact
|
|
257
|
+
call_sites.concat(sites)
|
|
258
|
+
sites.each { |site| record_uncertainty(site) if site.dynamic }
|
|
259
|
+
record_uncertainty_at(node, context) if sites.empty? && unresolved_dynamic_dispatch?(node)
|
|
152
260
|
|
|
153
|
-
|
|
261
|
+
visit_call_children(node, context, visit_block: !callback_block_consumed)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def visit_call_children(node, context, visit_receiver: true, visit_arguments: true, visit_block: true)
|
|
265
|
+
visit(node.receiver, context) if visit_receiver && node.receiver
|
|
266
|
+
arguments(node).each { |argument| visit(argument, context) } if visit_arguments
|
|
267
|
+
visit(node.block, context) if visit_block && node.block
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def visit_synthetic_body(body, context, purpose)
|
|
271
|
+
return unless body
|
|
272
|
+
|
|
273
|
+
synthetic = add_definition(
|
|
274
|
+
symbol_id: "synthetic:#{purpose}:#{context.relative_file}:#{body.location.start_line}",
|
|
275
|
+
kind: :block_entry,
|
|
276
|
+
source_node: body,
|
|
277
|
+
context: context,
|
|
278
|
+
defined_via: purpose,
|
|
279
|
+
owner: context.owner,
|
|
280
|
+
name: purpose,
|
|
281
|
+
visibility: :public
|
|
282
|
+
)
|
|
283
|
+
body_context = context.dup
|
|
284
|
+
body_context.current_caller_id = synthetic.graph_id
|
|
285
|
+
body_context.current_method_name = nil
|
|
286
|
+
body_context.current_kind = :block_entry
|
|
287
|
+
body_context.static_ancestry = false
|
|
288
|
+
body_context.flow_result = nil
|
|
289
|
+
visit(body, body_context)
|
|
290
|
+
synthetic
|
|
154
291
|
end
|
|
155
292
|
|
|
156
293
|
def visit_super(node, context)
|
|
157
|
-
method_name = context.
|
|
294
|
+
method_name = context.current_method_name
|
|
158
295
|
if method_name
|
|
159
|
-
|
|
160
|
-
|
|
296
|
+
add_scanned_call_site(
|
|
297
|
+
source_node: node,
|
|
298
|
+
context: context,
|
|
299
|
+
role: :super,
|
|
161
300
|
message: method_name,
|
|
162
301
|
receiver_kind: :super,
|
|
163
302
|
receiver_name: context.owner,
|
|
164
|
-
file: context.relative_file,
|
|
165
|
-
line: node.location.start_line,
|
|
166
|
-
test: context.test,
|
|
167
303
|
dynamic: false,
|
|
168
|
-
metadata: {
|
|
304
|
+
metadata: {
|
|
305
|
+
'super' => true,
|
|
306
|
+
'zsuper' => node.is_a?(Prism::ForwardingSuperNode),
|
|
307
|
+
'arguments' => node.is_a?(Prism::ForwardingSuperNode) ? incomplete_arguments : call_arguments(node)
|
|
308
|
+
}
|
|
169
309
|
)
|
|
170
310
|
end
|
|
171
311
|
visit_children(node, context)
|
|
172
312
|
end
|
|
173
313
|
|
|
174
314
|
def visit_singleton_class(node, context)
|
|
175
|
-
|
|
315
|
+
owner = singleton_class_owner(node.expression, context)
|
|
316
|
+
unless owner
|
|
317
|
+
visit(node.expression, context)
|
|
318
|
+
record_semantic_blocker(
|
|
319
|
+
:dynamic_singleton_definition,
|
|
320
|
+
node,
|
|
321
|
+
context,
|
|
322
|
+
'singleton class receiver is not statically bounded',
|
|
323
|
+
suggested_action: :make_singleton_receiver_static,
|
|
324
|
+
force_global: true
|
|
325
|
+
)
|
|
326
|
+
return
|
|
327
|
+
end
|
|
176
328
|
|
|
177
329
|
singleton_context = context.dup
|
|
330
|
+
singleton_context.namespace = owner
|
|
331
|
+
singleton_context.owner = owner
|
|
178
332
|
singleton_context.singleton_scope = true
|
|
179
333
|
singleton_context.visibility = :public
|
|
180
334
|
singleton_context.module_function = false
|
|
181
335
|
visit(node.body, singleton_context)
|
|
182
336
|
end
|
|
337
|
+
|
|
338
|
+
def singleton_class_owner(expression, context)
|
|
339
|
+
return context.owner if expression.is_a?(Prism::SelfNode)
|
|
340
|
+
|
|
341
|
+
name = constant_name(expression)
|
|
342
|
+
return unless name
|
|
343
|
+
|
|
344
|
+
resolve_candidate_group(constant_candidates(name, context.lexical_nesting))
|
|
345
|
+
end
|
|
183
346
|
end
|
|
184
347
|
end
|
|
@@ -14,31 +14,40 @@ module Necropsy
|
|
|
14
14
|
data[:line] = node.location.start_line
|
|
15
15
|
instantiated_classes << owner
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
value_arguments = arguments(node.value)
|
|
18
|
+
names = value_arguments.filter_map { |argument| literal_value(argument) if literal_name?(argument) }
|
|
19
|
+
if names.length != value_arguments.length
|
|
20
|
+
record_semantic_blocker(
|
|
21
|
+
:dynamic_generated_methods,
|
|
22
|
+
node.value,
|
|
23
|
+
context,
|
|
24
|
+
"#{node.value.name} has runtime-computed member names",
|
|
25
|
+
suggested_action: :make_member_name_literal,
|
|
26
|
+
scope_owner: owner
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
names.each do |name|
|
|
31
|
+
add_definition(
|
|
32
|
+
symbol_id: "#{owner}##{name}",
|
|
20
33
|
kind: :instance_method,
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
end_line: node.location.end_line,
|
|
34
|
+
source_node: node,
|
|
35
|
+
context: context,
|
|
24
36
|
defined_via: node.value.name == :define ? :data_define : :struct_new,
|
|
25
37
|
owner: owner,
|
|
26
38
|
name: name,
|
|
27
|
-
test: context.test,
|
|
28
39
|
visibility: :public
|
|
29
40
|
)
|
|
30
41
|
next if node.value.name == :define
|
|
31
42
|
|
|
32
|
-
|
|
33
|
-
|
|
43
|
+
add_definition(
|
|
44
|
+
symbol_id: "#{owner}##{name}=",
|
|
34
45
|
kind: :instance_method,
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
end_line: node.location.end_line,
|
|
46
|
+
source_node: node,
|
|
47
|
+
context: context,
|
|
38
48
|
defined_via: :struct_new,
|
|
39
49
|
owner: owner,
|
|
40
50
|
name: "#{name}=",
|
|
41
|
-
test: context.test,
|
|
42
51
|
visibility: :public
|
|
43
52
|
)
|
|
44
53
|
end
|
|
@@ -51,6 +60,7 @@ module Necropsy
|
|
|
51
60
|
block_context.singleton_scope = false
|
|
52
61
|
block_context.visibility = :public
|
|
53
62
|
block_context.module_function = false
|
|
63
|
+
block_context.static_ancestry = false
|
|
54
64
|
visit(node.value.block.body, block_context)
|
|
55
65
|
end
|
|
56
66
|
|
data/lib/necropsy/ast_scanner.rb
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'prism'
|
|
4
|
+
require_relative 'ast_scanner/definition_creation'
|
|
5
|
+
require_relative 'ast_scanner/call_site_creation'
|
|
4
6
|
require_relative 'ast_scanner/traversal'
|
|
5
7
|
require_relative 'ast_scanner/value_definitions'
|
|
6
8
|
require_relative 'ast_scanner/method_definitions'
|
|
@@ -10,9 +12,29 @@ require_relative 'ast_scanner/references'
|
|
|
10
12
|
require_relative 'ast_scanner/ruby_semantics'
|
|
11
13
|
|
|
12
14
|
module Necropsy
|
|
13
|
-
ScanResult = Data.define(
|
|
15
|
+
ScanResult = Data.define(
|
|
16
|
+
:nodes,
|
|
17
|
+
:call_sites,
|
|
18
|
+
:instantiated_classes,
|
|
19
|
+
:uncertainties,
|
|
20
|
+
:class_infos,
|
|
21
|
+
:entrypoint_hints,
|
|
22
|
+
:file_statuses,
|
|
23
|
+
:source_errors,
|
|
24
|
+
:source_domains,
|
|
25
|
+
:scope_diagnostics,
|
|
26
|
+
:method_signatures,
|
|
27
|
+
:semantic_blockers
|
|
28
|
+
) do
|
|
29
|
+
def initialize(nodes:, call_sites:, instantiated_classes:, uncertainties:, class_infos:, entrypoint_hints:,
|
|
30
|
+
file_statuses: {}, source_errors: [], source_domains: {}, scope_diagnostics: {},
|
|
31
|
+
method_signatures: {}, semantic_blockers: [])
|
|
32
|
+
super
|
|
33
|
+
end
|
|
34
|
+
end
|
|
14
35
|
|
|
15
36
|
class AstScanner
|
|
37
|
+
CallTraversal = Data.define(:receiver, :arguments, :block)
|
|
16
38
|
ATTR_MACROS = %i[attr_reader attr_writer attr_accessor].freeze
|
|
17
39
|
DYNAMIC_SENDS = %i[send public_send __send__].freeze
|
|
18
40
|
MODULE_RELATION_MACROS = %i[include prepend extend].freeze
|
|
@@ -20,13 +42,24 @@ module Necropsy
|
|
|
20
42
|
before_action after_action around_action
|
|
21
43
|
before_validation after_validation before_save after_save around_save
|
|
22
44
|
around_validation before_touch after_touch
|
|
45
|
+
before_enqueue around_enqueue after_enqueue before_perform around_perform after_perform
|
|
23
46
|
before_create after_create around_create before_update after_update around_update
|
|
24
47
|
before_destroy after_destroy around_destroy after_commit after_rollback after_initialize after_find
|
|
25
48
|
after_create_commit after_update_commit after_destroy_commit after_save_commit
|
|
26
49
|
validate rescue_from helper_method
|
|
27
50
|
].freeze
|
|
51
|
+
RAILS_GENERATED_METHOD_MACROS = %i[
|
|
52
|
+
enum store store_accessor attribute class_attribute mattr_reader mattr_writer mattr_accessor
|
|
53
|
+
cattr_reader cattr_writer cattr_accessor belongs_to has_one has_many scope
|
|
54
|
+
].freeze
|
|
55
|
+
RAILS_RUNTIME_BLOCK_MACROS = %i[discard_on retry_on stream_for stream_from].freeze
|
|
56
|
+
SIDEKIQ_RUNTIME_BLOCK_MACROS = %i[sidekiq_retries_exhausted sidekiq_retry_in].freeze
|
|
28
57
|
VISIBILITY_MACROS = %i[public protected private public_class_method private_class_method].freeze
|
|
29
58
|
SYMBOL_REFERENCE_CALLS = %i[method respond_to? try try!].freeze
|
|
59
|
+
ANCESTRY_CONTROL_FLOW_TYPES = %i[
|
|
60
|
+
and_node block_node case_match_node case_node else_node for_node if_node in_node lambda_node
|
|
61
|
+
or_node rescue_modifier_node rescue_node until_node when_node while_node
|
|
62
|
+
].freeze
|
|
30
63
|
RAILS_BUILTIN_VALIDATORS = %w[
|
|
31
64
|
absence acceptance allow_blank allow_nil comparison confirmation exclusion format if inclusion length message numericality
|
|
32
65
|
on presence strict uniqueness unless
|
|
@@ -34,8 +67,10 @@ module Necropsy
|
|
|
34
67
|
|
|
35
68
|
Context = Struct.new(
|
|
36
69
|
:namespace,
|
|
70
|
+
:lexical_nesting,
|
|
37
71
|
:owner,
|
|
38
72
|
:current_caller_id,
|
|
73
|
+
:current_method_name,
|
|
39
74
|
:current_kind,
|
|
40
75
|
:root_id,
|
|
41
76
|
:file,
|
|
@@ -44,37 +79,63 @@ module Necropsy
|
|
|
44
79
|
:singleton_scope,
|
|
45
80
|
:visibility,
|
|
46
81
|
:module_function,
|
|
82
|
+
:static_ancestry,
|
|
83
|
+
:flow_result,
|
|
47
84
|
keyword_init: true
|
|
48
85
|
)
|
|
49
86
|
|
|
50
|
-
def initialize(project:, files:)
|
|
87
|
+
def initialize(project:, files:, source_domains: nil, scope_diagnostics: {})
|
|
51
88
|
@project = project
|
|
52
89
|
@files = files
|
|
90
|
+
@source_domains = source_domains || files.to_h { |file| [project.relative_path(file), :analyze] }
|
|
91
|
+
@scope_diagnostics = scope_diagnostics
|
|
53
92
|
@nodes = []
|
|
54
93
|
@call_sites = []
|
|
55
94
|
@instantiated_classes = Set.new
|
|
56
95
|
@uncertainties = Hash.new { |hash, key| hash[key] = [] }
|
|
57
96
|
@class_data = {}
|
|
58
97
|
@entrypoint_hints = []
|
|
98
|
+
@file_statuses = {}
|
|
99
|
+
@source_errors = []
|
|
100
|
+
@definition_ordinals = Hash.new(0)
|
|
101
|
+
@call_site_ordinals = Hash.new(0)
|
|
102
|
+
@module_function_sources = {}
|
|
103
|
+
@deferred_module_functions = {}
|
|
104
|
+
@method_signatures = {}
|
|
105
|
+
@semantic_blockers = []
|
|
59
106
|
@factory_methods = project.config.factory_methods.to_set(&:to_s)
|
|
107
|
+
@convention_rules = ConventionRules.new
|
|
60
108
|
end
|
|
61
109
|
|
|
62
110
|
def scan
|
|
63
|
-
files.each { |file| scan_file(file) }
|
|
111
|
+
files.sort_by { |file| project.relative_path(file) }.each { |file| scan_file(file) }
|
|
112
|
+
resolve_deferred_module_function_sources
|
|
64
113
|
copy_module_function_call_sites
|
|
65
114
|
ScanResult.new(
|
|
66
|
-
nodes: nodes
|
|
115
|
+
nodes: nodes,
|
|
67
116
|
call_sites: call_sites,
|
|
68
117
|
instantiated_classes: instantiated_classes,
|
|
69
118
|
uncertainties: uncertainties,
|
|
70
119
|
class_infos: class_infos,
|
|
71
|
-
entrypoint_hints: entrypoint_hints.uniq
|
|
120
|
+
entrypoint_hints: entrypoint_hints.uniq,
|
|
121
|
+
file_statuses: file_statuses,
|
|
122
|
+
source_errors: source_errors,
|
|
123
|
+
source_domains: source_domains,
|
|
124
|
+
scope_diagnostics: scope_diagnostics,
|
|
125
|
+
method_signatures: method_signatures,
|
|
126
|
+
semantic_blockers: semantic_blockers.sort_by { |blocker| BoundedCanonicalizer.dump(blocker.to_h) }
|
|
72
127
|
)
|
|
73
128
|
end
|
|
74
129
|
|
|
75
130
|
private
|
|
76
131
|
|
|
132
|
+
def handled_call(receiver: true, arguments: true, block: false)
|
|
133
|
+
CallTraversal.new(receiver: receiver, arguments: arguments, block: block)
|
|
134
|
+
end
|
|
135
|
+
|
|
77
136
|
attr_reader :project, :files, :nodes, :call_sites, :instantiated_classes, :uncertainties, :class_data,
|
|
78
|
-
:entrypoint_hints
|
|
137
|
+
:entrypoint_hints, :file_statuses, :source_errors, :definition_ordinals, :module_function_sources,
|
|
138
|
+
:deferred_module_functions, :call_site_ordinals, :source_domains, :scope_diagnostics,
|
|
139
|
+
:method_signatures, :semantic_blockers, :convention_rules
|
|
79
140
|
end
|
|
80
141
|
end
|