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.
Files changed (131) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +33 -0
  3. data/CHANGELOG.md +44 -0
  4. data/MEASUREMENTS.md +112 -0
  5. data/README.md +233 -16
  6. data/Rakefile +3 -1
  7. data/bench/README.md +92 -0
  8. data/bench/audit.rb +113 -0
  9. data/bench/audits/0.2.1/audit.json +501 -0
  10. data/bench/audits/0.2.1/audit.md +55 -0
  11. data/bench/audits/0.2.1/baseline_performance.yml +16 -0
  12. data/bench/audits/0.2.1/config.yml +42 -0
  13. data/bench/audits/0.2.1/review.yml +56 -0
  14. data/bench/audits/0.2.1/review_queue.yml +3651 -0
  15. data/bench/corpora/v1/README.md +30 -0
  16. data/bench/corpora/v1/labels.yml +37 -0
  17. data/bench/corpora/v1/manifest.yml +63 -0
  18. data/bench/corpora/v1/rubocop.necropsy.yml +7 -0
  19. data/bench/corpora/v1/self.necropsy.yml +8 -0
  20. data/bench/corpora/v1/tools/debride.yml +42 -0
  21. data/bench/corpora/v1/tools/spoom.yml +12 -0
  22. data/bench/corpora/v1/tools/type_aware.yml +11 -0
  23. data/bench/golden/v1/candidate_union.json +21918 -0
  24. data/bench/golden/v1/metadata.json +12 -0
  25. data/bench/golden/v1/reports/dynamic_evidence.json +146 -0
  26. data/bench/golden/v1/reports/plain_ruby.json +135 -0
  27. data/bench/golden/v1/reports/rails.json +176 -0
  28. data/bench/golden/v1/reports/rubocop_1_75_0.json +26848 -0
  29. data/bench/golden/v1/reports/self.json +3449 -0
  30. data/bench/review_queue.rb +35 -0
  31. data/bench/run.rb +31 -0
  32. data/bench/schema/candidate-union-v1.schema.json +70 -0
  33. data/docs/impv_implementation_matrix.md +179 -0
  34. data/docs/migrations/0.2.1.md +57 -0
  35. data/docs/migrations/0.3.0.md +207 -0
  36. data/docs/migrations/0.4.0.md +13 -0
  37. data/docs/necropsy_performance_adr.md +43 -0
  38. data/docs/necropsy_scope_decisions.md +58 -0
  39. data/docs/necropsy_type_facts_adr.md +22 -0
  40. data/gemfiles/prism_min.gemfile +9 -0
  41. data/gemfiles/prism_min.gemfile.lock +50 -0
  42. data/lib/necropsy/analyzer.rb +121 -2
  43. data/lib/necropsy/analyzers/dynamic/coverage_collector.rb +81 -19
  44. data/lib/necropsy/analyzers/dynamic/coverage_importer.rb +104 -10
  45. data/lib/necropsy/analyzers/dynamic/coverband_importer.rb +74 -250
  46. data/lib/necropsy/analyzers/dynamic/coverband_payload_set.rb +149 -0
  47. data/lib/necropsy/analyzers/dynamic/observation_policy.rb +96 -0
  48. data/lib/necropsy/analyzers/dynamic/redis_input_limits.rb +121 -0
  49. data/lib/necropsy/analyzers/dynamic/redis_nonblocking_io.rb +94 -0
  50. data/lib/necropsy/analyzers/dynamic/redis_payload_loader.rb +165 -0
  51. data/lib/necropsy/analyzers/dynamic/redis_transport.rb +217 -0
  52. data/lib/necropsy/analyzers/dynamic/runtime_reference.rb +96 -0
  53. data/lib/necropsy/analyzers/dynamic/trace_point_collector.rb +225 -20
  54. data/lib/necropsy/analyzers/dynamic/trace_point_importer.rb +3 -1
  55. data/lib/necropsy/analyzers/legacy_result_adapter.rb +226 -0
  56. data/lib/necropsy/analyzers/static/cha.rb +33 -69
  57. data/lib/necropsy/analyzers/static/name_resolution.rb +151 -21
  58. data/lib/necropsy/analyzers/static/rta.rb +249 -25
  59. data/lib/necropsy/ast_scanner/call_recording.rb +275 -0
  60. data/lib/necropsy/ast_scanner/call_site_creation.rb +54 -0
  61. data/lib/necropsy/ast_scanner/definition_creation.rb +43 -0
  62. data/lib/necropsy/ast_scanner/dsl_macros.rb +532 -0
  63. data/lib/necropsy/ast_scanner/method_definitions.rb +358 -0
  64. data/lib/necropsy/ast_scanner/references.rb +115 -0
  65. data/lib/necropsy/ast_scanner/ruby_semantics.rb +263 -0
  66. data/lib/necropsy/ast_scanner/traversal.rb +347 -0
  67. data/lib/necropsy/ast_scanner/value_definitions.rb +78 -0
  68. data/lib/necropsy/ast_scanner.rb +89 -564
  69. data/lib/necropsy/bench/candidate_union.rb +555 -0
  70. data/lib/necropsy/bench/claim_gate.rb +112 -0
  71. data/lib/necropsy/bench/evaluator.rb +356 -24
  72. data/lib/necropsy/bench/finding_facts.rb +152 -0
  73. data/lib/necropsy/bench/precision_gate.rb +144 -0
  74. data/lib/necropsy/bench/release_audit/adversarial_runner.rb +56 -0
  75. data/lib/necropsy/bench/release_audit/artifact_writer.rb +112 -0
  76. data/lib/necropsy/bench/release_audit/config_validator.rb +112 -0
  77. data/lib/necropsy/bench/release_audit/git_snapshot.rb +36 -0
  78. data/lib/necropsy/bench/release_audit/performance_gate.rb +165 -0
  79. data/lib/necropsy/bench/release_audit/run_provenance.rb +133 -0
  80. data/lib/necropsy/bench/release_audit.rb +360 -0
  81. data/lib/necropsy/bench/report_normalizer.rb +140 -0
  82. data/lib/necropsy/bench/review_queue.rb +154 -0
  83. data/lib/necropsy/bench/safety_mutation_harness.rb +59 -0
  84. data/lib/necropsy/bench/seed_runner.rb +408 -0
  85. data/lib/necropsy/bounded_canonicalizer.rb +218 -0
  86. data/lib/necropsy/cache/scan_cache.rb +94 -18
  87. data/lib/necropsy/call_site_identity.rb +54 -0
  88. data/lib/necropsy/cli.rb +296 -53
  89. data/lib/necropsy/clock.rb +40 -0
  90. data/lib/necropsy/confidence/scorer.rb +181 -58
  91. data/lib/necropsy/configuration.rb +339 -18
  92. data/lib/necropsy/convention_rules.rb +138 -0
  93. data/lib/necropsy/definition_identity/canonical_digest.rb +278 -0
  94. data/lib/necropsy/definition_identity.rb +37 -0
  95. data/lib/necropsy/diagnostics.rb +342 -0
  96. data/lib/necropsy/embedded_ruby.rb +55 -0
  97. data/lib/necropsy/entry_points/plain.rb +125 -7
  98. data/lib/necropsy/entry_points/rails.rb +422 -105
  99. data/lib/necropsy/entry_points/test.rb +6 -1
  100. data/lib/necropsy/flow_interpreter.rb +460 -0
  101. data/lib/necropsy/graph/blocker_matching.rb +338 -0
  102. data/lib/necropsy/graph/call_graph.rb +1234 -62
  103. data/lib/necropsy/graph/definition_index.rb +149 -0
  104. data/lib/necropsy/graph/dynamic_evidence_tracking.rb +206 -0
  105. data/lib/necropsy/graph/evidence_store.rb +213 -0
  106. data/lib/necropsy/graph/resolution_store.rb +497 -0
  107. data/lib/necropsy/graph_self_check.rb +79 -0
  108. data/lib/necropsy/guardrail/baseline.rb +359 -13
  109. data/lib/necropsy/guardrail/diff.rb +5 -2
  110. data/lib/necropsy/guardrail/quarantine.rb +101 -10
  111. data/lib/necropsy/load_graph.rb +206 -0
  112. data/lib/necropsy/models.rb +894 -21
  113. data/lib/necropsy/performance_profiler.rb +108 -0
  114. data/lib/necropsy/project.rb +361 -13
  115. data/lib/necropsy/reachability/engine.rb +80 -15
  116. data/lib/necropsy/reference_barrier.rb +458 -0
  117. data/lib/necropsy/report.rb +153 -14
  118. data/lib/necropsy/reporter.rb +441 -19
  119. data/lib/necropsy/runner.rb +258 -14
  120. data/lib/necropsy/runtime_feedback.rb +136 -0
  121. data/lib/necropsy/semantics_matrix.rb +153 -0
  122. data/lib/necropsy/trace_point_runtime.rb +19 -0
  123. data/lib/necropsy/type_facts.rb +53 -0
  124. data/lib/necropsy/version.rb +1 -1
  125. data/lib/necropsy/why_not_explanation.rb +436 -0
  126. data/lib/necropsy/why_not_renderer.rb +197 -0
  127. data/lib/necropsy/world_policy.rb +90 -0
  128. data/lib/necropsy.rb +39 -2
  129. data/schema/necropsy-report-v2.schema.json +366 -0
  130. data/script/measure.rb +20 -0
  131. 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 'yaml'
4
+ require 'json'
5
+ require 'digest'
5
6
 
6
7
  module Necropsy
7
8
  module Cache
8
9
  class ScanCache
9
- VERSION = 3
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 = cache_metadata(files)
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
- YAML.load_file(path)
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
- File.write(path, {
61
+ payload = JSON.generate(
53
62
  'version' => VERSION,
54
63
  'metadata' => metadata,
55
64
  'scan_result' => serialize_scan_result(result)
56
- }.to_yaml)
57
- rescue StandardError
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
- 'configuration' => project.config.scan_cache_key
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: data['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
- caller_id: data['caller_id'],
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
- EntryPoint.new(node_id: data['node_id'], reason: data['reason'].to_sym)
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