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
|
@@ -6,19 +6,38 @@ module Necropsy
|
|
|
6
6
|
class NameResolution < Analyzer
|
|
7
7
|
def analyze(graph, _project)
|
|
8
8
|
edge_evidences = []
|
|
9
|
+
resolutions = []
|
|
9
10
|
uncertainties = {}
|
|
11
|
+
blockers = []
|
|
10
12
|
graph.call_sites.each do |site|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
lookup = graph.method_lookup(site)
|
|
14
|
+
candidates = lookup.targets
|
|
15
|
+
fallback = %w[dynamic_message unknown_receiver unknown_self_owner self_lookup_fallback].include?(
|
|
16
|
+
lookup.reason
|
|
17
|
+
)
|
|
18
|
+
site_edges = edge_evidences_for(site, candidates, fallback, lookup)
|
|
19
|
+
edge_evidences.concat(site_edges)
|
|
20
|
+
resolutions << resolution_record(
|
|
21
|
+
site,
|
|
22
|
+
candidates,
|
|
23
|
+
site_edges,
|
|
24
|
+
status: lookup.status,
|
|
25
|
+
rejected_targets: lookup.rejected_targets,
|
|
26
|
+
unknown_scope: graph.residual_scope_for(site)
|
|
27
|
+
)
|
|
28
|
+
record_unresolved_uncertainty(graph, site, lookup, uncertainties)
|
|
29
|
+
blocker = unresolved_blocker(graph, site, lookup)
|
|
30
|
+
blockers << blocker if blocker
|
|
15
31
|
end
|
|
16
32
|
|
|
17
33
|
AnalyzerResult.new(
|
|
18
34
|
edge_evidences: edge_evidences,
|
|
19
35
|
alive_evidences: [],
|
|
20
36
|
uncertainties: uncertainties,
|
|
21
|
-
observation: {}
|
|
37
|
+
observation: {},
|
|
38
|
+
blockers: blockers,
|
|
39
|
+
resolutions: resolutions,
|
|
40
|
+
evidences: result_evidences(edge_evidences)
|
|
22
41
|
)
|
|
23
42
|
end
|
|
24
43
|
|
|
@@ -27,35 +46,137 @@ module Necropsy
|
|
|
27
46
|
name: :name_resolution,
|
|
28
47
|
kind: :static,
|
|
29
48
|
soundness: :unsound,
|
|
30
|
-
description: 'Resolves Ruby call sites by exact receiver and method name, falling back to same-name candidates.'
|
|
49
|
+
description: 'Resolves Ruby call sites by exact receiver and method name, falling back to same-name candidates.',
|
|
50
|
+
version: Necropsy::VERSION,
|
|
51
|
+
assumptions: %w[bounded_same_name_fallback scanned_receiver_hints]
|
|
31
52
|
)
|
|
32
53
|
end
|
|
33
54
|
|
|
55
|
+
def capabilities
|
|
56
|
+
[:complete_resolution].freeze
|
|
57
|
+
end
|
|
58
|
+
|
|
34
59
|
private
|
|
35
60
|
|
|
36
|
-
def edge_evidences_for(site, candidates, fallback)
|
|
61
|
+
def edge_evidences_for(site, candidates, fallback, lookup)
|
|
37
62
|
candidates.map do |candidate|
|
|
63
|
+
grade = if lookup.complete?
|
|
64
|
+
:exact
|
|
65
|
+
elsif fallback
|
|
66
|
+
:heuristic
|
|
67
|
+
else
|
|
68
|
+
:conservative
|
|
69
|
+
end
|
|
38
70
|
EdgeEvidence.new(
|
|
39
71
|
caller_id: site.caller_id,
|
|
40
|
-
callee_id: candidate.
|
|
72
|
+
callee_id: candidate.graph_id,
|
|
41
73
|
evidence: evidence(
|
|
42
74
|
kind: :call_edge,
|
|
43
75
|
details: "Name resolution#{' fallback' if fallback} at #{site.file}:#{site.line}",
|
|
44
76
|
weight: fallback ? 0.35 : 1.0,
|
|
45
|
-
metadata: site.to_h
|
|
77
|
+
metadata: site.to_h.merge(
|
|
78
|
+
'target_definition_id' => candidate.graph_id,
|
|
79
|
+
'lookup_status' => lookup.status.to_s,
|
|
80
|
+
'lookup_chain' => lookup.lookup_chain
|
|
81
|
+
),
|
|
82
|
+
grade: grade,
|
|
83
|
+
relation: :call_edge,
|
|
84
|
+
source: call_site_evidence_source(site).merge('target_definition_id' => candidate.graph_id),
|
|
85
|
+
scope: call_site_evidence_scope(site).merge('target_definition_id' => candidate.graph_id)
|
|
46
86
|
)
|
|
47
87
|
)
|
|
48
88
|
end
|
|
49
89
|
end
|
|
50
90
|
|
|
51
|
-
def record_unresolved_uncertainty(graph, site,
|
|
52
|
-
return if site.dynamic ||
|
|
53
|
-
|
|
91
|
+
def record_unresolved_uncertainty(graph, site, lookup, uncertainties)
|
|
92
|
+
return if site.dynamic || lookup.targets.any? || lookup.complete?
|
|
93
|
+
|
|
94
|
+
domain = site.test ? :test : :runtime
|
|
95
|
+
return if graph.candidate_nodes(site.message, domain: domain).empty? && site.receiver_kind != :unknown
|
|
54
96
|
|
|
55
97
|
uncertainties[site.caller_id] ||= []
|
|
56
98
|
uncertainties[site.caller_id] <<
|
|
57
99
|
"Unknown receiver for #{site.message} at #{site.file}:#{site.line}; dispatch may be ambiguous"
|
|
58
100
|
end
|
|
101
|
+
|
|
102
|
+
def unresolved_blocker(graph, site, lookup)
|
|
103
|
+
return if lookup.targets.any? || lookup.complete?
|
|
104
|
+
|
|
105
|
+
domain = site.test ? :test : :runtime
|
|
106
|
+
candidate_count = graph.candidate_nodes(site.message, domain: domain).size
|
|
107
|
+
limit_exceeded = graph.ambiguity_exceeded?(site.message, domain: domain)
|
|
108
|
+
return unless limit_exceeded || site.receiver_kind == :unknown
|
|
109
|
+
|
|
110
|
+
reason_code = limit_exceeded ? :ambiguity_limit_exceeded : :unknown_receiver
|
|
111
|
+
scope_kind, scope_value = blocker_scope(graph, site)
|
|
112
|
+
Blocker.new(
|
|
113
|
+
kind: :unknown_dispatch,
|
|
114
|
+
scope_kind: scope_kind,
|
|
115
|
+
scope_value: scope_value,
|
|
116
|
+
source: :name_resolution,
|
|
117
|
+
reason: blocker_reason(site, candidate_count, reason_code),
|
|
118
|
+
suggested_action: :review_receiver_flow,
|
|
119
|
+
metadata: blocker_metadata(graph, site, candidate_count, reason_code)
|
|
120
|
+
)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def blocker_scope(graph, site)
|
|
124
|
+
return [:message, site.message] if site.receiver_kind == :unknown
|
|
125
|
+
|
|
126
|
+
owners = receiver_hints(site)
|
|
127
|
+
owners = [graph.nodes[site.caller_id]&.owner].compact if owners.empty?
|
|
128
|
+
owners.empty? ? [:message, site.message] : [:owner, owners.sort]
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def blocker_reason(site, candidate_count, reason_code)
|
|
132
|
+
if reason_code == :ambiguity_limit_exceeded
|
|
133
|
+
"Dispatch #{site.message} has #{candidate_count} candidates, exceeding the configured ambiguity limit"
|
|
134
|
+
else
|
|
135
|
+
"Receiver for #{site.message} is unknown and no candidate target could be materialized"
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def blocker_metadata(graph, site, candidate_count, reason_code)
|
|
140
|
+
caller = graph.nodes[site.caller_id]
|
|
141
|
+
owner_scope = receiver_hints(site)
|
|
142
|
+
owner_scope = [caller&.owner].compact if owner_scope.empty? && site.receiver_kind != :unknown
|
|
143
|
+
metadata = {
|
|
144
|
+
'caller_id' => site.caller_id,
|
|
145
|
+
'caller_kind' => caller&.kind&.to_s,
|
|
146
|
+
'caller_domain' => site.test ? 'test' : 'runtime',
|
|
147
|
+
'message' => site.message,
|
|
148
|
+
'receiver_kind' => site.receiver_kind.to_s,
|
|
149
|
+
'receiver_name' => site.receiver_name,
|
|
150
|
+
'receiver_hints' => receiver_hints(site),
|
|
151
|
+
'owner_scope' => owner_scope,
|
|
152
|
+
'namespace_scope' => namespace_scope(owner_scope, caller&.owner),
|
|
153
|
+
'file' => site.file,
|
|
154
|
+
'line' => site.line,
|
|
155
|
+
'candidate_count' => candidate_count,
|
|
156
|
+
'ambiguity_limit' => finite_limit(graph.ambiguity_limit),
|
|
157
|
+
'reason_code' => reason_code.to_s,
|
|
158
|
+
'original_message' => site.metadata['original_message'] || site.metadata[:original_message]
|
|
159
|
+
}
|
|
160
|
+
return metadata unless site.metadata.key?('include_private') || site.metadata.key?(:include_private)
|
|
161
|
+
|
|
162
|
+
metadata.merge('include_private' => site.metadata.fetch('include_private') { site.metadata[:include_private] })
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def receiver_hints(site)
|
|
166
|
+
candidates = site.metadata['receiver_candidates'] || site.metadata[:receiver_candidates]
|
|
167
|
+
values = Array(candidates).compact
|
|
168
|
+
values = [site.receiver_name].compact if values.empty?
|
|
169
|
+
values.map(&:to_s).uniq
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def namespace_scope(owner_scope, caller_owner)
|
|
173
|
+
values = owner_scope.empty? ? [caller_owner].compact : owner_scope
|
|
174
|
+
values.filter_map { |owner| owner.to_s.split('::')[0...-1].then { |parts| parts.join('::') unless parts.empty? } }.uniq
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def finite_limit(limit)
|
|
178
|
+
limit.finite? ? limit : 'unlimited'
|
|
179
|
+
end
|
|
59
180
|
end
|
|
60
181
|
end
|
|
61
182
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'digest'
|
|
4
|
+
|
|
3
5
|
module Necropsy
|
|
4
6
|
module Analyzers
|
|
5
7
|
module Static
|
|
@@ -11,83 +13,289 @@ module Necropsy
|
|
|
11
13
|
reverse_each select sort sort_by take take_while to_a to_h
|
|
12
14
|
].freeze
|
|
13
15
|
COMPARISON_MESSAGES = %w[< <= > >= between? clamp sort sort_by max min].freeze
|
|
16
|
+
CORE_ENUMERABLE_RECEIVERS = %w[Array Enumerator Hash Range Set].freeze
|
|
17
|
+
CORE_COMPARISON_RECEIVERS = %w[Array].freeze
|
|
18
|
+
KERNEL_OUTPUT_MESSAGES = %w[print puts warn].freeze
|
|
19
|
+
REFLECTION_HOOK_MESSAGES = %w[method respond_to?].freeze
|
|
20
|
+
BLOCK_DEPENDENT_ENUMERABLE_MESSAGES = %w[
|
|
21
|
+
all? any? chunk collect cycle detect drop_while each_cons each_entry each_slice
|
|
22
|
+
each_with_index each_with_object filter find find_all flat_map group_by
|
|
23
|
+
map none? one? partition reject reverse_each select sort_by take_while
|
|
24
|
+
].freeze
|
|
25
|
+
BLOCK_COMPARISON_MESSAGES = %w[max min sort].freeze
|
|
26
|
+
|
|
27
|
+
attr_reader :pruning, :emit_redundant_edges
|
|
28
|
+
|
|
29
|
+
def initialize(pruning: :rank_only, emit_redundant_edges: true)
|
|
30
|
+
mode = pruning.to_s
|
|
31
|
+
unless Configuration::RTA_PRUNING_MODES.include?(mode)
|
|
32
|
+
raise Error, "RTA pruning must be one of: #{Configuration::RTA_PRUNING_MODES.join(', ')}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
@pruning = mode.to_sym
|
|
36
|
+
@emit_redundant_edges = emit_redundant_edges == true
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def with_pruning(mode, emit_redundant_edges: @emit_redundant_edges)
|
|
40
|
+
self.class.new(pruning: mode, emit_redundant_edges: emit_redundant_edges)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def without_redundant_edges
|
|
44
|
+
self.class.new(pruning: pruning, emit_redundant_edges: false)
|
|
45
|
+
end
|
|
14
46
|
|
|
15
47
|
def analyze(graph, _project)
|
|
16
48
|
sites = expanded_call_sites(graph)
|
|
17
|
-
|
|
18
|
-
|
|
49
|
+
original_call_site_ids = graph.call_sites.to_set(&:call_site_id)
|
|
50
|
+
derived_sites = sites.reject { |site| original_call_site_ids.include?(site.call_site_id) }
|
|
51
|
+
instantiated_metadata = instantiated_classes_metadata(graph)
|
|
52
|
+
analyses = sites.map do |site|
|
|
53
|
+
targets = rta_candidates(graph, site)
|
|
54
|
+
site_edges = targets.filter_map do |candidate|
|
|
55
|
+
# In rank-only mode CHA/name resolution already materialize the
|
|
56
|
+
# same conservative edge. Keep the RTA resolution record, but
|
|
57
|
+
# avoid allocating a duplicate evidence object for an edge that
|
|
58
|
+
# cannot change reachability. Legacy pruning still emits every
|
|
59
|
+
# candidate because reconciliation uses those edge identities.
|
|
60
|
+
next if pruning == :rank_only && !emit_redundant_edges &&
|
|
61
|
+
graph.edge_present?(site.caller_id, candidate.graph_id)
|
|
62
|
+
|
|
19
63
|
EdgeEvidence.new(
|
|
20
64
|
caller_id: site.caller_id,
|
|
21
|
-
callee_id: candidate.
|
|
65
|
+
callee_id: candidate.graph_id,
|
|
22
66
|
evidence: evidence(
|
|
23
67
|
kind: :call_edge,
|
|
24
68
|
details: "RTA candidate at #{site.file}:#{site.line}",
|
|
25
|
-
metadata: site.to_h.merge('
|
|
69
|
+
metadata: site.to_h.merge(instantiated_metadata).merge('target_definition_id' => candidate.graph_id),
|
|
70
|
+
grade: :heuristic,
|
|
71
|
+
relation: :call_edge,
|
|
72
|
+
source: call_site_evidence_source(site).merge('target_definition_id' => candidate.graph_id),
|
|
73
|
+
scope: call_site_evidence_scope(site).merge('target_definition_id' => candidate.graph_id)
|
|
26
74
|
)
|
|
27
75
|
)
|
|
28
76
|
end
|
|
77
|
+
[site, targets, site_edges]
|
|
78
|
+
end
|
|
79
|
+
edge_evidences = analyses.flat_map(&:last)
|
|
80
|
+
analyses_by_call_site_id = analyses.to_h { |site, targets, edges| [site.call_site_id, [targets, edges]] }
|
|
81
|
+
resolutions = sites.map do |site|
|
|
82
|
+
targets, edges = analyses_by_call_site_id.fetch(site.call_site_id)
|
|
83
|
+
resolution_record(site, targets, edges, unknown_scope: graph.residual_scope_for(site))
|
|
29
84
|
end
|
|
30
85
|
|
|
31
86
|
AnalyzerResult.new(
|
|
32
87
|
edge_evidences: edge_evidences,
|
|
33
88
|
alive_evidences: [],
|
|
34
89
|
uncertainties: {},
|
|
35
|
-
observation: { 'rta' => { 'analyzed_sites' => sites.map(&:to_h) } }
|
|
90
|
+
observation: { 'rta' => { 'pruning' => pruning.to_s, 'analyzed_sites' => sites.map(&:to_h) } },
|
|
91
|
+
resolutions: resolutions,
|
|
92
|
+
evidences: result_evidences(edge_evidences),
|
|
93
|
+
derived_call_sites: derived_sites
|
|
36
94
|
)
|
|
37
95
|
end
|
|
38
96
|
|
|
39
97
|
def profile
|
|
98
|
+
description = if pruning == :legacy
|
|
99
|
+
'RTA pruning mode legacy removes static candidates without scanned construction evidence.'
|
|
100
|
+
else
|
|
101
|
+
'RTA pruning mode rank_only records constructed-class hints without removing static edges.'
|
|
102
|
+
end
|
|
40
103
|
AnalyzerProfile.new(
|
|
41
104
|
name: :rta,
|
|
42
105
|
kind: :static,
|
|
43
106
|
soundness: :partial,
|
|
44
|
-
description:
|
|
107
|
+
description: description,
|
|
108
|
+
version: Necropsy::VERSION,
|
|
109
|
+
assumptions: ['scanned_allocations', "pruning=#{pruning}"]
|
|
45
110
|
)
|
|
46
111
|
end
|
|
47
112
|
|
|
113
|
+
def capabilities
|
|
114
|
+
[:complete_resolution].freeze
|
|
115
|
+
end
|
|
116
|
+
|
|
48
117
|
def expanded_call_sites(graph)
|
|
49
118
|
graph.call_sites.flat_map do |site|
|
|
50
|
-
[site, *implicit_sites(site)]
|
|
119
|
+
[site, *implicit_sites(site, graph: graph)]
|
|
51
120
|
end
|
|
52
121
|
end
|
|
53
122
|
|
|
54
|
-
def implicit_sites(site)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
line: site.line,
|
|
63
|
-
test: site.test,
|
|
64
|
-
dynamic: site.dynamic,
|
|
65
|
-
metadata: site.metadata.merge('implicit_from' => site.message)
|
|
66
|
-
)
|
|
123
|
+
def implicit_sites(site, graph: nil)
|
|
124
|
+
summaries = []
|
|
125
|
+
summaries << ['each', :same_receiver] if core_enumerable_receiver?(site) && enumerable_protocol?(site)
|
|
126
|
+
summaries.concat(comparison_summaries(site)) if core_comparison_receiver?(site)
|
|
127
|
+
summaries.concat(output_summaries(site)) if kernel_output_call?(site, graph)
|
|
128
|
+
summaries << ['respond_to_missing?', :same_receiver] if reflection_hook_call?(site, graph)
|
|
129
|
+
summaries.map.with_index do |(message, receiver), index|
|
|
130
|
+
derived_protocol_site(site, message, receiver, index: index)
|
|
67
131
|
end
|
|
68
132
|
end
|
|
69
133
|
|
|
70
|
-
def implicit_messages(
|
|
71
|
-
|
|
72
|
-
messages << 'each' if ENUMERABLE_MESSAGES.include?(message) && message != 'each'
|
|
73
|
-
messages << '<=>' if COMPARISON_MESSAGES.include?(message)
|
|
74
|
-
messages << 'to_s' if %w[puts print warn].include?(message)
|
|
75
|
-
messages
|
|
134
|
+
def implicit_messages(site, graph: nil)
|
|
135
|
+
implicit_sites(site, graph: graph).map(&:message)
|
|
76
136
|
end
|
|
77
137
|
|
|
78
138
|
private
|
|
79
139
|
|
|
140
|
+
def derived_protocol_site(site, message, receiver, index: 0)
|
|
141
|
+
receiver_kind, receiver_name, receiver_metadata = protocol_receiver(site, receiver)
|
|
142
|
+
receiver_label = protocol_receiver_label(receiver)
|
|
143
|
+
CallSite.new(
|
|
144
|
+
call_site_id: CallSiteIdentity.derived_id(
|
|
145
|
+
parent_call_site_id: site.call_site_id,
|
|
146
|
+
derivation: :rta_implicit,
|
|
147
|
+
caller_definition_id: site.caller_id,
|
|
148
|
+
message: message,
|
|
149
|
+
discriminator: index.positive? ? "#{receiver_label}:#{index}" : nil
|
|
150
|
+
),
|
|
151
|
+
caller_id: site.caller_id,
|
|
152
|
+
message: message,
|
|
153
|
+
receiver_kind: receiver_kind,
|
|
154
|
+
receiver_name: receiver_name,
|
|
155
|
+
file: site.file,
|
|
156
|
+
line: site.line,
|
|
157
|
+
test: site.test,
|
|
158
|
+
dynamic: false,
|
|
159
|
+
metadata: site.metadata.except('receiver_candidates', 'receiver_value_fact').merge(
|
|
160
|
+
receiver_metadata,
|
|
161
|
+
'implicit_from' => site.message,
|
|
162
|
+
'protocol_receiver' => receiver_label,
|
|
163
|
+
'derived_from_call_site_id' => site.call_site_id,
|
|
164
|
+
'derived_via' => 'rta_implicit'
|
|
165
|
+
).tap do |metadata|
|
|
166
|
+
metadata['implicit_private_dispatch'] = true if message == 'respond_to_missing?'
|
|
167
|
+
end
|
|
168
|
+
)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def protocol_receiver(site, receiver)
|
|
172
|
+
if receiver == :same_receiver
|
|
173
|
+
metadata = site.metadata.slice('receiver_candidates', 'receiver_value_fact')
|
|
174
|
+
return [site.receiver_kind, site.receiver_name, metadata]
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
argument_index = receiver[1] if receiver.is_a?(Array) && receiver.first == :argument
|
|
178
|
+
fact = Array(site.metadata['argument_value_facts'])[argument_index] if argument_index
|
|
179
|
+
fact = site.metadata.dig('receiver_value_fact', 'summary', 'element_fact') if receiver == :element_receiver
|
|
180
|
+
return receiver_from_fact(fact) if fact
|
|
181
|
+
|
|
182
|
+
[:unknown, nil, { 'receiver_candidates' => [] }]
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def receiver_from_fact(fact)
|
|
186
|
+
return [:unknown, nil, { 'receiver_candidates' => [] }] unless fact.is_a?(Hash)
|
|
187
|
+
return [:unknown, nil, { 'receiver_candidates' => [] }] unless fact['kind'].to_s == 'instance_types'
|
|
188
|
+
return [:unknown, nil, { 'receiver_candidates' => [] }] unless fact['exact'] == true
|
|
189
|
+
|
|
190
|
+
values = Array(fact['values']).map(&:to_s).reject(&:empty?).uniq.sort
|
|
191
|
+
return [:unknown, nil, { 'receiver_candidates' => [] }] if values.empty?
|
|
192
|
+
|
|
193
|
+
[:instance, values.first, { 'receiver_candidates' => values, 'receiver_value_fact' => fact }]
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def core_enumerable_receiver?(site)
|
|
197
|
+
receiver_owners(site).intersect?(CORE_ENUMERABLE_RECEIVERS)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def core_comparison_receiver?(site)
|
|
201
|
+
receiver_owners(site).intersect?(CORE_COMPARISON_RECEIVERS)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def enumerable_protocol?(site)
|
|
205
|
+
return false unless ENUMERABLE_MESSAGES.include?(site.message) && site.message != 'each'
|
|
206
|
+
return true unless BLOCK_DEPENDENT_ENUMERABLE_MESSAGES.include?(site.message)
|
|
207
|
+
|
|
208
|
+
site.metadata['block_kind'] != 'none'
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def comparison_protocol?(site)
|
|
212
|
+
return false unless COMPARISON_MESSAGES.include?(site.message)
|
|
213
|
+
return true unless BLOCK_COMPARISON_MESSAGES.include?(site.message)
|
|
214
|
+
|
|
215
|
+
!site.metadata.key?('block_kind') || %w[none nil dynamic].include?(site.metadata['block_kind'])
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def comparison_summaries(site)
|
|
219
|
+
if site.message == 'sort_by'
|
|
220
|
+
return [] if %w[none nil].include?(site.metadata['block_kind'])
|
|
221
|
+
|
|
222
|
+
return [['<=>', :block_result_receiver]]
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
comparison_protocol?(site) ? [['<=>', :element_receiver]] : []
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def output_summaries(site)
|
|
229
|
+
facts = Array(site.metadata['argument_value_facts'])
|
|
230
|
+
count = [facts.length, site.metadata.dig('arguments', 'positional_count').to_i].max
|
|
231
|
+
count = 1 if count.zero? && !site.metadata.key?('arguments')
|
|
232
|
+
count.times.map { |index| ['to_s', [:argument, index]] }
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def protocol_receiver_label(receiver)
|
|
236
|
+
return 'first_argument' if receiver == [:argument, 0]
|
|
237
|
+
return "argument_#{receiver[1]}" if receiver.is_a?(Array) && receiver.first == :argument
|
|
238
|
+
|
|
239
|
+
receiver.to_s
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def receiver_owners(site)
|
|
243
|
+
values = Array(site.metadata['receiver_candidates'])
|
|
244
|
+
values = [container_receiver_owner(site.metadata['receiver_value_fact'])] if values.empty?
|
|
245
|
+
values = [site.receiver_name] if values.empty?
|
|
246
|
+
values.compact.map(&:to_s)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def container_receiver_owner(fact)
|
|
250
|
+
return unless fact.is_a?(Hash) && fact['kind'].to_s == 'container' && fact['exact'] == true
|
|
251
|
+
|
|
252
|
+
{ 'array' => 'Array', 'hash' => 'Hash' }[fact.dig('summary', 'type').to_s]
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def kernel_output_call?(site, graph)
|
|
256
|
+
return false unless KERNEL_OUTPUT_MESSAGES.include?(site.message)
|
|
257
|
+
return false unless %i[implicit self].include?(site.receiver_kind)
|
|
258
|
+
return true unless graph
|
|
259
|
+
|
|
260
|
+
graph.method_lookup(site).targets.empty?
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def reflection_hook_call?(site, graph)
|
|
264
|
+
return false unless REFLECTION_HOOK_MESSAGES.include?(site.message)
|
|
265
|
+
return true unless graph
|
|
266
|
+
|
|
267
|
+
graph.cha_method_lookup(site).targets.none? { |target| target.name == site.message }
|
|
268
|
+
end
|
|
269
|
+
|
|
80
270
|
def rta_candidates(graph, site)
|
|
81
|
-
candidates =
|
|
271
|
+
candidates = graph.cha_method_lookup(site).targets
|
|
82
272
|
candidates = fallback_candidates(graph, site) if candidates.empty?
|
|
83
273
|
graph.retain_rta_candidates(candidates, site)
|
|
84
274
|
end
|
|
85
275
|
|
|
86
276
|
def fallback_candidates(graph, site)
|
|
87
|
-
|
|
277
|
+
domain = site.test ? :test : :runtime
|
|
278
|
+
return graph.candidate_nodes(site.message, domain: domain) if site.receiver_kind == :unknown
|
|
88
279
|
return [] unless graph.ambiguous_resolution?
|
|
89
280
|
|
|
90
|
-
graph.ambiguous_fallback_candidates(site.message)
|
|
281
|
+
graph.ambiguous_fallback_candidates(site.message, domain: domain)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# A full allocation list in every RTA evidence record makes evidence
|
|
285
|
+
# identity canonicalization quadratic for a repository with many
|
|
286
|
+
# scanned constructors. Keep the complete list for small fixtures
|
|
287
|
+
# (preserving the existing diagnostic shape), and use a deterministic
|
|
288
|
+
# bounded sample plus digest for larger projects.
|
|
289
|
+
def instantiated_classes_metadata(graph)
|
|
290
|
+
classes = graph.instantiated_classes.to_a.sort
|
|
291
|
+
return { 'instantiated_classes' => classes } if classes.length <= 64
|
|
292
|
+
|
|
293
|
+
{
|
|
294
|
+
'instantiated_classes' => classes.first(16),
|
|
295
|
+
'instantiated_classes_count' => classes.length,
|
|
296
|
+
'instantiated_classes_truncated' => true,
|
|
297
|
+
'instantiated_classes_sha256' => Digest::SHA256.hexdigest(classes.join("\0"))
|
|
298
|
+
}
|
|
91
299
|
end
|
|
92
300
|
end
|
|
93
301
|
end
|