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
|
@@ -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
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'fileutils'
|
|
4
|
-
require '
|
|
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,21 +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, Psych::Exception
|
|
26
|
-
yield
|
|
27
28
|
end
|
|
28
29
|
|
|
29
30
|
private
|
|
30
31
|
|
|
31
32
|
attr_reader :project
|
|
32
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
|
+
|
|
33
41
|
def read(metadata)
|
|
34
42
|
payload = load_payload
|
|
35
43
|
return nil unless payload
|
|
@@ -37,24 +45,31 @@ module Necropsy
|
|
|
37
45
|
return nil unless payload['metadata'] == metadata
|
|
38
46
|
|
|
39
47
|
deserialize_scan_result(payload.fetch('scan_result'))
|
|
40
|
-
rescue StandardError
|
|
48
|
+
rescue StandardError => e
|
|
49
|
+
warn_cache("Ignoring invalid cache: #{e.message}")
|
|
41
50
|
nil
|
|
42
51
|
end
|
|
43
52
|
|
|
44
53
|
def load_payload
|
|
45
54
|
return nil unless File.exist?(path)
|
|
46
55
|
|
|
47
|
-
|
|
56
|
+
JSON.parse(File.read(path))
|
|
48
57
|
end
|
|
49
58
|
|
|
50
59
|
def write(metadata, result)
|
|
51
60
|
FileUtils.mkdir_p(File.dirname(path))
|
|
52
|
-
|
|
61
|
+
payload = JSON.generate(
|
|
53
62
|
'version' => VERSION,
|
|
54
63
|
'metadata' => metadata,
|
|
55
64
|
'scan_result' => serialize_scan_result(result)
|
|
56
|
-
|
|
57
|
-
|
|
65
|
+
)
|
|
66
|
+
temporary = "#{path}.tmp-#{Process.pid}-#{Thread.current.object_id}"
|
|
67
|
+
File.write(temporary, payload)
|
|
68
|
+
File.rename(temporary, path)
|
|
69
|
+
rescue StandardError => e
|
|
70
|
+
warn_cache("Could not write cache: #{e.message}")
|
|
71
|
+
ensure
|
|
72
|
+
FileUtils.rm_f(temporary) if temporary && File.exist?(temporary)
|
|
58
73
|
nil
|
|
59
74
|
end
|
|
60
75
|
|
|
@@ -67,7 +82,8 @@ module Necropsy
|
|
|
67
82
|
stat = File.stat(file)
|
|
68
83
|
metadata[project.relative_path(file)] = {
|
|
69
84
|
'size' => stat.size,
|
|
70
|
-
'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
|
|
71
87
|
}
|
|
72
88
|
end
|
|
73
89
|
end
|
|
@@ -75,7 +91,16 @@ module Necropsy
|
|
|
75
91
|
def cache_metadata(files)
|
|
76
92
|
{
|
|
77
93
|
'files' => file_metadata(files),
|
|
78
|
-
'
|
|
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
|
+
}
|
|
79
104
|
}
|
|
80
105
|
end
|
|
81
106
|
|
|
@@ -86,7 +111,13 @@ module Necropsy
|
|
|
86
111
|
'instantiated_classes' => result.instantiated_classes.to_a.sort,
|
|
87
112
|
'uncertainties' => result.uncertainties.transform_values { |messages| Array(messages).map(&:to_s) },
|
|
88
113
|
'class_infos' => result.class_infos.map(&:to_h),
|
|
89
|
-
'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)
|
|
90
121
|
}
|
|
91
122
|
end
|
|
92
123
|
|
|
@@ -97,13 +128,45 @@ module Necropsy
|
|
|
97
128
|
instantiated_classes: Set.new(Array(data['instantiated_classes'])),
|
|
98
129
|
uncertainties: deserialize_uncertainties(data['uncertainties']),
|
|
99
130
|
class_infos: Array(data['class_infos']).map { |info| deserialize_class_info(info) },
|
|
100
|
-
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
|
|
101
159
|
)
|
|
102
160
|
end
|
|
103
161
|
|
|
104
162
|
def deserialize_node(data)
|
|
163
|
+
id = data.fetch('id')
|
|
105
164
|
Node.new(
|
|
106
|
-
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,
|
|
107
170
|
kind: data['kind'].to_sym,
|
|
108
171
|
file: data['file'],
|
|
109
172
|
line: data['line'].to_i,
|
|
@@ -111,13 +174,15 @@ module Necropsy
|
|
|
111
174
|
defined_via: data['defined_via'].to_sym,
|
|
112
175
|
owner: data['owner'],
|
|
113
176
|
name: data['name'],
|
|
114
|
-
test: data['test']
|
|
177
|
+
test: data['test'],
|
|
178
|
+
visibility: (data['visibility'] || 'public').to_sym
|
|
115
179
|
)
|
|
116
180
|
end
|
|
117
181
|
|
|
118
182
|
def deserialize_call_site(data)
|
|
119
183
|
CallSite.new(
|
|
120
|
-
|
|
184
|
+
call_site_id: data.fetch('call_site_id'),
|
|
185
|
+
caller_id: data['caller_definition_id'] || data.fetch('caller_id'),
|
|
121
186
|
message: data['message'],
|
|
122
187
|
receiver_kind: data['receiver_kind'].to_sym,
|
|
123
188
|
receiver_name: data['receiver_name'],
|
|
@@ -140,12 +205,19 @@ module Necropsy
|
|
|
140
205
|
includes: Array(data['includes']),
|
|
141
206
|
prepends: Array(data['prepends']),
|
|
142
207
|
extends: Array(data['extends']),
|
|
208
|
+
singleton_includes: Array(data['singleton_includes']),
|
|
209
|
+
singleton_prepends: Array(data['singleton_prepends']),
|
|
143
210
|
dynamic: data['dynamic']
|
|
144
211
|
)
|
|
145
212
|
end
|
|
146
213
|
|
|
147
214
|
def deserialize_entry_point(data)
|
|
148
|
-
|
|
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
|
+
)
|
|
149
221
|
end
|
|
150
222
|
|
|
151
223
|
def deserialize_uncertainties(data)
|
|
@@ -153,6 +225,10 @@ module Necropsy
|
|
|
153
225
|
(data || {}).each { |node_id, messages| uncertainties[node_id] = Array(messages) }
|
|
154
226
|
end
|
|
155
227
|
end
|
|
228
|
+
|
|
229
|
+
def warn_cache(message)
|
|
230
|
+
warn "Necropsy: #{message}" if project.config.verbose?
|
|
231
|
+
end
|
|
156
232
|
end
|
|
157
233
|
end
|
|
158
234
|
end
|
|
@@ -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
|