necropsy 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +33 -0
- data/CHANGELOG.md +44 -0
- data/MEASUREMENTS.md +112 -0
- data/README.md +233 -16
- data/Rakefile +3 -1
- data/bench/README.md +92 -0
- data/bench/audit.rb +113 -0
- data/bench/audits/0.2.1/audit.json +501 -0
- data/bench/audits/0.2.1/audit.md +55 -0
- data/bench/audits/0.2.1/baseline_performance.yml +16 -0
- data/bench/audits/0.2.1/config.yml +42 -0
- data/bench/audits/0.2.1/review.yml +56 -0
- data/bench/audits/0.2.1/review_queue.yml +3651 -0
- data/bench/corpora/v1/README.md +30 -0
- data/bench/corpora/v1/labels.yml +37 -0
- data/bench/corpora/v1/manifest.yml +63 -0
- data/bench/corpora/v1/rubocop.necropsy.yml +7 -0
- data/bench/corpora/v1/self.necropsy.yml +8 -0
- data/bench/corpora/v1/tools/debride.yml +42 -0
- data/bench/corpora/v1/tools/spoom.yml +12 -0
- data/bench/corpora/v1/tools/type_aware.yml +11 -0
- data/bench/golden/v1/candidate_union.json +21918 -0
- data/bench/golden/v1/metadata.json +12 -0
- data/bench/golden/v1/reports/dynamic_evidence.json +146 -0
- data/bench/golden/v1/reports/plain_ruby.json +135 -0
- data/bench/golden/v1/reports/rails.json +176 -0
- data/bench/golden/v1/reports/rubocop_1_75_0.json +26848 -0
- data/bench/golden/v1/reports/self.json +3449 -0
- data/bench/review_queue.rb +35 -0
- data/bench/run.rb +31 -0
- data/bench/schema/candidate-union-v1.schema.json +70 -0
- data/docs/impv_implementation_matrix.md +179 -0
- data/docs/migrations/0.2.1.md +57 -0
- data/docs/migrations/0.3.0.md +207 -0
- data/docs/migrations/0.4.0.md +13 -0
- data/docs/necropsy_performance_adr.md +43 -0
- data/docs/necropsy_scope_decisions.md +58 -0
- data/docs/necropsy_type_facts_adr.md +22 -0
- data/gemfiles/prism_min.gemfile +9 -0
- data/gemfiles/prism_min.gemfile.lock +50 -0
- data/lib/necropsy/analyzer.rb +121 -2
- data/lib/necropsy/analyzers/dynamic/coverage_collector.rb +81 -19
- data/lib/necropsy/analyzers/dynamic/coverage_importer.rb +104 -10
- data/lib/necropsy/analyzers/dynamic/coverband_importer.rb +74 -250
- data/lib/necropsy/analyzers/dynamic/coverband_payload_set.rb +149 -0
- data/lib/necropsy/analyzers/dynamic/observation_policy.rb +96 -0
- data/lib/necropsy/analyzers/dynamic/redis_input_limits.rb +121 -0
- data/lib/necropsy/analyzers/dynamic/redis_nonblocking_io.rb +94 -0
- data/lib/necropsy/analyzers/dynamic/redis_payload_loader.rb +165 -0
- data/lib/necropsy/analyzers/dynamic/redis_transport.rb +217 -0
- data/lib/necropsy/analyzers/dynamic/runtime_reference.rb +96 -0
- data/lib/necropsy/analyzers/dynamic/trace_point_collector.rb +225 -20
- data/lib/necropsy/analyzers/dynamic/trace_point_importer.rb +3 -1
- data/lib/necropsy/analyzers/legacy_result_adapter.rb +226 -0
- data/lib/necropsy/analyzers/static/cha.rb +33 -69
- data/lib/necropsy/analyzers/static/name_resolution.rb +151 -21
- data/lib/necropsy/analyzers/static/rta.rb +249 -25
- data/lib/necropsy/ast_scanner/call_recording.rb +275 -0
- data/lib/necropsy/ast_scanner/call_site_creation.rb +54 -0
- data/lib/necropsy/ast_scanner/definition_creation.rb +43 -0
- data/lib/necropsy/ast_scanner/dsl_macros.rb +532 -0
- data/lib/necropsy/ast_scanner/method_definitions.rb +358 -0
- data/lib/necropsy/ast_scanner/references.rb +115 -0
- data/lib/necropsy/ast_scanner/ruby_semantics.rb +263 -0
- data/lib/necropsy/ast_scanner/traversal.rb +347 -0
- data/lib/necropsy/ast_scanner/value_definitions.rb +78 -0
- data/lib/necropsy/ast_scanner.rb +89 -564
- data/lib/necropsy/bench/candidate_union.rb +555 -0
- data/lib/necropsy/bench/claim_gate.rb +112 -0
- data/lib/necropsy/bench/evaluator.rb +356 -24
- data/lib/necropsy/bench/finding_facts.rb +152 -0
- data/lib/necropsy/bench/precision_gate.rb +144 -0
- data/lib/necropsy/bench/release_audit/adversarial_runner.rb +56 -0
- data/lib/necropsy/bench/release_audit/artifact_writer.rb +112 -0
- data/lib/necropsy/bench/release_audit/config_validator.rb +112 -0
- data/lib/necropsy/bench/release_audit/git_snapshot.rb +36 -0
- data/lib/necropsy/bench/release_audit/performance_gate.rb +165 -0
- data/lib/necropsy/bench/release_audit/run_provenance.rb +133 -0
- data/lib/necropsy/bench/release_audit.rb +360 -0
- data/lib/necropsy/bench/report_normalizer.rb +140 -0
- data/lib/necropsy/bench/review_queue.rb +154 -0
- data/lib/necropsy/bench/safety_mutation_harness.rb +59 -0
- data/lib/necropsy/bench/seed_runner.rb +408 -0
- data/lib/necropsy/bounded_canonicalizer.rb +218 -0
- data/lib/necropsy/cache/scan_cache.rb +94 -18
- data/lib/necropsy/call_site_identity.rb +54 -0
- data/lib/necropsy/cli.rb +296 -53
- data/lib/necropsy/clock.rb +40 -0
- data/lib/necropsy/confidence/scorer.rb +181 -58
- data/lib/necropsy/configuration.rb +339 -18
- data/lib/necropsy/convention_rules.rb +138 -0
- data/lib/necropsy/definition_identity/canonical_digest.rb +278 -0
- data/lib/necropsy/definition_identity.rb +37 -0
- data/lib/necropsy/diagnostics.rb +342 -0
- data/lib/necropsy/embedded_ruby.rb +55 -0
- data/lib/necropsy/entry_points/plain.rb +125 -7
- data/lib/necropsy/entry_points/rails.rb +422 -105
- data/lib/necropsy/entry_points/test.rb +6 -1
- data/lib/necropsy/flow_interpreter.rb +460 -0
- data/lib/necropsy/graph/blocker_matching.rb +338 -0
- data/lib/necropsy/graph/call_graph.rb +1234 -62
- data/lib/necropsy/graph/definition_index.rb +149 -0
- data/lib/necropsy/graph/dynamic_evidence_tracking.rb +206 -0
- data/lib/necropsy/graph/evidence_store.rb +213 -0
- data/lib/necropsy/graph/resolution_store.rb +497 -0
- data/lib/necropsy/graph_self_check.rb +79 -0
- data/lib/necropsy/guardrail/baseline.rb +359 -13
- data/lib/necropsy/guardrail/diff.rb +5 -2
- data/lib/necropsy/guardrail/quarantine.rb +101 -10
- data/lib/necropsy/load_graph.rb +206 -0
- data/lib/necropsy/models.rb +894 -21
- data/lib/necropsy/performance_profiler.rb +108 -0
- data/lib/necropsy/project.rb +361 -13
- data/lib/necropsy/reachability/engine.rb +80 -15
- data/lib/necropsy/reference_barrier.rb +458 -0
- data/lib/necropsy/report.rb +153 -14
- data/lib/necropsy/reporter.rb +441 -19
- data/lib/necropsy/runner.rb +258 -14
- data/lib/necropsy/runtime_feedback.rb +136 -0
- data/lib/necropsy/semantics_matrix.rb +153 -0
- data/lib/necropsy/trace_point_runtime.rb +19 -0
- data/lib/necropsy/type_facts.rb +53 -0
- data/lib/necropsy/version.rb +1 -1
- data/lib/necropsy/why_not_explanation.rb +436 -0
- data/lib/necropsy/why_not_renderer.rb +197 -0
- data/lib/necropsy/world_policy.rb +90 -0
- data/lib/necropsy.rb +39 -2
- data/schema/necropsy-report-v2.schema.json +366 -0
- data/script/measure.rb +20 -0
- metadata +106 -2
|
@@ -2,41 +2,163 @@
|
|
|
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
|
-
def initialize(scan_result)
|
|
9
|
-
@nodes =
|
|
10
|
-
@edges =
|
|
14
|
+
def initialize(scan_result, ambiguity_limit: 4)
|
|
15
|
+
@nodes = DefinitionIndex.new
|
|
16
|
+
@edges = {}
|
|
17
|
+
@incoming_edges = {}
|
|
18
|
+
initialize_evidence_store
|
|
11
19
|
@call_sites = scan_result.call_sites
|
|
12
20
|
@instantiated_classes = scan_result.instantiated_classes.dup
|
|
13
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!)
|
|
14
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
|
|
37
|
+
@ambiguity_limit = ambiguity_limit
|
|
38
|
+
@blockers = []
|
|
39
|
+
initialize_blocker_indexes
|
|
15
40
|
@entry_points = []
|
|
16
41
|
@profiles = []
|
|
17
|
-
@uncertainties = scan_result.uncertainties
|
|
42
|
+
@uncertainties = scan_result.uncertainties.to_h do |node_id, messages|
|
|
43
|
+
[node_id, Array(messages).dup]
|
|
44
|
+
end
|
|
18
45
|
@dynamic_alive = {}
|
|
19
46
|
@observation = {}
|
|
47
|
+
record_generated_macro_observation(scan_result.nodes)
|
|
48
|
+
initialize_resolution_store
|
|
49
|
+
@descendants = {}
|
|
50
|
+
@rta_instantiated_owner_cache = {}
|
|
51
|
+
@duplicate_blockers_initialized = false
|
|
20
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) }
|
|
21
57
|
retain_known_instantiated_classes
|
|
22
58
|
end
|
|
23
59
|
|
|
24
60
|
def add_node(node)
|
|
25
|
-
nodes
|
|
61
|
+
existing = nodes.exact(node.graph_id)
|
|
62
|
+
return existing if existing
|
|
63
|
+
|
|
64
|
+
@method_nodes = nil
|
|
65
|
+
@nodes_by_name = nil
|
|
66
|
+
@runtime_nodes_by_name = nil
|
|
67
|
+
@dispatch_cache = nil
|
|
68
|
+
@lookup_chain_cache = nil
|
|
69
|
+
@singleton_lookup_chain_cache = nil
|
|
70
|
+
@owner_ancestor_cache = nil
|
|
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
|
|
26
112
|
end
|
|
27
113
|
|
|
28
|
-
def
|
|
29
|
-
|
|
114
|
+
def definitions_for(symbol_id)
|
|
115
|
+
nodes.definitions_for(symbol_id)
|
|
116
|
+
end
|
|
30
117
|
|
|
31
|
-
|
|
32
|
-
|
|
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, [])
|
|
33
121
|
end
|
|
34
122
|
|
|
35
|
-
def apply_result(result)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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)
|
|
144
|
+
result.uncertainties.each do |node_id, messages|
|
|
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
|
|
151
|
+
end
|
|
152
|
+
Array(result.respond_to?(:blockers) ? result.blockers : []).each { |blocker| add_blocker(blocker) }
|
|
39
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
|
|
40
162
|
end
|
|
41
163
|
|
|
42
164
|
def add_profile(profile)
|
|
@@ -44,55 +166,167 @@ module Necropsy
|
|
|
44
166
|
end
|
|
45
167
|
|
|
46
168
|
def add_edge(caller_id, callee_id, evidence)
|
|
47
|
-
|
|
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
|
|
48
176
|
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
51
183
|
end
|
|
52
184
|
|
|
53
185
|
def add_alive(node_id, evidence)
|
|
54
|
-
|
|
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
|
|
55
196
|
|
|
56
|
-
|
|
57
|
-
|
|
197
|
+
@dynamic_alive[definition.graph_id] ||= Set.new
|
|
198
|
+
@dynamic_alive[definition.graph_id] << evidence_id
|
|
199
|
+
end
|
|
200
|
+
true
|
|
58
201
|
end
|
|
59
202
|
|
|
60
203
|
def dynamic_alive?(node_id)
|
|
61
|
-
|
|
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
|
|
62
207
|
end
|
|
63
208
|
|
|
64
|
-
def alive_evidences(node_id)
|
|
65
|
-
|
|
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)
|
|
66
215
|
end
|
|
67
216
|
|
|
68
217
|
def dynamic_enabled?
|
|
69
|
-
@dynamic_alive.any?
|
|
218
|
+
@dynamic_alive.any?
|
|
70
219
|
end
|
|
71
220
|
|
|
72
|
-
def
|
|
73
|
-
|
|
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
|
|
74
227
|
end
|
|
75
228
|
|
|
76
|
-
def
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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)
|
|
80
245
|
end
|
|
81
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
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def edge_present?(caller_id, callee_id)
|
|
254
|
+
@edges.dig(caller_id, callee_id)&.any? { |evidence_id| evidence_record(evidence_id) }
|
|
82
255
|
end
|
|
83
256
|
|
|
84
|
-
def
|
|
85
|
-
|
|
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] }
|
|
86
292
|
end
|
|
87
293
|
|
|
88
294
|
def uncertainties(node_id = nil)
|
|
89
295
|
return @uncertainties unless node_id
|
|
90
296
|
|
|
91
|
-
|
|
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
|
+
}
|
|
92
322
|
end
|
|
93
323
|
|
|
94
324
|
def method_nodes
|
|
95
|
-
nodes.values.select(&:method?)
|
|
325
|
+
@method_nodes ||= nodes.values.select(&:method?)
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def nodes_by_name
|
|
329
|
+
@nodes_by_name ||= method_nodes.group_by(&:name).freeze
|
|
96
330
|
end
|
|
97
331
|
|
|
98
332
|
def class_info(owner)
|
|
@@ -100,7 +334,21 @@ module Necropsy
|
|
|
100
334
|
end
|
|
101
335
|
|
|
102
336
|
def descendants_of(owner)
|
|
103
|
-
|
|
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
|
|
351
|
+
end
|
|
104
352
|
end
|
|
105
353
|
|
|
106
354
|
def modules_for(owner)
|
|
@@ -110,56 +358,868 @@ module Necropsy
|
|
|
110
358
|
(info.prepends + info.includes + info.extends).uniq
|
|
111
359
|
end
|
|
112
360
|
|
|
113
|
-
def candidate_nodes(message)
|
|
114
|
-
|
|
361
|
+
def candidate_nodes(message, domain: nil)
|
|
362
|
+
index = domain&.to_sym == :runtime ? runtime_nodes_by_name : nodes_by_name
|
|
363
|
+
index.fetch(message, [])
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def ambiguous_fallback_candidates(message, domain: nil)
|
|
367
|
+
candidates = candidate_nodes(message, domain: domain)
|
|
368
|
+
return candidates if candidates.one?
|
|
369
|
+
return [] if candidates.size > @ambiguity_limit
|
|
370
|
+
|
|
371
|
+
candidates
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def ambiguity_exceeded?(message, domain: nil)
|
|
375
|
+
candidate_nodes(message, domain: domain).size > ambiguity_limit
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def ambiguous_resolution?
|
|
379
|
+
@ambiguity_limit > 1
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def owner_reachable_from_ancestor?(owner, ancestor)
|
|
383
|
+
@owner_ancestor_cache ||= {}
|
|
384
|
+
key = [owner, ancestor]
|
|
385
|
+
return @owner_ancestor_cache[key] if @owner_ancestor_cache.key?(key)
|
|
386
|
+
|
|
387
|
+
@owner_ancestor_cache[key] = descendants_of(ancestor).any? do |descendant|
|
|
388
|
+
cached_lookup_chain(descendant).include?(owner)
|
|
389
|
+
end
|
|
115
390
|
end
|
|
116
391
|
|
|
117
392
|
def resolve_call_site(site, rta: false)
|
|
118
|
-
candidates =
|
|
393
|
+
candidates = rta ? rta_candidates_for_receiver(site) : method_lookup(site).targets
|
|
119
394
|
candidates = candidates.select { |node| rta_candidate?(node, site) } if rta
|
|
120
395
|
candidates
|
|
121
396
|
end
|
|
122
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
|
+
|
|
569
|
+
def retain_rta_candidates(candidates, site)
|
|
570
|
+
candidates.select { |node| rta_candidate?(node, site) }
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
def reconcile_rta_result(result)
|
|
574
|
+
analyzed_sites = result.observation.dig('rta', 'analyzed_sites')
|
|
575
|
+
return unless analyzed_sites
|
|
576
|
+
|
|
577
|
+
analyzed_keys = analyzed_sites.to_set { |site| call_site_key(site) }
|
|
578
|
+
allowed = result.edge_evidences.each_with_object(Hash.new { |hash, key| hash[key] = Set.new }) do |edge, memo|
|
|
579
|
+
memo[call_site_key(edge.evidence.metadata)] << edge.callee_id
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
@edges.each_value do |callees|
|
|
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)
|
|
590
|
+
|
|
591
|
+
key = call_site_key(item.metadata)
|
|
592
|
+
analyzed_keys.include?(key) && !allowed[key].include?(callee_id)
|
|
593
|
+
end
|
|
594
|
+
end
|
|
595
|
+
callees.delete_if { |_callee_id, evidence_ids| evidence_ids.empty? }
|
|
596
|
+
end
|
|
597
|
+
@edges.delete_if { |_caller_id, callees| callees.empty? }
|
|
598
|
+
rebuild_incoming_edges
|
|
599
|
+
refresh_resolution_derived_state
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
def fallback_resolution?(site, resolved: nil)
|
|
603
|
+
resolved ||= resolve_call_site(site)
|
|
604
|
+
return false if resolved.empty?
|
|
605
|
+
|
|
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
|
|
610
|
+
end
|
|
611
|
+
|
|
123
612
|
def to_h
|
|
124
613
|
{
|
|
125
614
|
'nodes' => nodes.values.map(&:to_h),
|
|
615
|
+
'call_sites' => call_sites.map(&:to_h),
|
|
126
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,
|
|
127
621
|
'entry_points' => entry_points.map(&:to_h),
|
|
128
622
|
'class_infos' => class_infos.values.map(&:to_h),
|
|
129
623
|
'instantiated_classes' => instantiated_classes.to_a.sort,
|
|
130
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,
|
|
131
632
|
'observation' => observation
|
|
132
633
|
}
|
|
133
634
|
end
|
|
134
635
|
|
|
135
636
|
private
|
|
136
637
|
|
|
137
|
-
def
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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)
|
|
149
678
|
else
|
|
150
|
-
|
|
679
|
+
value
|
|
151
680
|
end
|
|
152
681
|
end
|
|
153
682
|
|
|
154
|
-
def
|
|
155
|
-
|
|
156
|
-
|
|
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
|
|
157
692
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
+
))
|
|
906
|
+
end
|
|
907
|
+
end
|
|
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
|
+
|
|
916
|
+
def rta_candidates_for_receiver(site)
|
|
917
|
+
exact = method_lookup(site).targets
|
|
918
|
+
return exact unless exact.empty?
|
|
919
|
+
|
|
920
|
+
candidate_nodes(site.message, domain: site.test ? :test : :runtime)
|
|
921
|
+
end
|
|
922
|
+
|
|
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')
|
|
941
|
+
end
|
|
942
|
+
|
|
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
|
|
946
|
+
|
|
947
|
+
separator = caller.kind == :singleton_method ? '.' : '#'
|
|
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
|
|
1183
|
+
|
|
1184
|
+
Array(blocker.scope_value).map(&:to_s).intersect?(owners)
|
|
1185
|
+
end
|
|
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
|
|
163
1223
|
end
|
|
164
1224
|
|
|
165
1225
|
def receiver_candidates(site)
|
|
@@ -169,10 +1229,61 @@ module Necropsy
|
|
|
169
1229
|
|
|
170
1230
|
def rta_candidate?(node, site)
|
|
171
1231
|
return true unless node.kind == :instance_method
|
|
172
|
-
|
|
1232
|
+
|
|
1233
|
+
caller_owner = nodes[site.caller_id]&.owner
|
|
1234
|
+
return true if site.receiver_kind == :super
|
|
1235
|
+
return dispatched_instance_owner(caller_owner, site.message) == node.owner if %i[self implicit].include?(site.receiver_kind)
|
|
1236
|
+
|
|
1237
|
+
return true if node.owner == caller_owner
|
|
173
1238
|
return true if class_info(node.owner)&.dynamic
|
|
174
1239
|
|
|
175
|
-
|
|
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
|
|
1256
|
+
end
|
|
1257
|
+
|
|
1258
|
+
def dispatched_instance_owner(owner, message)
|
|
1259
|
+
@dispatch_cache ||= {}
|
|
1260
|
+
key = [owner, message]
|
|
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
|
|
1267
|
+
|
|
1268
|
+
@dispatch_cache[key] = cached_lookup_chain(owner).find do |candidate|
|
|
1269
|
+
definitions_for("#{candidate}##{message}").any?
|
|
1270
|
+
end
|
|
1271
|
+
end
|
|
1272
|
+
|
|
1273
|
+
def cached_lookup_chain(owner)
|
|
1274
|
+
@lookup_chain_cache ||= {}
|
|
1275
|
+
@lookup_chain_cache[owner] ||= method_lookup_chain(owner)
|
|
1276
|
+
end
|
|
1277
|
+
|
|
1278
|
+
def method_lookup_chain(owner, seen = Set.new)
|
|
1279
|
+
return [] unless owner && seen.add?(owner)
|
|
1280
|
+
|
|
1281
|
+
info = class_info(owner)
|
|
1282
|
+
return [owner] unless info
|
|
1283
|
+
|
|
1284
|
+
prepends = info.prepends.reverse.flat_map { |name| method_lookup_chain(name, seen) }
|
|
1285
|
+
includes = info.includes.reverse.flat_map { |name| method_lookup_chain(name, seen) }
|
|
1286
|
+
prepends + [owner] + includes + method_lookup_chain(info.superclass, seen)
|
|
176
1287
|
end
|
|
177
1288
|
|
|
178
1289
|
def ancestor_chain(owner)
|
|
@@ -188,7 +1299,68 @@ module Necropsy
|
|
|
188
1299
|
def merge_observation(left, right)
|
|
189
1300
|
return right unless left.is_a?(Hash) && right.is_a?(Hash)
|
|
190
1301
|
|
|
191
|
-
|
|
1302
|
+
Analyzers::Dynamic::ObservationPolicy.compatible_merge(left, right)
|
|
1303
|
+
end
|
|
1304
|
+
|
|
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
|
+
|
|
1309
|
+
metadata = site['metadata'] || site[:metadata] || {}
|
|
1310
|
+
[
|
|
1311
|
+
site['caller_definition_id'] || site[:caller_definition_id] || site['caller_id'] || site[:caller_id],
|
|
1312
|
+
site['message'] || site[:message],
|
|
1313
|
+
(site['receiver_kind'] || site[:receiver_kind])&.to_s,
|
|
1314
|
+
site['receiver_name'] || site[:receiver_name],
|
|
1315
|
+
site['file'] || site[:file],
|
|
1316
|
+
site['line'] || site[:line],
|
|
1317
|
+
Array(metadata['receiver_candidates'] || metadata[:receiver_candidates]).sort,
|
|
1318
|
+
metadata['implicit_from'] || metadata[:implicit_from]
|
|
1319
|
+
]
|
|
1320
|
+
end
|
|
1321
|
+
|
|
1322
|
+
def rebuild_incoming_edges
|
|
1323
|
+
@incoming_edges = {}
|
|
1324
|
+
@edges.each do |caller_id, callees|
|
|
1325
|
+
callees.each do |callee_id, evidences|
|
|
1326
|
+
@incoming_edges[callee_id] ||= {}
|
|
1327
|
+
@incoming_edges[callee_id][caller_id] = evidences
|
|
1328
|
+
end
|
|
1329
|
+
end
|
|
1330
|
+
end
|
|
1331
|
+
|
|
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]
|
|
1346
|
+
end
|
|
1347
|
+
|
|
1348
|
+
def record_ambiguous_input(kind, identifier, definitions)
|
|
1349
|
+
return unless definitions.length > 1 && definitions_for(identifier).length > 1
|
|
1350
|
+
|
|
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)
|
|
192
1364
|
end
|
|
193
1365
|
|
|
194
1366
|
def retain_known_instantiated_classes
|