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
|
@@ -2,18 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
module Necropsy
|
|
4
4
|
class CallGraph
|
|
5
|
+
include DynamicEvidenceTracking
|
|
6
|
+
include EvidenceStore
|
|
7
|
+
include BlockerMatching
|
|
8
|
+
include ResolutionStore
|
|
9
|
+
|
|
5
10
|
attr_reader :nodes, :call_sites, :instantiated_classes, :entry_points, :profiles, :observation, :class_infos,
|
|
6
|
-
:entrypoint_hints
|
|
11
|
+
:entrypoint_hints, :ambiguity_limit, :file_statuses, :source_errors, :source_domains,
|
|
12
|
+
:scope_diagnostics, :method_signatures
|
|
7
13
|
|
|
8
14
|
def initialize(scan_result, ambiguity_limit: 4)
|
|
9
|
-
@nodes =
|
|
15
|
+
@nodes = DefinitionIndex.new
|
|
10
16
|
@edges = {}
|
|
11
17
|
@incoming_edges = {}
|
|
18
|
+
initialize_evidence_store
|
|
12
19
|
@call_sites = scan_result.call_sites
|
|
13
20
|
@instantiated_classes = scan_result.instantiated_classes.dup
|
|
14
21
|
@class_infos = scan_result.class_infos.to_h { |info| [info.id, info] }
|
|
22
|
+
@direct_subclasses = Hash.new { |hash, key| hash[key] = [] }
|
|
23
|
+
@class_infos.each_value do |info|
|
|
24
|
+
@direct_subclasses[info.superclass] << info.id if info.superclass
|
|
25
|
+
end
|
|
26
|
+
@direct_subclasses.each_value(&:sort!)
|
|
15
27
|
@entrypoint_hints = scan_result.entrypoint_hints
|
|
28
|
+
@file_statuses = scan_result.file_statuses.to_h do |file, status|
|
|
29
|
+
[file.to_s, status.to_sym]
|
|
30
|
+
end
|
|
31
|
+
@source_errors = scan_result.source_errors.dup
|
|
32
|
+
@source_domains = scan_result.source_domains.to_h do |file, domain|
|
|
33
|
+
[file.to_s, domain.to_sym]
|
|
34
|
+
end
|
|
35
|
+
@scope_diagnostics = scan_result.scope_diagnostics.dup
|
|
36
|
+
@method_signatures = scan_result.method_signatures.dup
|
|
16
37
|
@ambiguity_limit = ambiguity_limit
|
|
38
|
+
@blockers = []
|
|
39
|
+
initialize_blocker_indexes
|
|
17
40
|
@entry_points = []
|
|
18
41
|
@profiles = []
|
|
19
42
|
@uncertainties = scan_result.uncertainties.to_h do |node_id, messages|
|
|
@@ -21,38 +44,121 @@ module Necropsy
|
|
|
21
44
|
end
|
|
22
45
|
@dynamic_alive = {}
|
|
23
46
|
@observation = {}
|
|
47
|
+
record_generated_macro_observation(scan_result.nodes)
|
|
48
|
+
initialize_resolution_store
|
|
24
49
|
@descendants = {}
|
|
50
|
+
@rta_instantiated_owner_cache = {}
|
|
51
|
+
@duplicate_blockers_initialized = false
|
|
25
52
|
scan_result.nodes.each { |node| add_node(node) }
|
|
53
|
+
register_duplicate_definition_blockers
|
|
54
|
+
@duplicate_blockers_initialized = true
|
|
55
|
+
register_incomplete_source_blockers
|
|
56
|
+
scan_result.semantic_blockers.each { |blocker| add_blocker(blocker) }
|
|
26
57
|
retain_known_instantiated_classes
|
|
27
58
|
end
|
|
28
59
|
|
|
29
60
|
def add_node(node)
|
|
30
|
-
|
|
61
|
+
existing = nodes.exact(node.graph_id)
|
|
62
|
+
return existing if existing
|
|
31
63
|
|
|
32
64
|
@method_nodes = nil
|
|
33
65
|
@nodes_by_name = nil
|
|
66
|
+
@runtime_nodes_by_name = nil
|
|
34
67
|
@dispatch_cache = nil
|
|
35
68
|
@lookup_chain_cache = nil
|
|
69
|
+
@singleton_lookup_chain_cache = nil
|
|
36
70
|
@owner_ancestor_cache = nil
|
|
37
|
-
|
|
71
|
+
@flow_lookup_cache = nil
|
|
72
|
+
@rta_instantiated_owner_cache = {}
|
|
73
|
+
@dynamic_ancestry_cache = {}
|
|
74
|
+
added = nodes.add(node)
|
|
75
|
+
register_duplicate_definition_blocker(node.symbol_id) if @duplicate_blockers_initialized
|
|
76
|
+
refresh_resolution_derived_state
|
|
77
|
+
added
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def record_generated_macro_observation(scanned_nodes)
|
|
81
|
+
groups = scanned_nodes.select { |node| node.defined_via.to_s.start_with?('rails_') }.group_by do |node|
|
|
82
|
+
[node.file, node.line, node.owner, node.defined_via.to_s]
|
|
83
|
+
end
|
|
84
|
+
return if groups.empty?
|
|
85
|
+
|
|
86
|
+
observation['rails_generated_macros'] = {
|
|
87
|
+
'count' => groups.length,
|
|
88
|
+
'generated_method_count' => groups.values.sum(&:length),
|
|
89
|
+
'macros' => groups.sort_by(&:first).map do |(file, line, owner, macro), nodes|
|
|
90
|
+
{
|
|
91
|
+
'file' => file,
|
|
92
|
+
'line' => line,
|
|
93
|
+
'owner' => owner,
|
|
94
|
+
'macro' => macro.delete_prefix('rails_'),
|
|
95
|
+
'generated_methods' => nodes.map(&:name).uniq.sort
|
|
96
|
+
}
|
|
97
|
+
end
|
|
98
|
+
}
|
|
99
|
+
end
|
|
100
|
+
private :record_generated_macro_observation
|
|
101
|
+
|
|
102
|
+
def add_entry_point(node_id, reason, domain: nil, evidence: nil)
|
|
103
|
+
resolve_definitions(node_id).each do |definition|
|
|
104
|
+
entry = Root.new(
|
|
105
|
+
definition_id: definition.graph_id,
|
|
106
|
+
domain: domain,
|
|
107
|
+
reason: reason,
|
|
108
|
+
evidence: evidence
|
|
109
|
+
)
|
|
110
|
+
entry_points << entry unless entry_points.include?(entry)
|
|
111
|
+
end
|
|
38
112
|
end
|
|
39
113
|
|
|
40
|
-
def
|
|
41
|
-
|
|
114
|
+
def definitions_for(symbol_id)
|
|
115
|
+
nodes.definitions_for(symbol_id)
|
|
116
|
+
end
|
|
42
117
|
|
|
43
|
-
|
|
44
|
-
|
|
118
|
+
def call_sites_for_message(message)
|
|
119
|
+
@call_sites_by_message ||= call_sites.group_by { |site| site.message.to_s }.transform_values(&:freeze).freeze
|
|
120
|
+
@call_sites_by_message.fetch(message.to_s, [])
|
|
45
121
|
end
|
|
46
122
|
|
|
47
|
-
def apply_result(result)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
123
|
+
def apply_result(result, refresh: true)
|
|
124
|
+
staged = transactional_copy
|
|
125
|
+
staged.send(:apply_result!, result, refresh: refresh)
|
|
126
|
+
staged.instance_variables.each do |name|
|
|
127
|
+
instance_variable_set(name, staged.instance_variable_get(name))
|
|
128
|
+
end
|
|
129
|
+
nil
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def apply_result!(result, refresh: true)
|
|
133
|
+
register_result_call_sites(result)
|
|
134
|
+
dynamic_result = dynamic_result?(result)
|
|
135
|
+
edge_matches = result.edge_evidences.map do |edge|
|
|
136
|
+
next apply_dynamic_edge(edge) if dynamic_result
|
|
137
|
+
|
|
138
|
+
add_edge(edge.caller_id, edge.callee_id, edge.evidence)
|
|
139
|
+
end
|
|
140
|
+
alive_matches = result.alive_evidences.map do |alive|
|
|
141
|
+
dynamic_result ? apply_dynamic_alive(alive.node_id, alive.evidence) : add_alive(alive.node_id, alive.evidence)
|
|
142
|
+
end
|
|
143
|
+
register_result_evidences(result)
|
|
51
144
|
result.uncertainties.each do |node_id, messages|
|
|
52
|
-
|
|
53
|
-
|
|
145
|
+
resolved_ids = resolve_definitions(node_id).map(&:graph_id)
|
|
146
|
+
resolved_ids = [node_id] if resolved_ids.empty?
|
|
147
|
+
resolved_ids.each do |resolved_id|
|
|
148
|
+
@uncertainties[resolved_id] ||= []
|
|
149
|
+
@uncertainties[resolved_id].concat(Array(messages))
|
|
150
|
+
end
|
|
54
151
|
end
|
|
152
|
+
Array(result.respond_to?(:blockers) ? result.blockers : []).each { |blocker| add_blocker(blocker) }
|
|
55
153
|
observation.merge!(result.observation) { |_key, left, right| merge_observation(left, right) }
|
|
154
|
+
record_dynamic_evidence(result, alive_matches, edge_matches) if dynamic_result
|
|
155
|
+
register_result_resolutions(result, refresh: refresh)
|
|
156
|
+
refresh_resolution_derived_state if refresh && (!result.respond_to?(:resolutions) || result.resolutions.nil?)
|
|
157
|
+
end
|
|
158
|
+
private :apply_result!
|
|
159
|
+
|
|
160
|
+
def refresh_derived_state
|
|
161
|
+
refresh_resolution_derived_state
|
|
56
162
|
end
|
|
57
163
|
|
|
58
164
|
def add_profile(profile)
|
|
@@ -60,57 +166,159 @@ module Necropsy
|
|
|
60
166
|
end
|
|
61
167
|
|
|
62
168
|
def add_edge(caller_id, callee_id, evidence)
|
|
63
|
-
|
|
169
|
+
callers = resolve_definitions(caller_id)
|
|
170
|
+
callees = resolve_definitions(callee_id)
|
|
171
|
+
if callers.empty? || callees.empty?
|
|
172
|
+
domain = callers.any? && callers.all?(&:test) ? :test : :runtime
|
|
173
|
+
register_evidence(evidence, domain: domain)
|
|
174
|
+
return false
|
|
175
|
+
end
|
|
64
176
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
177
|
+
record_ambiguous_input(:edge_caller, caller_id, callers)
|
|
178
|
+
record_ambiguous_input(:edge_callee, callee_id, callees)
|
|
179
|
+
callers.product(callees).each do |caller, callee|
|
|
180
|
+
add_physical_edge(caller.graph_id, callee.graph_id, evidence)
|
|
181
|
+
end
|
|
182
|
+
true
|
|
70
183
|
end
|
|
71
184
|
|
|
72
185
|
def add_alive(node_id, evidence)
|
|
73
|
-
|
|
186
|
+
definitions = resolve_definitions(node_id)
|
|
187
|
+
if definitions.empty?
|
|
188
|
+
register_evidence(evidence)
|
|
189
|
+
return false
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
record_ambiguous_input(:alive, node_id, definitions)
|
|
193
|
+
definitions.each do |definition|
|
|
194
|
+
evidence_id = register_evidence(evidence, domain: definition.test ? :test : :runtime)
|
|
195
|
+
next unless evidence_id
|
|
74
196
|
|
|
75
|
-
|
|
76
|
-
|
|
197
|
+
@dynamic_alive[definition.graph_id] ||= Set.new
|
|
198
|
+
@dynamic_alive[definition.graph_id] << evidence_id
|
|
199
|
+
end
|
|
77
200
|
true
|
|
78
201
|
end
|
|
79
202
|
|
|
80
203
|
def dynamic_alive?(node_id)
|
|
81
|
-
|
|
204
|
+
resolve_definitions(node_id).any? do |definition|
|
|
205
|
+
@dynamic_alive.fetch(definition.graph_id, Set.new).any? { |evidence_id| evidence_record(evidence_id) }
|
|
206
|
+
end
|
|
82
207
|
end
|
|
83
208
|
|
|
84
|
-
def alive_evidences(node_id)
|
|
85
|
-
|
|
209
|
+
def alive_evidences(node_id, projection: :conservative, scope: nil)
|
|
210
|
+
projection = normalize_projection(projection)
|
|
211
|
+
evidence_ids = resolve_definitions(node_id).flat_map do |definition|
|
|
212
|
+
@dynamic_alive.fetch(definition.graph_id, Set.new).to_a
|
|
213
|
+
end
|
|
214
|
+
projected_evidence_records(evidence_ids.uniq, projection: projection, scope: scope)
|
|
86
215
|
end
|
|
87
216
|
|
|
88
217
|
def dynamic_enabled?
|
|
89
218
|
@dynamic_alive.any?
|
|
90
219
|
end
|
|
91
220
|
|
|
92
|
-
def
|
|
93
|
-
|
|
221
|
+
def runtime_feedback(observed_targets:, max_fixtures: RuntimeFeedback::DEFAULT_FIXTURE_LIMIT)
|
|
222
|
+
RuntimeFeedback.from_graph(
|
|
223
|
+
self,
|
|
224
|
+
observed_targets: observed_targets,
|
|
225
|
+
max_fixtures: max_fixtures
|
|
226
|
+
).call
|
|
94
227
|
end
|
|
95
228
|
|
|
96
|
-
def
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
229
|
+
def performance_counts
|
|
230
|
+
{
|
|
231
|
+
'definitions' => nodes.length,
|
|
232
|
+
'call_sites' => call_sites.length,
|
|
233
|
+
'edges' => edges.length,
|
|
234
|
+
'blockers' => blockers.length,
|
|
235
|
+
'resolution_cache_hits' => @resolution_cache_hits.to_i,
|
|
236
|
+
'resolution_cache_misses' => @resolution_cache_misses.to_i
|
|
237
|
+
}
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def edges_from(node_id, projection: :conservative, scope: nil)
|
|
241
|
+
projection = normalize_projection(projection)
|
|
242
|
+
evidence_ids_by_callee = resolve_definitions(node_id).each_with_object(Hash.new { |hash, key| hash[key] = Set.new }) do |definition, merged|
|
|
243
|
+
@edges.fetch(definition.graph_id, {}).each do |callee_id, evidence_ids|
|
|
244
|
+
merged[callee_id].merge(evidence_ids)
|
|
100
245
|
end
|
|
101
246
|
end
|
|
247
|
+
evidence_ids_by_callee.each_with_object({}) do |(callee_id, evidence_ids), projected|
|
|
248
|
+
records = projected_evidence_records(evidence_ids, projection: projection, scope: scope)
|
|
249
|
+
projected[callee_id] = records unless records.empty?
|
|
250
|
+
end
|
|
102
251
|
end
|
|
103
252
|
|
|
104
|
-
def
|
|
105
|
-
@
|
|
106
|
-
|
|
107
|
-
|
|
253
|
+
def edge_present?(caller_id, callee_id)
|
|
254
|
+
@edges.dig(caller_id, callee_id)&.any? { |evidence_id| evidence_record(evidence_id) }
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def edges(projection: :conservative, scope: nil)
|
|
258
|
+
projection = normalize_projection(projection)
|
|
259
|
+
@edges.flat_map do |caller_id, callees|
|
|
260
|
+
callees.filter_map do |callee_id, evidence_ids|
|
|
261
|
+
evidences = projected_evidence_records(evidence_ids, projection: projection, scope: scope)
|
|
262
|
+
Edge.new(caller_id: caller_id, callee_id: callee_id, evidences: evidences) unless evidences.empty?
|
|
263
|
+
end
|
|
264
|
+
end.sort_by { |edge| [edge.caller_id, edge.callee_id] }
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def edge_relations(projection: :conservative, scope: nil)
|
|
268
|
+
projection = normalize_projection(projection)
|
|
269
|
+
@edges.flat_map do |caller_id, callees|
|
|
270
|
+
callees.filter_map do |callee_id, evidence_ids|
|
|
271
|
+
projected_ids = projected_evidence_ids(evidence_ids, projection: projection, scope: scope)
|
|
272
|
+
next if projected_ids.empty?
|
|
273
|
+
|
|
274
|
+
EdgeRelation.new(
|
|
275
|
+
caller_id: caller_id,
|
|
276
|
+
callee_id: callee_id,
|
|
277
|
+
evidence_ids: projected_ids,
|
|
278
|
+
projection: projection
|
|
279
|
+
)
|
|
280
|
+
end
|
|
281
|
+
end.sort_by { |edge| [edge.caller_id, edge.callee_id] }
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def incoming_edges(node_id, projection: :conservative, scope: nil)
|
|
285
|
+
projection = normalize_projection(projection)
|
|
286
|
+
resolve_definitions(node_id).flat_map do |definition|
|
|
287
|
+
@incoming_edges.fetch(definition.graph_id, {}).filter_map do |caller_id, evidence_ids|
|
|
288
|
+
evidences = projected_evidence_records(evidence_ids, projection: projection, scope: scope)
|
|
289
|
+
Edge.new(caller_id: caller_id, callee_id: definition.graph_id, evidences: evidences) unless evidences.empty?
|
|
290
|
+
end
|
|
291
|
+
end.sort_by { |edge| [edge.caller_id, edge.callee_id] }
|
|
108
292
|
end
|
|
109
293
|
|
|
110
294
|
def uncertainties(node_id = nil)
|
|
111
295
|
return @uncertainties unless node_id
|
|
112
296
|
|
|
113
|
-
|
|
297
|
+
resolved = resolve_definitions(node_id)
|
|
298
|
+
return @uncertainties.fetch(node_id, []) if resolved.empty?
|
|
299
|
+
|
|
300
|
+
resolved.flat_map { |definition| @uncertainties.fetch(definition.graph_id, []) }.uniq
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def incomplete_files
|
|
304
|
+
file_statuses.filter_map { |file, status| file unless status == :complete }.sort
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def analyze_source?(file)
|
|
308
|
+
source_domains.fetch(file.to_s, :analyze) == :analyze
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def source_incompleteness
|
|
312
|
+
{
|
|
313
|
+
'incomplete_files' => incomplete_files.length,
|
|
314
|
+
'files' => incomplete_files.map do |file|
|
|
315
|
+
{
|
|
316
|
+
'file' => file,
|
|
317
|
+
'status' => file_statuses.fetch(file).to_s,
|
|
318
|
+
'errors' => source_errors.select { |error| error.file == file }.map(&:to_h)
|
|
319
|
+
}
|
|
320
|
+
end
|
|
321
|
+
}
|
|
114
322
|
end
|
|
115
323
|
|
|
116
324
|
def method_nodes
|
|
@@ -126,8 +334,20 @@ module Necropsy
|
|
|
126
334
|
end
|
|
127
335
|
|
|
128
336
|
def descendants_of(owner)
|
|
129
|
-
@descendants[owner] ||=
|
|
130
|
-
|
|
337
|
+
@descendants[owner] ||= begin
|
|
338
|
+
descendants = []
|
|
339
|
+
queue = class_infos.key?(owner) ? [owner] : []
|
|
340
|
+
seen = Set.new
|
|
341
|
+
head = 0
|
|
342
|
+
while head < queue.length
|
|
343
|
+
candidate = queue.fetch(head)
|
|
344
|
+
head += 1
|
|
345
|
+
next unless seen.add?(candidate)
|
|
346
|
+
|
|
347
|
+
descendants << candidate
|
|
348
|
+
queue.concat(@direct_subclasses.fetch(candidate, []))
|
|
349
|
+
end
|
|
350
|
+
descendants.freeze
|
|
131
351
|
end
|
|
132
352
|
end
|
|
133
353
|
|
|
@@ -138,18 +358,23 @@ module Necropsy
|
|
|
138
358
|
(info.prepends + info.includes + info.extends).uniq
|
|
139
359
|
end
|
|
140
360
|
|
|
141
|
-
def candidate_nodes(message)
|
|
142
|
-
nodes_by_name
|
|
361
|
+
def candidate_nodes(message, domain: nil)
|
|
362
|
+
index = domain&.to_sym == :runtime ? runtime_nodes_by_name : nodes_by_name
|
|
363
|
+
index.fetch(message, [])
|
|
143
364
|
end
|
|
144
365
|
|
|
145
|
-
def ambiguous_fallback_candidates(message)
|
|
146
|
-
candidates = candidate_nodes(message)
|
|
366
|
+
def ambiguous_fallback_candidates(message, domain: nil)
|
|
367
|
+
candidates = candidate_nodes(message, domain: domain)
|
|
147
368
|
return candidates if candidates.one?
|
|
148
369
|
return [] if candidates.size > @ambiguity_limit
|
|
149
370
|
|
|
150
371
|
candidates
|
|
151
372
|
end
|
|
152
373
|
|
|
374
|
+
def ambiguity_exceeded?(message, domain: nil)
|
|
375
|
+
candidate_nodes(message, domain: domain).size > ambiguity_limit
|
|
376
|
+
end
|
|
377
|
+
|
|
153
378
|
def ambiguous_resolution?
|
|
154
379
|
@ambiguity_limit > 1
|
|
155
380
|
end
|
|
@@ -165,11 +390,182 @@ module Necropsy
|
|
|
165
390
|
end
|
|
166
391
|
|
|
167
392
|
def resolve_call_site(site, rta: false)
|
|
168
|
-
candidates = rta ? rta_candidates_for_receiver(site) :
|
|
393
|
+
candidates = rta ? rta_candidates_for_receiver(site) : method_lookup(site).targets
|
|
169
394
|
candidates = candidates.select { |node| rta_candidate?(node, site) } if rta
|
|
170
395
|
candidates
|
|
171
396
|
end
|
|
172
397
|
|
|
398
|
+
def cha_method_lookup(site)
|
|
399
|
+
primary = method_lookup(site)
|
|
400
|
+
return primary if primary.complete?
|
|
401
|
+
|
|
402
|
+
lookups = case site.receiver_kind
|
|
403
|
+
when :constant
|
|
404
|
+
receiver_candidates(site).map { |owner| canonical_receiver_lookup(site, owner, :constant) }
|
|
405
|
+
when :instance
|
|
406
|
+
receiver_candidates(site).flat_map do |owner|
|
|
407
|
+
descendants_of(owner).map { |candidate| canonical_receiver_lookup(site, candidate, :instance) }
|
|
408
|
+
end
|
|
409
|
+
else
|
|
410
|
+
[]
|
|
411
|
+
end
|
|
412
|
+
targets = [primary, *lookups].flat_map(&:targets).uniq(&:graph_id).sort_by(&:graph_id)
|
|
413
|
+
chain = [primary, *lookups].flat_map(&:lookup_chain).uniq
|
|
414
|
+
incomplete_method_lookup(targets, chain, 'cha_canonical_lookup')
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def residual_scope_for(site)
|
|
418
|
+
return UnknownScope.new(scope_kind: :message, scope_value: site.message, match: :exact) if site.receiver_kind == :unknown
|
|
419
|
+
|
|
420
|
+
owners = receiver_candidates(site)
|
|
421
|
+
owners = [nodes.exact(site.caller_id)&.owner].compact if owners.empty?
|
|
422
|
+
return UnknownScope.new(scope_kind: :message, scope_value: site.message, match: :exact) if owners.empty?
|
|
423
|
+
|
|
424
|
+
UnknownScope.new(scope_kind: :owner, scope_value: owners.sort, match: :exact)
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
def method_lookup(site)
|
|
428
|
+
return fallback_method_lookup(site, reason: 'dynamic_message') if site.dynamic
|
|
429
|
+
return physical_target_lookup(site) if physical_target_id(site)
|
|
430
|
+
return unproven_initialize_lookup(site) if unproven_initialize_dispatch?(site)
|
|
431
|
+
return callable_method_lookup(site) if flow_callable?(site)
|
|
432
|
+
return flow_instance_method_lookup(site) if flow_instance_types(site)
|
|
433
|
+
|
|
434
|
+
case site.receiver_kind
|
|
435
|
+
when :constant
|
|
436
|
+
owner = resolved_receiver_owner(site)
|
|
437
|
+
ordered_method_lookup(
|
|
438
|
+
site,
|
|
439
|
+
singleton_lookup_entries(owner),
|
|
440
|
+
reason: 'singleton_lookup',
|
|
441
|
+
completeness_entries: [[owner, '.']]
|
|
442
|
+
)
|
|
443
|
+
when :instance
|
|
444
|
+
owner = resolved_receiver_owner(site)
|
|
445
|
+
ordered_method_lookup(
|
|
446
|
+
site,
|
|
447
|
+
instance_lookup_entries(owner),
|
|
448
|
+
reason: 'instance_lookup',
|
|
449
|
+
completeness_entries: [[owner, '#']]
|
|
450
|
+
)
|
|
451
|
+
when :self, :implicit
|
|
452
|
+
self_method_lookup(site)
|
|
453
|
+
when :super
|
|
454
|
+
super_method_lookup(site)
|
|
455
|
+
else
|
|
456
|
+
fallback_method_lookup(site, reason: 'unknown_receiver')
|
|
457
|
+
end
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def flow_instance_method_lookup(site)
|
|
461
|
+
@flow_lookup_cache ||= {}
|
|
462
|
+
cache_key = [site.call_site_id, flow_instance_types(site)]
|
|
463
|
+
return @flow_lookup_cache[cache_key] if @flow_lookup_cache.key?(cache_key)
|
|
464
|
+
|
|
465
|
+
results = flow_instance_types(site).map do |owner|
|
|
466
|
+
ordered_method_lookup(
|
|
467
|
+
site,
|
|
468
|
+
instance_lookup_entries(owner),
|
|
469
|
+
reason: 'flow_instance_lookup',
|
|
470
|
+
completeness_entries: [[owner, '#']]
|
|
471
|
+
)
|
|
472
|
+
end
|
|
473
|
+
result = results.first if results.one?
|
|
474
|
+
return @flow_lookup_cache[cache_key] = result if result
|
|
475
|
+
|
|
476
|
+
@flow_lookup_cache[cache_key] = merge_flow_method_lookups(results)
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def physical_target_id(site)
|
|
480
|
+
hash_value(site.metadata, 'physical_target_definition_id')
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
def physical_target_lookup(site)
|
|
484
|
+
target = nodes.exact(physical_target_id(site).to_s)
|
|
485
|
+
return incomplete_method_lookup([], [], 'physical_target_missing') unless target
|
|
486
|
+
|
|
487
|
+
MethodLookup.new(
|
|
488
|
+
targets: [target],
|
|
489
|
+
status: :complete,
|
|
490
|
+
lookup_chain: [target.owner],
|
|
491
|
+
reason: 'physical_definition_relation'
|
|
492
|
+
)
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
def merge_flow_method_lookups(results)
|
|
496
|
+
targets = results.flat_map(&:targets).uniq(&:graph_id).sort_by(&:graph_id)
|
|
497
|
+
chain = results.flat_map(&:lookup_chain).uniq
|
|
498
|
+
return incomplete_method_lookup(targets, chain, 'flow_instance_lookup_incomplete') unless results.all?(&:complete?)
|
|
499
|
+
|
|
500
|
+
accepted_ids = targets.to_set(&:graph_id)
|
|
501
|
+
rejections = results.flat_map(&:rejected_targets).reject do |rejection|
|
|
502
|
+
accepted_ids.include?(rejection.definition_id)
|
|
503
|
+
end.uniq { |rejection| [rejection.definition_id, rejection.reason] }
|
|
504
|
+
MethodLookup.new(
|
|
505
|
+
targets: targets,
|
|
506
|
+
status: :complete,
|
|
507
|
+
rejected_targets: rejections,
|
|
508
|
+
lookup_chain: chain,
|
|
509
|
+
reason: 'flow_instance_lookup'
|
|
510
|
+
)
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
def flow_instance_types(site)
|
|
514
|
+
fact = site.metadata['receiver_value_fact'] || site.metadata[:receiver_value_fact]
|
|
515
|
+
return nil unless fact.is_a?(Hash)
|
|
516
|
+
return nil unless hash_value(fact, 'kind').to_s == 'instance_types'
|
|
517
|
+
return nil unless hash_value(fact, 'exact')
|
|
518
|
+
|
|
519
|
+
values = Array(hash_value(fact, 'values')).map(&:to_s).reject(&:empty?).uniq.sort
|
|
520
|
+
return nil if values.empty?
|
|
521
|
+
return nil unless values.all? { |owner| constructor_dispatch_exact?(owner, site) }
|
|
522
|
+
|
|
523
|
+
values
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
def unproven_initialize_dispatch?(site)
|
|
527
|
+
metadata = site.metadata
|
|
528
|
+
return false unless hash_value(metadata, 'implicit_from').to_s == 'new'
|
|
529
|
+
|
|
530
|
+
!constructor_dispatch_exact?(resolved_receiver_owner(site), site)
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
def unproven_initialize_lookup(site)
|
|
534
|
+
owner = resolved_receiver_owner(site)
|
|
535
|
+
incomplete_method_lookup([], instance_lookup_entries(owner), 'constructor_dispatch_unproven')
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
def constructor_dispatch_exact?(owner, site)
|
|
539
|
+
info = class_info(owner)
|
|
540
|
+
return false unless info && !info.dynamic
|
|
541
|
+
return false if dynamic_ancestry_uncertain?(site)
|
|
542
|
+
|
|
543
|
+
entries = singleton_lookup_entries(owner)
|
|
544
|
+
return false if dynamic_lookup_chain?(entries)
|
|
545
|
+
return false if entries.any? { |candidate, separator| definitions_for("#{candidate}#{separator}new").any? }
|
|
546
|
+
|
|
547
|
+
!constructor_mutation_blocker?(owner, entries, site)
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
def constructor_mutation_blocker?(owner, entries, site)
|
|
551
|
+
owners = [owner, *entries.map(&:first)].to_set
|
|
552
|
+
@blockers.any? do |blocker|
|
|
553
|
+
next false unless %i[dynamic_ancestry unsupported_refinement variable_eval].include?(blocker.kind)
|
|
554
|
+
next false unless blocker.caller_domain == :runtime || site.test
|
|
555
|
+
|
|
556
|
+
blocker.scope_kind == :global || (blocker.scope_kind == :owner && owners.include?(blocker.scope_value))
|
|
557
|
+
end
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
def flow_callable?(site)
|
|
561
|
+
fact = site.metadata['receiver_value_fact'] || site.metadata[:receiver_value_fact]
|
|
562
|
+
fact.is_a?(Hash) && hash_value(fact, 'kind').to_s == 'callable_set' && hash_value(fact, 'exact') == true
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
def callable_method_lookup(_site)
|
|
566
|
+
incomplete_method_lookup([], [], 'callable_invocation')
|
|
567
|
+
end
|
|
568
|
+
|
|
173
569
|
def retain_rta_candidates(candidates, site)
|
|
174
570
|
candidates.select { |node| rta_candidate?(node, site) }
|
|
175
571
|
end
|
|
@@ -184,104 +580,646 @@ module Necropsy
|
|
|
184
580
|
end
|
|
185
581
|
|
|
186
582
|
@edges.each_value do |callees|
|
|
187
|
-
callees.each do |callee_id,
|
|
188
|
-
|
|
189
|
-
|
|
583
|
+
callees.each do |callee_id, evidence_ids|
|
|
584
|
+
evidence_ids.delete_if do |evidence_id|
|
|
585
|
+
item = evidence_record(evidence_id)
|
|
586
|
+
next false unless item
|
|
587
|
+
|
|
588
|
+
producer = item.producer || item.analyzer
|
|
589
|
+
next false unless %w[name_resolution cha].include?(producer.to_s)
|
|
190
590
|
|
|
191
591
|
key = call_site_key(item.metadata)
|
|
192
592
|
analyzed_keys.include?(key) && !allowed[key].include?(callee_id)
|
|
193
593
|
end
|
|
194
594
|
end
|
|
195
|
-
callees.delete_if { |_callee_id,
|
|
595
|
+
callees.delete_if { |_callee_id, evidence_ids| evidence_ids.empty? }
|
|
196
596
|
end
|
|
197
597
|
@edges.delete_if { |_caller_id, callees| callees.empty? }
|
|
198
598
|
rebuild_incoming_edges
|
|
599
|
+
refresh_resolution_derived_state
|
|
199
600
|
end
|
|
200
601
|
|
|
201
602
|
def fallback_resolution?(site, resolved: nil)
|
|
202
603
|
resolved ||= resolve_call_site(site)
|
|
203
604
|
return false if resolved.empty?
|
|
204
605
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
receiver_candidates(site).none? { |name| nodes.key?("#{name}##{site.message}") }
|
|
210
|
-
when :implicit
|
|
211
|
-
same_owner_candidates(site).empty?
|
|
212
|
-
when :unknown
|
|
213
|
-
true
|
|
214
|
-
else
|
|
215
|
-
false
|
|
216
|
-
end
|
|
606
|
+
lookup = method_lookup(site)
|
|
607
|
+
domain = site.test ? :test : :runtime
|
|
608
|
+
fallback = ambiguous_fallback_candidates(site.message, domain: domain)
|
|
609
|
+
!lookup.complete? && resolved.map(&:graph_id).sort == fallback.map(&:graph_id).sort
|
|
217
610
|
end
|
|
218
611
|
|
|
219
612
|
def to_h
|
|
220
613
|
{
|
|
221
614
|
'nodes' => nodes.values.map(&:to_h),
|
|
615
|
+
'call_sites' => call_sites.map(&:to_h),
|
|
222
616
|
'edges' => edges.map(&:to_h),
|
|
617
|
+
'edge_projection' => 'conservative',
|
|
618
|
+
'edge_relations' => edge_relations.map(&:to_h),
|
|
619
|
+
'evidence_records' => evidence_records.map(&:to_h),
|
|
620
|
+
'evidence_collisions' => evidence_collisions,
|
|
223
621
|
'entry_points' => entry_points.map(&:to_h),
|
|
224
622
|
'class_infos' => class_infos.values.map(&:to_h),
|
|
225
623
|
'instantiated_classes' => instantiated_classes.to_a.sort,
|
|
226
624
|
'profiles' => profiles.map(&:to_h),
|
|
625
|
+
'resolutions' => resolution_records.map(&:to_h),
|
|
626
|
+
'resolution_conflicts' => resolution_conflicts,
|
|
627
|
+
'blockers' => blockers.map(&:to_h),
|
|
628
|
+
'file_statuses' => file_statuses.transform_values(&:to_s),
|
|
629
|
+
'source_errors' => source_errors.map(&:to_h),
|
|
630
|
+
'source_domains' => source_domains.transform_values(&:to_s),
|
|
631
|
+
'scope_diagnostics' => scope_diagnostics,
|
|
227
632
|
'observation' => observation
|
|
228
633
|
}
|
|
229
634
|
end
|
|
230
635
|
|
|
231
636
|
private
|
|
232
637
|
|
|
233
|
-
def
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
638
|
+
def runtime_nodes_by_name
|
|
639
|
+
@runtime_nodes_by_name ||= method_nodes.reject(&:test).group_by(&:name).freeze
|
|
640
|
+
end
|
|
641
|
+
|
|
642
|
+
def canonical_receiver_lookup(site, owner, kind)
|
|
643
|
+
entries = kind == :constant ? singleton_lookup_entries(owner) : instance_lookup_entries(owner)
|
|
644
|
+
separator = kind == :constant ? '.' : '#'
|
|
645
|
+
scoped_site = site.with(
|
|
646
|
+
receiver_kind: kind,
|
|
647
|
+
receiver_name: owner,
|
|
648
|
+
metadata: site.metadata.merge('receiver_candidates' => [owner])
|
|
649
|
+
)
|
|
650
|
+
ordered_method_lookup(
|
|
651
|
+
scoped_site,
|
|
652
|
+
entries,
|
|
653
|
+
reason: "cha_#{kind}_lookup",
|
|
654
|
+
completeness_entries: [[owner, separator]]
|
|
655
|
+
)
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
def transactional_copy
|
|
659
|
+
copy = dup
|
|
660
|
+
memo = {}.compare_by_identity
|
|
661
|
+
instance_variables.each do |name|
|
|
662
|
+
value = instance_variable_get(name)
|
|
663
|
+
copy.instance_variable_set(name, duplicate_transaction_value(value, memo))
|
|
664
|
+
end
|
|
665
|
+
copy
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
def duplicate_transaction_value(value, memo)
|
|
669
|
+
return memo.fetch(value) if memo.key?(value)
|
|
670
|
+
|
|
671
|
+
case value
|
|
672
|
+
when Hash
|
|
673
|
+
duplicate_transaction_hash(value, memo)
|
|
674
|
+
when Array
|
|
675
|
+
duplicate_transaction_array(value, memo)
|
|
676
|
+
when Set
|
|
677
|
+
duplicate_transaction_set(value, memo)
|
|
249
678
|
else
|
|
250
|
-
|
|
679
|
+
value
|
|
680
|
+
end
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
def duplicate_transaction_hash(value, memo)
|
|
684
|
+
copy = {}
|
|
685
|
+
copy.compare_by_identity if value.compare_by_identity?
|
|
686
|
+
copy.default_proc = value.default_proc if value.default_proc
|
|
687
|
+
copy.default = value.default unless value.default_proc
|
|
688
|
+
memo[value] = copy
|
|
689
|
+
value.each { |key, item| copy[key] = duplicate_transaction_value(item, memo) }
|
|
690
|
+
copy
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
def duplicate_transaction_array(value, memo)
|
|
694
|
+
copy = []
|
|
695
|
+
memo[value] = copy
|
|
696
|
+
value.each { |item| copy << duplicate_transaction_value(item, memo) }
|
|
697
|
+
copy
|
|
698
|
+
end
|
|
699
|
+
|
|
700
|
+
def duplicate_transaction_set(value, memo)
|
|
701
|
+
copy = Set.new
|
|
702
|
+
memo[value] = copy
|
|
703
|
+
value.each { |item| copy.add(duplicate_transaction_value(item, memo)) }
|
|
704
|
+
copy
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
def register_result_evidences(result)
|
|
708
|
+
records = result.respond_to?(:evidences) ? result.evidences : []
|
|
709
|
+
Array(records).each do |evidence|
|
|
710
|
+
record = evidence_with_identity(evidence)
|
|
711
|
+
register_evidence(record) unless evidence_payload_registered?(record)
|
|
712
|
+
end
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
def register_result_call_sites(result)
|
|
716
|
+
sites = result.respond_to?(:derived_call_sites) ? result.derived_call_sites : []
|
|
717
|
+
Array(sites).each do |site|
|
|
718
|
+
raise TypeError, 'derived call sites must be CallSite values' unless site.is_a?(CallSite)
|
|
719
|
+
|
|
720
|
+
existing = call_sites.select { |candidate| candidate.call_site_id == site.call_site_id }
|
|
721
|
+
next if existing.include?(site)
|
|
722
|
+
raise Error, "Conflicting derived call site identity: #{site.call_site_id}" if existing.any?
|
|
723
|
+
|
|
724
|
+
call_sites << site
|
|
725
|
+
@call_sites_by_id[site.call_site_id] = [site]
|
|
726
|
+
@call_sites_by_message = nil
|
|
727
|
+
end
|
|
728
|
+
end
|
|
729
|
+
|
|
730
|
+
def register_duplicate_definition_blockers
|
|
731
|
+
method_nodes.reject(&:test).map(&:symbol_id).uniq.sort.each do |symbol_id|
|
|
732
|
+
register_duplicate_definition_blocker(symbol_id)
|
|
733
|
+
end
|
|
734
|
+
end
|
|
735
|
+
|
|
736
|
+
def register_duplicate_definition_blocker(symbol_id)
|
|
737
|
+
definitions = definitions_for(symbol_id).reject(&:test)
|
|
738
|
+
remove_blockers_matching do |blocker|
|
|
739
|
+
blocker.kind == :duplicate_definition && blocker.scope_kind == :symbol && blocker.scope_value == symbol_id
|
|
740
|
+
end
|
|
741
|
+
return unless definitions.length > 1
|
|
742
|
+
|
|
743
|
+
add_blocker(Blocker.new(
|
|
744
|
+
kind: :duplicate_definition,
|
|
745
|
+
scope_kind: :symbol,
|
|
746
|
+
scope_value: symbol_id,
|
|
747
|
+
source: :definition_index,
|
|
748
|
+
reason: 'Multiple production definitions have no proven activation order',
|
|
749
|
+
suggested_action: :review_definitions,
|
|
750
|
+
metadata: {
|
|
751
|
+
'caller_domain' => 'runtime',
|
|
752
|
+
'definition_count' => definitions.length,
|
|
753
|
+
'definition_ids' => definitions.map(&:graph_id).sort,
|
|
754
|
+
'locations' => definitions.map do |definition|
|
|
755
|
+
{
|
|
756
|
+
'definition_id' => definition.graph_id,
|
|
757
|
+
'file' => definition.file,
|
|
758
|
+
'line' => definition.line
|
|
759
|
+
}
|
|
760
|
+
end
|
|
761
|
+
}
|
|
762
|
+
))
|
|
763
|
+
end
|
|
764
|
+
|
|
765
|
+
def apply_dynamic_edge(edge)
|
|
766
|
+
caller = resolve_dynamic_reference(edge.caller_id)
|
|
767
|
+
callee = resolve_dynamic_reference(edge.callee_id)
|
|
768
|
+
record_dynamic_ambiguous_input(:edge_caller, caller)
|
|
769
|
+
record_dynamic_ambiguous_input(:edge_callee, callee)
|
|
770
|
+
mark_dynamic_alive(caller, edge.evidence)
|
|
771
|
+
mark_dynamic_alive(callee, edge.evidence)
|
|
772
|
+
materialized = precise_dynamic_resolution?(caller) && precise_dynamic_resolution?(callee)
|
|
773
|
+
if materialized
|
|
774
|
+
add_physical_edge(caller.fetch(:definitions).first.graph_id, callee.fetch(:definitions).first.graph_id,
|
|
775
|
+
edge.evidence)
|
|
776
|
+
end
|
|
777
|
+
register_evidence(edge.evidence) if caller.fetch(:definitions).empty? && callee.fetch(:definitions).empty?
|
|
778
|
+
|
|
779
|
+
{ caller: caller, callee: callee, materialized: materialized }
|
|
780
|
+
end
|
|
781
|
+
|
|
782
|
+
def apply_dynamic_alive(reference, evidence)
|
|
783
|
+
resolution = resolve_dynamic_reference(reference)
|
|
784
|
+
record_dynamic_ambiguous_input(:alive, resolution)
|
|
785
|
+
mark_dynamic_alive(resolution, evidence)
|
|
786
|
+
register_evidence(evidence) if resolution.fetch(:definitions).empty?
|
|
787
|
+
resolution
|
|
788
|
+
end
|
|
789
|
+
|
|
790
|
+
def mark_dynamic_alive(resolution, evidence)
|
|
791
|
+
resolution.fetch(:definitions).each do |definition|
|
|
792
|
+
evidence_id = register_evidence(evidence, domain: definition.test ? :test : :runtime)
|
|
793
|
+
next unless evidence_id
|
|
794
|
+
|
|
795
|
+
@dynamic_alive[definition.graph_id] ||= Set.new
|
|
796
|
+
@dynamic_alive[definition.graph_id] << evidence_id
|
|
797
|
+
end
|
|
798
|
+
end
|
|
799
|
+
|
|
800
|
+
def precise_dynamic_resolution?(resolution)
|
|
801
|
+
%i[exact unique].include?(resolution.fetch(:status))
|
|
802
|
+
end
|
|
803
|
+
|
|
804
|
+
def resolve_dynamic_reference(reference)
|
|
805
|
+
normalized = normalize_dynamic_reference(reference)
|
|
806
|
+
return resolve_legacy_dynamic_reference(normalized) if normalized.key?('identifier')
|
|
807
|
+
return dynamic_resolution(normalized, :missing, []) unless normalized['symbol_id']
|
|
808
|
+
|
|
809
|
+
definition = nodes.exact(normalized['definition_id']) if normalized['definition_id']
|
|
810
|
+
exact_match = definition && dynamic_reference_matches?(definition, normalized)
|
|
811
|
+
return dynamic_resolution(normalized, :exact, [definition]) if exact_match
|
|
812
|
+
|
|
813
|
+
resolve_dynamic_reference_hints(normalized, definition_id_supplied: normalized.key?('definition_id'))
|
|
814
|
+
end
|
|
815
|
+
|
|
816
|
+
def normalize_dynamic_reference(reference)
|
|
817
|
+
return { 'identifier' => reference.to_s } unless reference.is_a?(Hash)
|
|
818
|
+
|
|
819
|
+
normalized = {}
|
|
820
|
+
%w[definition_id symbol_id file].each do |key|
|
|
821
|
+
value = reference[key] || reference[key.to_sym]
|
|
822
|
+
normalized[key] = value.to_s unless value.nil? || value.to_s.empty?
|
|
823
|
+
end
|
|
824
|
+
line = reference['line'] || reference[:line]
|
|
825
|
+
normalized['line'] = line.to_i unless line.nil?
|
|
826
|
+
normalized
|
|
827
|
+
end
|
|
828
|
+
|
|
829
|
+
def resolve_legacy_dynamic_reference(normalized)
|
|
830
|
+
lookup = nodes.lookup(normalized.fetch('identifier'))
|
|
831
|
+
dynamic_resolution(normalized, lookup.status, lookup.definitions)
|
|
832
|
+
end
|
|
833
|
+
|
|
834
|
+
def resolve_dynamic_reference_hints(normalized, definition_id_supplied:)
|
|
835
|
+
symbol_id = normalized['symbol_id']
|
|
836
|
+
location_supplied = normalized.key?('file') || normalized.key?('line')
|
|
837
|
+
return dynamic_resolution(normalized, :missing, []) unless symbol_id
|
|
838
|
+
return dynamic_resolution(normalized, :missing, []) if definition_id_supplied && !location_supplied
|
|
839
|
+
|
|
840
|
+
definitions = definitions_for(symbol_id)
|
|
841
|
+
definitions = definitions.select { |node| node.file == normalized['file'] } if normalized.key?('file')
|
|
842
|
+
definitions = definitions.select { |node| node.line.to_i == normalized['line'] } if normalized.key?('line')
|
|
843
|
+
status = if definitions.empty?
|
|
844
|
+
:missing
|
|
845
|
+
elsif definitions.one?
|
|
846
|
+
:unique
|
|
847
|
+
else
|
|
848
|
+
:ambiguous
|
|
849
|
+
end
|
|
850
|
+
dynamic_resolution(normalized, status, definitions)
|
|
851
|
+
end
|
|
852
|
+
|
|
853
|
+
def dynamic_reference_matches?(definition, normalized)
|
|
854
|
+
return false if normalized['symbol_id'] && definition.symbol_id != normalized['symbol_id']
|
|
855
|
+
return false if normalized['file'] && definition.file != normalized['file']
|
|
856
|
+
return false if normalized.key?('line') && definition.line.to_i != normalized['line']
|
|
857
|
+
|
|
858
|
+
true
|
|
859
|
+
end
|
|
860
|
+
|
|
861
|
+
def dynamic_resolution(reference, status, definitions)
|
|
862
|
+
{ reference: reference.freeze, status: status, definitions: definitions.freeze }.freeze
|
|
863
|
+
end
|
|
864
|
+
|
|
865
|
+
def record_dynamic_ambiguous_input(kind, resolution)
|
|
866
|
+
return unless resolution.fetch(:status) == :ambiguous
|
|
867
|
+
|
|
868
|
+
record_definition_ambiguity(
|
|
869
|
+
kind,
|
|
870
|
+
{ 'reference' => resolution.fetch(:reference) },
|
|
871
|
+
resolution.fetch(:definitions)
|
|
872
|
+
)
|
|
873
|
+
end
|
|
874
|
+
|
|
875
|
+
def register_incomplete_source_blockers
|
|
876
|
+
incomplete_files.each do |file|
|
|
877
|
+
errors = source_errors.select { |error| error.file == file }
|
|
878
|
+
first_error = errors.first
|
|
879
|
+
status = file_statuses.fetch(file)
|
|
880
|
+
domain = source_domain(file)
|
|
881
|
+
metadata = {
|
|
882
|
+
'file' => file,
|
|
883
|
+
'line' => first_error&.line || 1,
|
|
884
|
+
'status' => status.to_s,
|
|
885
|
+
'caller_domain' => domain.to_s,
|
|
886
|
+
'source_errors' => errors.map(&:to_h)
|
|
887
|
+
}
|
|
888
|
+
add_blocker(Blocker.new(
|
|
889
|
+
kind: :parse_incomplete,
|
|
890
|
+
scope_kind: :file,
|
|
891
|
+
scope_value: file,
|
|
892
|
+
source: first_error || :ast_scanner,
|
|
893
|
+
reason: "Source file was #{status}; dead-code conclusions are incomplete",
|
|
894
|
+
metadata: metadata
|
|
895
|
+
))
|
|
896
|
+
next unless domain == :runtime
|
|
897
|
+
|
|
898
|
+
add_blocker(Blocker.new(
|
|
899
|
+
kind: :parse_incomplete,
|
|
900
|
+
scope_kind: :global,
|
|
901
|
+
scope_value: '*',
|
|
902
|
+
source: first_error || :ast_scanner,
|
|
903
|
+
reason: "Runtime source file was #{status}; missing calls can affect any dead-code conclusion",
|
|
904
|
+
metadata: metadata.merge('impact' => 'missing_outgoing_calls')
|
|
905
|
+
))
|
|
251
906
|
end
|
|
252
907
|
end
|
|
253
908
|
|
|
909
|
+
def source_domain(file)
|
|
910
|
+
source_nodes = nodes.values.select { |node| node.file == file }
|
|
911
|
+
return file.start_with?('spec/', 'test/') ? :test : :runtime if source_nodes.empty?
|
|
912
|
+
|
|
913
|
+
source_nodes.all?(&:test) ? :test : :runtime
|
|
914
|
+
end
|
|
915
|
+
|
|
254
916
|
def rta_candidates_for_receiver(site)
|
|
255
|
-
exact =
|
|
917
|
+
exact = method_lookup(site).targets
|
|
256
918
|
return exact unless exact.empty?
|
|
257
919
|
|
|
258
|
-
candidate_nodes(site.message)
|
|
920
|
+
candidate_nodes(site.message, domain: site.test ? :test : :runtime)
|
|
259
921
|
end
|
|
260
922
|
|
|
261
|
-
def
|
|
262
|
-
caller = nodes
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
923
|
+
def self_method_lookup(site)
|
|
924
|
+
caller = nodes.exact(site.caller_id)
|
|
925
|
+
owner = caller&.owner || site.receiver_name
|
|
926
|
+
return fallback_method_lookup(site, reason: 'unknown_self_owner') unless owner
|
|
927
|
+
|
|
928
|
+
singleton = caller&.kind == :singleton_method || !caller&.method?
|
|
929
|
+
entries = singleton ? singleton_lookup_entries(owner) : instance_lookup_entries(owner)
|
|
930
|
+
separator = singleton ? '.' : '#'
|
|
931
|
+
result = ordered_method_lookup(
|
|
932
|
+
site,
|
|
933
|
+
entries,
|
|
934
|
+
reason: singleton ? 'singleton_self_lookup' : 'instance_self_lookup',
|
|
935
|
+
completeness_entries: [[owner, separator]]
|
|
936
|
+
)
|
|
937
|
+
return result unless result.unknown?
|
|
938
|
+
return result if singleton
|
|
939
|
+
|
|
940
|
+
fallback_method_lookup(site, reason: 'self_lookup_fallback')
|
|
270
941
|
end
|
|
271
942
|
|
|
272
|
-
def
|
|
273
|
-
caller = nodes
|
|
274
|
-
return
|
|
943
|
+
def super_method_lookup(site)
|
|
944
|
+
caller = nodes.exact(site.caller_id)
|
|
945
|
+
return fallback_method_lookup(site, reason: 'unknown_super_owner') unless caller&.owner
|
|
275
946
|
|
|
276
947
|
separator = caller.kind == :singleton_method ? '.' : '#'
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
948
|
+
info = class_info(caller.owner)
|
|
949
|
+
return module_super_method_lookup(site, caller, separator) if info&.kind == :module && separator == '#'
|
|
950
|
+
|
|
951
|
+
entries = separator == '.' ? singleton_lookup_entries(caller.owner) : instance_lookup_entries(caller.owner)
|
|
952
|
+
current_index = entries.index([caller.owner, separator])
|
|
953
|
+
return incomplete_method_lookup([], entries, 'current_implementation_not_in_lookup_chain') unless current_index
|
|
954
|
+
|
|
955
|
+
ordered_method_lookup(
|
|
956
|
+
site,
|
|
957
|
+
entries.drop(current_index + 1),
|
|
958
|
+
reason: 'super_next_implementation',
|
|
959
|
+
completeness_entries: entries.first(current_index + 1)
|
|
960
|
+
)
|
|
961
|
+
end
|
|
962
|
+
|
|
963
|
+
def module_super_method_lookup(site, caller, separator)
|
|
964
|
+
hosts = class_infos.values.select { |info| info.kind == :class }.sort_by(&:id).flat_map do |host|
|
|
965
|
+
[instance_lookup_entries(host.id), singleton_lookup_entries(host.id)].filter_map do |entries|
|
|
966
|
+
index = entries.index([caller.owner, separator])
|
|
967
|
+
entries.drop(index + 1) if index
|
|
968
|
+
end
|
|
969
|
+
end
|
|
970
|
+
own_entries = instance_lookup_entries(caller.owner)
|
|
971
|
+
own_index = own_entries.index([caller.owner, separator])
|
|
972
|
+
hosts << own_entries.drop(own_index + 1) if own_index
|
|
973
|
+
results = hosts.map do |entries|
|
|
974
|
+
ordered_method_lookup(site, entries, reason: 'module_super_host_lookup', force_partial: true)
|
|
975
|
+
end
|
|
976
|
+
targets = results.flat_map(&:targets).uniq(&:graph_id).sort_by(&:graph_id)
|
|
977
|
+
chain = results.flat_map(&:lookup_chain).uniq
|
|
978
|
+
incomplete_method_lookup(targets, chain.map { |owner| [owner, '#'] }, 'module_super_open_hosts')
|
|
979
|
+
end
|
|
980
|
+
|
|
981
|
+
def ordered_method_lookup(site, entries, reason:, completeness_entries: [], force_partial: false)
|
|
982
|
+
entries = Array(entries)
|
|
983
|
+
target_index = entries.index do |owner, separator|
|
|
984
|
+
definitions_for("#{owner}#{separator}#{site.message}").any?
|
|
985
|
+
end
|
|
986
|
+
unless target_index
|
|
987
|
+
missing = method_missing_lookup(site, entries, reason, completeness_entries, force_partial)
|
|
988
|
+
return missing if missing
|
|
989
|
+
|
|
990
|
+
targets = dynamic_ancestry_candidates(site, [])
|
|
991
|
+
return incomplete_method_lookup(targets, entries, "#{reason}_missing")
|
|
992
|
+
end
|
|
993
|
+
|
|
994
|
+
target_entry = entries.fetch(target_index)
|
|
995
|
+
targets = definitions_for("#{target_entry.first}#{target_entry.last}#{site.message}")
|
|
996
|
+
prefix = completeness_entries + entries.first(target_index + 1)
|
|
997
|
+
complete = !force_partial && complete_lookup_prefix?(prefix) && !dynamic_lookup_chain?(entries) &&
|
|
998
|
+
!dynamic_ancestry_uncertain?(site)
|
|
999
|
+
targets = dynamic_ancestry_candidates(site, targets) if dynamic_ancestry_uncertain?(site)
|
|
1000
|
+
return incomplete_method_lookup(targets, entries, "#{reason}_incomplete") unless complete
|
|
1001
|
+
|
|
1002
|
+
if targets.any? { |target| protected_visibility_outcome(site, target) == :unknown }
|
|
1003
|
+
return incomplete_method_lookup(targets, entries, "#{reason}_protected_context_unknown")
|
|
1004
|
+
end
|
|
1005
|
+
|
|
1006
|
+
accepted, target_rejections = apply_complete_target_constraints(site, targets)
|
|
1007
|
+
shadowed = shadowed_rejections(site.message, entries.drop(target_index + 1))
|
|
1008
|
+
MethodLookup.new(
|
|
1009
|
+
targets: accepted,
|
|
1010
|
+
status: :complete,
|
|
1011
|
+
rejected_targets: target_rejections + shadowed,
|
|
1012
|
+
lookup_chain: entries.map(&:first),
|
|
1013
|
+
reason: reason
|
|
1014
|
+
)
|
|
1015
|
+
end
|
|
1016
|
+
|
|
1017
|
+
def method_missing_lookup(site, entries, reason, completeness_entries, force_partial)
|
|
1018
|
+
missing_index = entries.index do |owner, separator|
|
|
1019
|
+
definitions_for("#{owner}#{separator}method_missing").any?
|
|
1020
|
+
end
|
|
1021
|
+
return unless missing_index
|
|
1022
|
+
|
|
1023
|
+
entry = entries.fetch(missing_index)
|
|
1024
|
+
targets = definitions_for("#{entry.first}#{entry.last}method_missing")
|
|
1025
|
+
prefix = completeness_entries + entries.first(missing_index + 1)
|
|
1026
|
+
complete = !force_partial && complete_lookup_prefix?(prefix) && !dynamic_lookup_chain?(entries) &&
|
|
1027
|
+
!dynamic_ancestry_uncertain?(site)
|
|
1028
|
+
return incomplete_method_lookup(targets, entries, "#{reason}_method_missing_incomplete") unless complete
|
|
1029
|
+
|
|
1030
|
+
MethodLookup.new(
|
|
1031
|
+
targets: targets,
|
|
1032
|
+
status: :complete,
|
|
1033
|
+
lookup_chain: entries.map(&:first),
|
|
1034
|
+
reason: "#{reason}_method_missing"
|
|
1035
|
+
)
|
|
1036
|
+
end
|
|
1037
|
+
|
|
1038
|
+
def incomplete_method_lookup(targets, entries, reason)
|
|
1039
|
+
targets = Array(targets)
|
|
1040
|
+
MethodLookup.new(
|
|
1041
|
+
targets: targets,
|
|
1042
|
+
status: targets.empty? ? :unknown : :partial,
|
|
1043
|
+
lookup_chain: Array(entries).map { |entry| entry.is_a?(Array) ? entry.first : entry },
|
|
1044
|
+
reason: reason
|
|
1045
|
+
)
|
|
1046
|
+
end
|
|
1047
|
+
|
|
1048
|
+
def fallback_method_lookup(site, reason:)
|
|
1049
|
+
domain = site.test ? :test : :runtime
|
|
1050
|
+
incomplete_method_lookup(ambiguous_fallback_candidates(site.message, domain: domain), [], reason)
|
|
1051
|
+
end
|
|
1052
|
+
|
|
1053
|
+
def apply_complete_target_constraints(site, targets)
|
|
1054
|
+
accepted = []
|
|
1055
|
+
rejected = []
|
|
1056
|
+
targets.each do |target|
|
|
1057
|
+
visibility_reason = visibility_rejection_reason(site, target)
|
|
1058
|
+
if visibility_reason
|
|
1059
|
+
rejected << RejectedTarget.new(definition_id: target.graph_id, reason: visibility_reason)
|
|
1060
|
+
elsif arity_target_rejected?(site, target)
|
|
1061
|
+
rejected << RejectedTarget.new(definition_id: target.graph_id, reason: 'arity_mismatch')
|
|
1062
|
+
else
|
|
1063
|
+
accepted << target
|
|
1064
|
+
end
|
|
1065
|
+
end
|
|
1066
|
+
[accepted, rejected]
|
|
1067
|
+
end
|
|
1068
|
+
|
|
1069
|
+
def visibility_rejection_reason(site, target)
|
|
1070
|
+
return 'protected_visibility' if protected_visibility_outcome(site, target) == :reject
|
|
1071
|
+
|
|
1072
|
+
'private_visibility' if private_target_rejected?(site, target)
|
|
1073
|
+
end
|
|
1074
|
+
|
|
1075
|
+
def protected_visibility_outcome(site, target)
|
|
1076
|
+
return :allow unless target.visibility == :protected
|
|
1077
|
+
|
|
1078
|
+
original = site.metadata['original_message'] || site.metadata[:original_message]
|
|
1079
|
+
return :reject if original.to_s == 'public_send'
|
|
1080
|
+
return :allow if %w[send __send__ method].include?(original.to_s)
|
|
1081
|
+
return :allow if %i[implicit self super].include?(site.receiver_kind)
|
|
1082
|
+
|
|
1083
|
+
caller_owner = nodes.exact(site.caller_id)&.owner
|
|
1084
|
+
receiver_owner = resolved_receiver_owner(site)
|
|
1085
|
+
return :unknown unless protected_context_known?(caller_owner, receiver_owner, target.owner)
|
|
1086
|
+
|
|
1087
|
+
caller_family = cached_lookup_chain(caller_owner).include?(target.owner)
|
|
1088
|
+
receiver_family = cached_lookup_chain(receiver_owner).include?(target.owner)
|
|
1089
|
+
caller_family && receiver_family ? :allow : :reject
|
|
1090
|
+
end
|
|
1091
|
+
|
|
1092
|
+
def protected_context_known?(*owners)
|
|
1093
|
+
owners.all? do |owner|
|
|
1094
|
+
info = class_info(owner)
|
|
1095
|
+
info && !info.dynamic
|
|
1096
|
+
end
|
|
1097
|
+
end
|
|
1098
|
+
|
|
1099
|
+
def private_target_rejected?(site, target)
|
|
1100
|
+
return false unless target.visibility == :private
|
|
1101
|
+
return false if site.metadata['implicit_private_dispatch'] == true
|
|
1102
|
+
return false if %i[implicit self super].include?(site.receiver_kind)
|
|
1103
|
+
|
|
1104
|
+
original = site.metadata['original_message'] || site.metadata[:original_message]
|
|
1105
|
+
return false if %w[send __send__ method].include?(original.to_s)
|
|
1106
|
+
|
|
1107
|
+
if original.to_s == 'respond_to?'
|
|
1108
|
+
include_private = if site.metadata.key?('include_private')
|
|
1109
|
+
site.metadata['include_private']
|
|
1110
|
+
else
|
|
1111
|
+
site.metadata[:include_private]
|
|
1112
|
+
end
|
|
1113
|
+
return include_private == false
|
|
1114
|
+
end
|
|
1115
|
+
|
|
1116
|
+
true
|
|
1117
|
+
end
|
|
1118
|
+
|
|
1119
|
+
def arity_target_rejected?(site, target)
|
|
1120
|
+
arguments = site.metadata['arguments'] || site.metadata[:arguments]
|
|
1121
|
+
signature = method_signatures[target.graph_id]
|
|
1122
|
+
return false unless arguments.is_a?(Hash) && signature.is_a?(Hash)
|
|
1123
|
+
return false unless hash_value(arguments, 'complete') && hash_value(signature, 'complete')
|
|
1124
|
+
|
|
1125
|
+
keywords = Array(hash_value(arguments, 'keywords')).map(&:to_s)
|
|
1126
|
+
required = Array(hash_value(signature, 'required_keywords')).map(&:to_s)
|
|
1127
|
+
accepted = Array(hash_value(signature, 'accepted_keywords')).map(&:to_s)
|
|
1128
|
+
keyword_rest = hash_value(signature, 'keyword_rest')
|
|
1129
|
+
accepts_keywords = hash_value(signature, 'accepts_keywords')
|
|
1130
|
+
accepts_keywords = required.any? || accepted.any? || keyword_rest if accepts_keywords.nil?
|
|
1131
|
+
no_keywords = hash_value(signature, 'no_keywords')
|
|
1132
|
+
|
|
1133
|
+
count = hash_value(arguments, 'positional_count').to_i
|
|
1134
|
+
if keywords.any? && !accepts_keywords
|
|
1135
|
+
return true if no_keywords
|
|
1136
|
+
|
|
1137
|
+
count += 1
|
|
1138
|
+
end
|
|
1139
|
+
|
|
1140
|
+
minimum = hash_value(signature, 'minimum_positionals').to_i
|
|
1141
|
+
maximum = hash_value(signature, 'maximum_positionals')
|
|
1142
|
+
return true if count < minimum || (!maximum.nil? && count > maximum.to_i)
|
|
1143
|
+
return false unless accepts_keywords
|
|
1144
|
+
return true unless (required - keywords).empty?
|
|
1145
|
+
|
|
1146
|
+
!keyword_rest && !(keywords - accepted).empty?
|
|
1147
|
+
end
|
|
1148
|
+
|
|
1149
|
+
def hash_value(hash, key)
|
|
1150
|
+
hash.key?(key) ? hash[key] : hash[key.to_sym]
|
|
1151
|
+
end
|
|
1152
|
+
|
|
1153
|
+
def shadowed_rejections(message, entries)
|
|
1154
|
+
entries.flat_map do |owner, separator|
|
|
1155
|
+
definitions_for("#{owner}#{separator}#{message}").map do |target|
|
|
1156
|
+
RejectedTarget.new(definition_id: target.graph_id, reason: 'shadowed_by_lookup_order')
|
|
1157
|
+
end
|
|
1158
|
+
end
|
|
1159
|
+
end
|
|
1160
|
+
|
|
1161
|
+
def complete_lookup_prefix?(entries)
|
|
1162
|
+
entries.all? do |owner, _separator|
|
|
1163
|
+
info = class_info(owner)
|
|
1164
|
+
info && !info.dynamic
|
|
1165
|
+
end
|
|
1166
|
+
end
|
|
1167
|
+
|
|
1168
|
+
def dynamic_lookup_chain?(entries)
|
|
1169
|
+
entries.any? { |owner, _separator| class_info(owner)&.dynamic }
|
|
1170
|
+
end
|
|
1171
|
+
|
|
1172
|
+
def dynamic_ancestry_uncertain?(site)
|
|
1173
|
+
owners = receiver_candidates(site)
|
|
1174
|
+
owners = [nodes.exact(site.caller_id)&.owner].compact if owners.empty?
|
|
1175
|
+
key = [site.test ? :test : :runtime, owners.sort]
|
|
1176
|
+
return @dynamic_ancestry_cache[key] if @dynamic_ancestry_cache.key?(key)
|
|
1177
|
+
|
|
1178
|
+
@dynamic_ancestry_cache[key] = @blockers.any? do |blocker|
|
|
1179
|
+
next false unless blocker.kind == :dynamic_ancestry
|
|
1180
|
+
next false unless blocker.caller_domain == :runtime || site.test
|
|
1181
|
+
next true if blocker.scope_kind == :global
|
|
1182
|
+
next false unless blocker.scope_kind == :owner
|
|
281
1183
|
|
|
282
|
-
|
|
1184
|
+
Array(blocker.scope_value).map(&:to_s).intersect?(owners)
|
|
283
1185
|
end
|
|
284
|
-
|
|
1186
|
+
end
|
|
1187
|
+
|
|
1188
|
+
def dynamic_ancestry_candidates(site, targets)
|
|
1189
|
+
return targets unless dynamic_ancestry_uncertain?(site)
|
|
1190
|
+
|
|
1191
|
+
domain = site.test ? :test : :runtime
|
|
1192
|
+
(Array(targets) + ambiguous_fallback_candidates(site.message, domain: domain)).uniq(&:graph_id).sort_by(&:graph_id)
|
|
1193
|
+
end
|
|
1194
|
+
|
|
1195
|
+
def resolved_receiver_owner(site)
|
|
1196
|
+
candidates = receiver_candidates(site)
|
|
1197
|
+
candidates.find { |owner| class_info(owner) } || candidates.first
|
|
1198
|
+
end
|
|
1199
|
+
|
|
1200
|
+
def instance_lookup_entries(owner)
|
|
1201
|
+
cached_lookup_chain(owner).map { |candidate| [candidate, '#'] }
|
|
1202
|
+
end
|
|
1203
|
+
|
|
1204
|
+
def singleton_lookup_entries(owner, seen = Set.new)
|
|
1205
|
+
top_level = seen.empty?
|
|
1206
|
+
@singleton_lookup_chain_cache ||= {}
|
|
1207
|
+
return @singleton_lookup_chain_cache[owner] if top_level && @singleton_lookup_chain_cache.key?(owner)
|
|
1208
|
+
return [] unless owner && seen.add?(owner)
|
|
1209
|
+
|
|
1210
|
+
info = class_info(owner)
|
|
1211
|
+
result = if info
|
|
1212
|
+
prepends = info.singleton_prepends.reverse.flat_map do |extension|
|
|
1213
|
+
method_lookup_chain(extension).map { |candidate| [candidate, '#'] }
|
|
1214
|
+
end
|
|
1215
|
+
extensions = info.singleton_includes.reverse.flat_map do |extension|
|
|
1216
|
+
method_lookup_chain(extension).map { |candidate| [candidate, '#'] }
|
|
1217
|
+
end
|
|
1218
|
+
[*prepends, [owner, '.'], *extensions, *singleton_lookup_entries(info.superclass, seen)]
|
|
1219
|
+
else
|
|
1220
|
+
[[owner, '.']]
|
|
1221
|
+
end
|
|
1222
|
+
top_level ? (@singleton_lookup_chain_cache[owner] = result.freeze) : result
|
|
285
1223
|
end
|
|
286
1224
|
|
|
287
1225
|
def receiver_candidates(site)
|
|
@@ -299,15 +1237,37 @@ module Necropsy
|
|
|
299
1237
|
return true if node.owner == caller_owner
|
|
300
1238
|
return true if class_info(node.owner)&.dynamic
|
|
301
1239
|
|
|
302
|
-
|
|
1240
|
+
rta_instantiated_owners(site.message).include?(node.owner)
|
|
1241
|
+
end
|
|
1242
|
+
|
|
1243
|
+
# RTA evaluates every candidate for a call site against the same set of
|
|
1244
|
+
# scanned allocations. Computing that set inside the candidate predicate
|
|
1245
|
+
# turns a single call site into O(candidates * instantiated_classes)
|
|
1246
|
+
# dispatch checks. Cache the dispatched owners once per message instead;
|
|
1247
|
+
# this preserves the conservative owner test while making the hot path
|
|
1248
|
+
# linear in the number of candidates.
|
|
1249
|
+
def rta_instantiated_owners(message)
|
|
1250
|
+
@rta_instantiated_owner_cache.fetch(message) do
|
|
1251
|
+
@rta_instantiated_owner_cache[message] = instantiated_classes.each_with_object(Set.new) do |owner, owners|
|
|
1252
|
+
dispatched = dispatched_instance_owner(owner, message)
|
|
1253
|
+
owners << dispatched if dispatched
|
|
1254
|
+
end
|
|
1255
|
+
end
|
|
303
1256
|
end
|
|
304
1257
|
|
|
305
1258
|
def dispatched_instance_owner(owner, message)
|
|
306
1259
|
@dispatch_cache ||= {}
|
|
307
1260
|
key = [owner, message]
|
|
308
|
-
|
|
1261
|
+
if @dispatch_cache.key?(key)
|
|
1262
|
+
@resolution_cache_hits = @resolution_cache_hits.to_i + 1
|
|
1263
|
+
return @dispatch_cache[key]
|
|
1264
|
+
end
|
|
1265
|
+
|
|
1266
|
+
@resolution_cache_misses = @resolution_cache_misses.to_i + 1
|
|
309
1267
|
|
|
310
|
-
@dispatch_cache[key] = cached_lookup_chain(owner).find
|
|
1268
|
+
@dispatch_cache[key] = cached_lookup_chain(owner).find do |candidate|
|
|
1269
|
+
definitions_for("#{candidate}##{message}").any?
|
|
1270
|
+
end
|
|
311
1271
|
end
|
|
312
1272
|
|
|
313
1273
|
def cached_lookup_chain(owner)
|
|
@@ -339,13 +1299,16 @@ module Necropsy
|
|
|
339
1299
|
def merge_observation(left, right)
|
|
340
1300
|
return right unless left.is_a?(Hash) && right.is_a?(Hash)
|
|
341
1301
|
|
|
342
|
-
|
|
1302
|
+
Analyzers::Dynamic::ObservationPolicy.compatible_merge(left, right)
|
|
343
1303
|
end
|
|
344
1304
|
|
|
345
1305
|
def call_site_key(site)
|
|
1306
|
+
call_site_id = site['call_site_id'] || site[:call_site_id]
|
|
1307
|
+
return [:call_site_id, call_site_id] if call_site_id
|
|
1308
|
+
|
|
346
1309
|
metadata = site['metadata'] || site[:metadata] || {}
|
|
347
1310
|
[
|
|
348
|
-
site['caller_id'] || site[:caller_id],
|
|
1311
|
+
site['caller_definition_id'] || site[:caller_definition_id] || site['caller_id'] || site[:caller_id],
|
|
349
1312
|
site['message'] || site[:message],
|
|
350
1313
|
(site['receiver_kind'] || site[:receiver_kind])&.to_s,
|
|
351
1314
|
site['receiver_name'] || site[:receiver_name],
|
|
@@ -366,16 +1329,43 @@ module Necropsy
|
|
|
366
1329
|
end
|
|
367
1330
|
end
|
|
368
1331
|
|
|
369
|
-
def
|
|
370
|
-
|
|
371
|
-
|
|
1332
|
+
def resolve_definitions(identifier)
|
|
1333
|
+
nodes.resolve(identifier)
|
|
1334
|
+
end
|
|
1335
|
+
|
|
1336
|
+
def add_physical_edge(caller_id, callee_id, evidence)
|
|
1337
|
+
caller = nodes.exact(caller_id)
|
|
1338
|
+
evidence_id = register_evidence(evidence, domain: caller&.test ? :test : :runtime)
|
|
1339
|
+
return unless evidence_id
|
|
1340
|
+
|
|
1341
|
+
@edges[caller_id] ||= {}
|
|
1342
|
+
@edges[caller_id][callee_id] ||= Set.new
|
|
1343
|
+
@edges[caller_id][callee_id] << evidence_id
|
|
1344
|
+
@incoming_edges[callee_id] ||= {}
|
|
1345
|
+
@incoming_edges[callee_id][caller_id] = @edges[caller_id][callee_id]
|
|
372
1346
|
end
|
|
373
1347
|
|
|
374
|
-
def
|
|
375
|
-
return
|
|
1348
|
+
def record_ambiguous_input(kind, identifier, definitions)
|
|
1349
|
+
return unless definitions.length > 1 && definitions_for(identifier).length > 1
|
|
376
1350
|
|
|
377
|
-
|
|
378
|
-
|
|
1351
|
+
record_definition_ambiguity(kind, { 'identifier' => identifier }, definitions)
|
|
1352
|
+
end
|
|
1353
|
+
|
|
1354
|
+
def record_definition_ambiguity(kind, reference, definitions)
|
|
1355
|
+
resolution = observation['definition_resolution'] ||= { 'ambiguous_input_count' => 0, 'ambiguous_inputs' => [] }
|
|
1356
|
+
resolution['ambiguous_input_count'] += 1
|
|
1357
|
+
resolution['ambiguous_inputs'] << reference.merge(
|
|
1358
|
+
'kind' => kind.to_s,
|
|
1359
|
+
'definition_ids' => definitions.map(&:graph_id).sort
|
|
1360
|
+
)
|
|
1361
|
+
resolution['ambiguous_inputs'] = resolution['ambiguous_inputs'].uniq.sort_by do |sample|
|
|
1362
|
+
[sample.fetch('kind'), canonical_dynamic_sample(sample), sample.fetch('definition_ids')]
|
|
1363
|
+
end.first(DynamicEvidenceTracking::SAMPLE_LIMIT)
|
|
1364
|
+
end
|
|
1365
|
+
|
|
1366
|
+
def retain_known_instantiated_classes
|
|
1367
|
+
known_owners = method_nodes.map(&:owner).compact.to_set
|
|
1368
|
+
instantiated_classes.select! { |name| known_owners.include?(name) }
|
|
379
1369
|
end
|
|
380
1370
|
end
|
|
381
1371
|
end
|