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
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
module Necropsy
|
|
6
|
+
class BoundedCanonicalizer
|
|
7
|
+
MAX_DEPTH = 256
|
|
8
|
+
MAX_ITEMS = 1_000_000
|
|
9
|
+
MAX_STRING_BYTES = 64 * 1024 * 1024
|
|
10
|
+
MAX_TOTAL_BYTES = 256 * 1024 * 1024
|
|
11
|
+
|
|
12
|
+
class Error < StandardError; end
|
|
13
|
+
class CycleError < Error; end
|
|
14
|
+
class LimitError < Error; end
|
|
15
|
+
class UnsupportedTypeError < Error; end
|
|
16
|
+
|
|
17
|
+
def self.dump(value, **limits)
|
|
18
|
+
new(**limits).dump(value)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def initialize(max_depth: MAX_DEPTH, max_items: MAX_ITEMS, max_string_bytes: MAX_STRING_BYTES,
|
|
22
|
+
max_total_bytes: MAX_TOTAL_BYTES)
|
|
23
|
+
@max_depth = positive_limit(max_depth, :max_depth)
|
|
24
|
+
@max_items = positive_limit(max_items, :max_items)
|
|
25
|
+
@max_string_bytes = positive_limit(max_string_bytes, :max_string_bytes)
|
|
26
|
+
@max_total_bytes = positive_limit(max_total_bytes, :max_total_bytes)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Kept for callers that need the canonical Ruby representation rather than bytes.
|
|
30
|
+
def call(value)
|
|
31
|
+
JSON.parse(dump(value), max_nesting: false)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def dump(value)
|
|
35
|
+
reset_state
|
|
36
|
+
output = binary_string
|
|
37
|
+
process([[:value, value, 0, output]])
|
|
38
|
+
output
|
|
39
|
+
rescue JSON::GeneratorError => e
|
|
40
|
+
raise Error, "Could not encode canonical payload: #{e.message}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
attr_reader :max_depth, :max_items, :max_string_bytes, :max_total_bytes, :active_containers
|
|
46
|
+
|
|
47
|
+
def reset_state
|
|
48
|
+
@active_containers = {}
|
|
49
|
+
@items = 0
|
|
50
|
+
@written_bytes = 0
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def process(stack)
|
|
54
|
+
until stack.empty?
|
|
55
|
+
action, *arguments = stack.pop
|
|
56
|
+
send("process_#{action}", stack, *arguments)
|
|
57
|
+
end
|
|
58
|
+
rescue SystemStackError => e
|
|
59
|
+
raise CycleError, "Could not canonicalize payload: #{e.message}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def process_value(stack, value, depth, output)
|
|
63
|
+
count_item!
|
|
64
|
+
check_depth!(depth)
|
|
65
|
+
|
|
66
|
+
case value
|
|
67
|
+
when Hash then start_hash(stack, value, depth, output)
|
|
68
|
+
when Array then start_array(stack, value, depth, output)
|
|
69
|
+
when Symbol then append(output, %(["symbol",#{encoded_string(canonical_string(value.to_s))}]))
|
|
70
|
+
when String then append(output, %(["string",#{encoded_string(canonical_string(value))}]))
|
|
71
|
+
when Integer then append(output, %(["integer",#{json_string(value.to_s)}]))
|
|
72
|
+
when Float then append(output, %(["float",#{json_string(canonical_float(value))}]))
|
|
73
|
+
when true, false then append(output, %(["boolean",#{value}]))
|
|
74
|
+
when nil then append(output, '["nil"]')
|
|
75
|
+
else start_object(stack, value, depth, output)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def start_array(stack, value, depth, output)
|
|
80
|
+
enter_container!(value)
|
|
81
|
+
append(output, '["array",[')
|
|
82
|
+
stack << [:array_next, value, 0, depth, output]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def process_array_next(stack, value, index, depth, output)
|
|
86
|
+
if index >= value.length
|
|
87
|
+
append(output, ']]')
|
|
88
|
+
leave_container(value)
|
|
89
|
+
return
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
append(output, ',') if index.positive?
|
|
93
|
+
stack << [:array_next, value, index + 1, depth, output]
|
|
94
|
+
stack << [:value, value.fetch(index), depth + 1, output]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def start_hash(stack, value, depth, output)
|
|
98
|
+
enter_container!(value)
|
|
99
|
+
stack << [:hash_next, value, value.each_pair, [], depth, output]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def process_hash_next(stack, value, iterator, pairs, depth, output)
|
|
103
|
+
key, item = iterator.next
|
|
104
|
+
pair = binary_string
|
|
105
|
+
append(pair, '[')
|
|
106
|
+
stack << [:hash_pair_complete, value, iterator, pairs, depth, output, pair]
|
|
107
|
+
stack << [:raw, pair, ']']
|
|
108
|
+
stack << [:value, item, depth + 1, pair]
|
|
109
|
+
stack << [:raw, pair, ',']
|
|
110
|
+
stack << [:value, key, depth + 1, pair]
|
|
111
|
+
rescue StopIteration
|
|
112
|
+
append(output, '["hash",[')
|
|
113
|
+
pairs.sort.each_with_index do |pair, index|
|
|
114
|
+
append(output, ',') if index.positive?
|
|
115
|
+
append(output, pair)
|
|
116
|
+
end
|
|
117
|
+
append(output, ']]')
|
|
118
|
+
leave_container(value)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def process_hash_pair_complete(stack, value, iterator, pairs, depth, output, pair)
|
|
122
|
+
pairs << pair
|
|
123
|
+
stack << [:hash_next, value, iterator, pairs, depth, output]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def start_object(stack, value, depth, output)
|
|
127
|
+
raise UnsupportedTypeError, "Unsupported canonical payload type: #{value.class}" unless value.respond_to?(:to_h)
|
|
128
|
+
|
|
129
|
+
enter_container!(value)
|
|
130
|
+
converted = value.to_h
|
|
131
|
+
append(output, %(["object",#{json_string(value.class.name.to_s)},))
|
|
132
|
+
stack << [:object_complete, value, output]
|
|
133
|
+
stack << [:value, converted, depth + 1, output]
|
|
134
|
+
rescue Error
|
|
135
|
+
raise
|
|
136
|
+
rescue StandardError => e
|
|
137
|
+
leave_container(value)
|
|
138
|
+
raise UnsupportedTypeError, "Could not convert #{value.class} to a canonical payload: #{e.class}"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def process_object_complete(_stack, value, output)
|
|
142
|
+
append(output, ']')
|
|
143
|
+
leave_container(value)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def process_raw(_stack, output, bytes)
|
|
147
|
+
append(output, bytes)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def encoded_string(value)
|
|
151
|
+
%([#{json_string(value.fetch(0))},#{json_string(value.fetch(1))}])
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def canonical_string(value)
|
|
155
|
+
check_scalar_size!(value)
|
|
156
|
+
normalized = value.encode(Encoding::UTF_8)
|
|
157
|
+
['UTF-8', normalized.b.unpack1('H*')]
|
|
158
|
+
rescue EncodingError
|
|
159
|
+
[value.encoding.name, value.b.unpack1('H*')]
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def canonical_float(value)
|
|
163
|
+
raise UnsupportedTypeError, 'Canonical payload contains a non-finite float' unless value.finite?
|
|
164
|
+
|
|
165
|
+
value.to_s
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def json_string(value)
|
|
169
|
+
JSON.generate(value)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def append(output, bytes)
|
|
173
|
+
@written_bytes += bytes.bytesize
|
|
174
|
+
raise LimitError, "Canonical payload exceeds maximum size #{max_total_bytes}" if @written_bytes > max_total_bytes
|
|
175
|
+
|
|
176
|
+
output << bytes.b
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def enter_container!(value)
|
|
180
|
+
object_id = value.object_id
|
|
181
|
+
raise CycleError, "Canonical payload contains a cycle at #{value.class}" if active_containers.key?(object_id)
|
|
182
|
+
|
|
183
|
+
active_containers[object_id] = true
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def leave_container(value)
|
|
187
|
+
active_containers.delete(value.object_id)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def check_scalar_size!(value)
|
|
191
|
+
return if value.bytesize <= max_string_bytes
|
|
192
|
+
|
|
193
|
+
raise LimitError, "Canonical string exceeds maximum size #{max_string_bytes}"
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def check_depth!(depth)
|
|
197
|
+
return if depth <= max_depth
|
|
198
|
+
|
|
199
|
+
raise LimitError, "Canonical payload exceeds maximum depth #{max_depth}"
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def count_item!
|
|
203
|
+
@items += 1
|
|
204
|
+
raise LimitError, "Canonical payload exceeds maximum item count #{max_items}" if @items > max_items
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def positive_limit(value, name)
|
|
208
|
+
value = Integer(value)
|
|
209
|
+
raise ArgumentError, "#{name} must be positive" unless value.positive?
|
|
210
|
+
|
|
211
|
+
value
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def binary_string
|
|
215
|
+
String.new(encoding: Encoding::BINARY)
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
require 'fileutils'
|
|
4
4
|
require 'json'
|
|
5
|
+
require 'digest'
|
|
5
6
|
|
|
6
7
|
module Necropsy
|
|
7
8
|
module Cache
|
|
8
9
|
class ScanCache
|
|
9
|
-
VERSION =
|
|
10
|
+
VERSION = 13
|
|
10
11
|
|
|
11
12
|
def initialize(project:)
|
|
12
13
|
@project = project
|
|
@@ -15,22 +16,28 @@ module Necropsy
|
|
|
15
16
|
def fetch(files)
|
|
16
17
|
return yield unless project.config.cache_enabled?
|
|
17
18
|
|
|
18
|
-
metadata =
|
|
19
|
+
metadata = safe_cache_metadata(files)
|
|
20
|
+
return yield unless metadata
|
|
21
|
+
|
|
19
22
|
cached = read(metadata)
|
|
20
23
|
return cached if cached
|
|
21
24
|
|
|
22
25
|
result = yield
|
|
23
26
|
write(metadata, result)
|
|
24
27
|
result
|
|
25
|
-
rescue SystemCallError, JSON::ParserError => e
|
|
26
|
-
warn_cache("Cache unavailable: #{e.message}")
|
|
27
|
-
yield
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
private
|
|
31
31
|
|
|
32
32
|
attr_reader :project
|
|
33
33
|
|
|
34
|
+
def safe_cache_metadata(files)
|
|
35
|
+
cache_metadata(files)
|
|
36
|
+
rescue SystemCallError, JSON::ParserError => e
|
|
37
|
+
warn_cache("Cache unavailable: #{e.message}")
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
|
|
34
41
|
def read(metadata)
|
|
35
42
|
payload = load_payload
|
|
36
43
|
return nil unless payload
|
|
@@ -51,13 +58,18 @@ module Necropsy
|
|
|
51
58
|
|
|
52
59
|
def write(metadata, result)
|
|
53
60
|
FileUtils.mkdir_p(File.dirname(path))
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
payload = JSON.generate(
|
|
62
|
+
'version' => VERSION,
|
|
63
|
+
'metadata' => metadata,
|
|
64
|
+
'scan_result' => serialize_scan_result(result)
|
|
65
|
+
)
|
|
66
|
+
temporary = "#{path}.tmp-#{Process.pid}-#{Thread.current.object_id}"
|
|
67
|
+
File.write(temporary, payload)
|
|
68
|
+
File.rename(temporary, path)
|
|
59
69
|
rescue StandardError => e
|
|
60
70
|
warn_cache("Could not write cache: #{e.message}")
|
|
71
|
+
ensure
|
|
72
|
+
FileUtils.rm_f(temporary) if temporary && File.exist?(temporary)
|
|
61
73
|
nil
|
|
62
74
|
end
|
|
63
75
|
|
|
@@ -70,7 +82,8 @@ module Necropsy
|
|
|
70
82
|
stat = File.stat(file)
|
|
71
83
|
metadata[project.relative_path(file)] = {
|
|
72
84
|
'size' => stat.size,
|
|
73
|
-
'mtime' => "#{stat.mtime.to_i}.#{stat.mtime.nsec}"
|
|
85
|
+
'mtime' => "#{stat.mtime.to_i}.#{stat.mtime.nsec}",
|
|
86
|
+
'content_sha256' => Digest::SHA256.file(file).hexdigest
|
|
74
87
|
}
|
|
75
88
|
end
|
|
76
89
|
end
|
|
@@ -78,7 +91,16 @@ module Necropsy
|
|
|
78
91
|
def cache_metadata(files)
|
|
79
92
|
{
|
|
80
93
|
'files' => file_metadata(files),
|
|
81
|
-
'
|
|
94
|
+
'inventory' => project.scan_inventory_key,
|
|
95
|
+
'configuration' => project.config.scan_cache_key,
|
|
96
|
+
'environment' => {
|
|
97
|
+
'necropsy_version' => Necropsy::VERSION,
|
|
98
|
+
'ruby_engine' => RUBY_ENGINE,
|
|
99
|
+
'ruby_version' => RUBY_VERSION,
|
|
100
|
+
'prism_version' => Prism::VERSION,
|
|
101
|
+
'definition_identity_version' => DefinitionIdentity::VERSION,
|
|
102
|
+
'call_site_identity_version' => CallSiteIdentity::VERSION
|
|
103
|
+
}
|
|
82
104
|
}
|
|
83
105
|
end
|
|
84
106
|
|
|
@@ -89,7 +111,13 @@ module Necropsy
|
|
|
89
111
|
'instantiated_classes' => result.instantiated_classes.to_a.sort,
|
|
90
112
|
'uncertainties' => result.uncertainties.transform_values { |messages| Array(messages).map(&:to_s) },
|
|
91
113
|
'class_infos' => result.class_infos.map(&:to_h),
|
|
92
|
-
'entrypoint_hints' => result.entrypoint_hints.map(&:to_h)
|
|
114
|
+
'entrypoint_hints' => result.entrypoint_hints.map(&:to_h),
|
|
115
|
+
'file_statuses' => result.file_statuses.transform_values(&:to_s),
|
|
116
|
+
'source_errors' => result.source_errors.map(&:to_h),
|
|
117
|
+
'source_domains' => result.source_domains.transform_values(&:to_s),
|
|
118
|
+
'scope_diagnostics' => result.scope_diagnostics,
|
|
119
|
+
'method_signatures' => result.method_signatures,
|
|
120
|
+
'semantic_blockers' => result.semantic_blockers.map(&:to_h)
|
|
93
121
|
}
|
|
94
122
|
end
|
|
95
123
|
|
|
@@ -100,13 +128,45 @@ module Necropsy
|
|
|
100
128
|
instantiated_classes: Set.new(Array(data['instantiated_classes'])),
|
|
101
129
|
uncertainties: deserialize_uncertainties(data['uncertainties']),
|
|
102
130
|
class_infos: Array(data['class_infos']).map { |info| deserialize_class_info(info) },
|
|
103
|
-
entrypoint_hints: Array(data['entrypoint_hints']).map { |entry| deserialize_entry_point(entry) }
|
|
131
|
+
entrypoint_hints: Array(data['entrypoint_hints']).map { |entry| deserialize_entry_point(entry) },
|
|
132
|
+
file_statuses: (data['file_statuses'] || {}).transform_values(&:to_sym),
|
|
133
|
+
source_errors: Array(data['source_errors']).map { |error| deserialize_source_error(error) },
|
|
134
|
+
source_domains: (data['source_domains'] || {}).transform_values(&:to_sym),
|
|
135
|
+
scope_diagnostics: data['scope_diagnostics'] || {},
|
|
136
|
+
method_signatures: data['method_signatures'] || {},
|
|
137
|
+
semantic_blockers: Array(data['semantic_blockers']).map { |blocker| deserialize_blocker(blocker) }
|
|
138
|
+
)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def deserialize_blocker(data)
|
|
142
|
+
Blocker.new(
|
|
143
|
+
kind: data.fetch('kind').to_sym,
|
|
144
|
+
scope_kind: data.fetch('scope_kind').to_sym,
|
|
145
|
+
scope_value: data['scope_value'],
|
|
146
|
+
source: data['source'],
|
|
147
|
+
reason: data.fetch('reason'),
|
|
148
|
+
suggested_action: data.fetch('suggested_action', 'review').to_sym,
|
|
149
|
+
metadata: data['metadata'] || {}
|
|
150
|
+
)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def deserialize_source_error(data)
|
|
154
|
+
SourceError.new(
|
|
155
|
+
file: data['file'],
|
|
156
|
+
line: data['line']&.to_i || 1,
|
|
157
|
+
message: data['message'],
|
|
158
|
+
type: data['type'].to_sym
|
|
104
159
|
)
|
|
105
160
|
end
|
|
106
161
|
|
|
107
162
|
def deserialize_node(data)
|
|
163
|
+
id = data.fetch('id')
|
|
108
164
|
Node.new(
|
|
109
|
-
id:
|
|
165
|
+
id: id,
|
|
166
|
+
symbol_id: data['symbol_id'] || id,
|
|
167
|
+
definition_id: data['definition_id'] || id,
|
|
168
|
+
body_digest: data['body_digest'],
|
|
169
|
+
ordinal: data['ordinal'].to_i,
|
|
110
170
|
kind: data['kind'].to_sym,
|
|
111
171
|
file: data['file'],
|
|
112
172
|
line: data['line'].to_i,
|
|
@@ -121,7 +181,8 @@ module Necropsy
|
|
|
121
181
|
|
|
122
182
|
def deserialize_call_site(data)
|
|
123
183
|
CallSite.new(
|
|
124
|
-
|
|
184
|
+
call_site_id: data.fetch('call_site_id'),
|
|
185
|
+
caller_id: data['caller_definition_id'] || data.fetch('caller_id'),
|
|
125
186
|
message: data['message'],
|
|
126
187
|
receiver_kind: data['receiver_kind'].to_sym,
|
|
127
188
|
receiver_name: data['receiver_name'],
|
|
@@ -144,12 +205,19 @@ module Necropsy
|
|
|
144
205
|
includes: Array(data['includes']),
|
|
145
206
|
prepends: Array(data['prepends']),
|
|
146
207
|
extends: Array(data['extends']),
|
|
208
|
+
singleton_includes: Array(data['singleton_includes']),
|
|
209
|
+
singleton_prepends: Array(data['singleton_prepends']),
|
|
147
210
|
dynamic: data['dynamic']
|
|
148
211
|
)
|
|
149
212
|
end
|
|
150
213
|
|
|
151
214
|
def deserialize_entry_point(data)
|
|
152
|
-
|
|
215
|
+
Root.new(
|
|
216
|
+
definition_id: data['definition_id'] || data['node_id'],
|
|
217
|
+
domain: data['domain'],
|
|
218
|
+
reason: data['reason'].to_sym,
|
|
219
|
+
evidence: data['evidence']
|
|
220
|
+
)
|
|
153
221
|
end
|
|
154
222
|
|
|
155
223
|
def deserialize_uncertainties(data)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require_relative 'bounded_canonicalizer'
|
|
5
|
+
|
|
6
|
+
module Necropsy
|
|
7
|
+
module CallSiteIdentity
|
|
8
|
+
VERSION = 1
|
|
9
|
+
PREFIX = "call:v#{VERSION}".freeze
|
|
10
|
+
SOURCE_ROLES = %i[
|
|
11
|
+
call initialize symbol_reference symbol_to_proc super alias_target delegate_target delegate_message
|
|
12
|
+
].freeze
|
|
13
|
+
DERIVATIONS = %i[module_function rta_implicit].freeze
|
|
14
|
+
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
def source_id(caller_definition_id:, relative_path:, role:, message:, structural_digest:, ordinal:)
|
|
18
|
+
role = role.to_sym
|
|
19
|
+
raise ArgumentError, "invalid call site role: #{role.inspect}" unless SOURCE_ROLES.include?(role)
|
|
20
|
+
|
|
21
|
+
ordinal = Integer(ordinal)
|
|
22
|
+
raise ArgumentError, 'call site ordinal must be positive' unless ordinal.positive?
|
|
23
|
+
|
|
24
|
+
identifier(
|
|
25
|
+
'source', caller_definition_id.to_s, relative_path.to_s, role.to_s, message.to_s,
|
|
26
|
+
structural_digest.to_s, ordinal
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def derived_id(parent_call_site_id:, derivation:, caller_definition_id:, message:, discriminator: nil)
|
|
31
|
+
derivation = derivation.to_sym
|
|
32
|
+
raise ArgumentError, "invalid call site derivation: #{derivation.inspect}" unless DERIVATIONS.include?(derivation)
|
|
33
|
+
|
|
34
|
+
payload = [
|
|
35
|
+
'derived', derivation.to_s, parent_call_site_id.to_s, caller_definition_id.to_s, message.to_s
|
|
36
|
+
]
|
|
37
|
+
payload << discriminator.to_s unless discriminator.nil?
|
|
38
|
+
identifier(*payload)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def legacy_id(caller_definition_id:, message:, receiver_kind:, receiver_name:, file:, line:, test:, dynamic:,
|
|
42
|
+
metadata:)
|
|
43
|
+
identifier(
|
|
44
|
+
'legacy', caller_definition_id.to_s, message.to_s, receiver_kind.to_s, receiver_name&.to_s,
|
|
45
|
+
file.to_s, line, test, dynamic, metadata
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def identifier(*payload)
|
|
50
|
+
"#{PREFIX}:#{Digest::SHA256.hexdigest(BoundedCanonicalizer.dump(payload))}"
|
|
51
|
+
end
|
|
52
|
+
private_class_method :identifier
|
|
53
|
+
end
|
|
54
|
+
end
|