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,460 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'prism'
|
|
4
|
+
|
|
5
|
+
module Necropsy
|
|
6
|
+
class FlowInterpreter
|
|
7
|
+
DEFAULT_MAX_STEPS = 512
|
|
8
|
+
MAX_ATOMS = 8
|
|
9
|
+
class StepBudgetExceeded < StandardError; end
|
|
10
|
+
ControlTransfer = Data.define(:kind, :fact)
|
|
11
|
+
|
|
12
|
+
def initialize(constant_resolver:, max_steps: DEFAULT_MAX_STEPS)
|
|
13
|
+
@constant_resolver = constant_resolver
|
|
14
|
+
@max_steps = Integer(max_steps)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def analyze(body)
|
|
18
|
+
@locals = {}
|
|
19
|
+
@receiver_facts = {}.compare_by_identity
|
|
20
|
+
@value_facts = {}.compare_by_identity
|
|
21
|
+
@issues = []
|
|
22
|
+
@return_facts = []
|
|
23
|
+
@steps = 0
|
|
24
|
+
value = evaluate(body)
|
|
25
|
+
FlowResult.new(
|
|
26
|
+
receiver_facts: @receiver_facts,
|
|
27
|
+
value_facts: @value_facts,
|
|
28
|
+
return_fact: return_fact(value),
|
|
29
|
+
issues: @issues,
|
|
30
|
+
steps: @steps
|
|
31
|
+
)
|
|
32
|
+
rescue StepBudgetExceeded
|
|
33
|
+
FlowResult.new(
|
|
34
|
+
receiver_facts: @receiver_facts,
|
|
35
|
+
value_facts: @value_facts,
|
|
36
|
+
return_fact: ValueFact.unknown(:step_budget),
|
|
37
|
+
issues: [*@issues, 'step_budget'],
|
|
38
|
+
steps: @steps
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def evaluate(node)
|
|
45
|
+
return ValueFact.unknown(:missing_value) unless node
|
|
46
|
+
|
|
47
|
+
consume_step!
|
|
48
|
+
value = case node
|
|
49
|
+
when Prism::StatementsNode
|
|
50
|
+
evaluate_statements(node)
|
|
51
|
+
when Prism::EmbeddedStatementsNode
|
|
52
|
+
evaluate(node.statements)
|
|
53
|
+
when Prism::ParenthesesNode
|
|
54
|
+
evaluate(node.body)
|
|
55
|
+
when Prism::IfNode
|
|
56
|
+
evaluate_if(node)
|
|
57
|
+
when Prism::CaseNode
|
|
58
|
+
evaluate_case(node)
|
|
59
|
+
when Prism::ElseNode
|
|
60
|
+
evaluate_clause(node)
|
|
61
|
+
when Prism::WhenNode
|
|
62
|
+
evaluate_when(node)
|
|
63
|
+
when Prism::AndNode, Prism::OrNode
|
|
64
|
+
evaluate_logical(node)
|
|
65
|
+
when Prism::LocalVariableWriteNode
|
|
66
|
+
assign_local(node)
|
|
67
|
+
when Prism::LocalVariableReadNode
|
|
68
|
+
@locals.fetch(node.name, ValueFact.unknown(:unbound_local))
|
|
69
|
+
when Prism::CallNode
|
|
70
|
+
evaluate_call(node)
|
|
71
|
+
when Prism::ReturnNode
|
|
72
|
+
evaluate_return(node)
|
|
73
|
+
when Prism::BreakNode
|
|
74
|
+
evaluate_transfer(node, :break)
|
|
75
|
+
when Prism::NextNode
|
|
76
|
+
evaluate_transfer(node, :next)
|
|
77
|
+
when Prism::ArrayNode
|
|
78
|
+
evaluate_array(node)
|
|
79
|
+
when Prism::HashNode
|
|
80
|
+
evaluate_hash(node)
|
|
81
|
+
when Prism::LambdaNode, Prism::BlockNode
|
|
82
|
+
evaluate_callable(node)
|
|
83
|
+
when Prism::NilNode
|
|
84
|
+
ValueFact.new(kind: :nil, exact: true, nilable: true, origin: :literal)
|
|
85
|
+
when Prism::TrueNode, Prism::FalseNode
|
|
86
|
+
ValueFact.new(kind: :boolean, values: [node.type.to_s.delete_suffix('_node')], origin: :literal)
|
|
87
|
+
when Prism::SymbolNode
|
|
88
|
+
ValueFact.new(kind: :symbol_set, values: [node.value.to_s], origin: :literal)
|
|
89
|
+
when Prism::StringNode
|
|
90
|
+
ValueFact.new(kind: :string_set, values: [node.content], origin: :literal)
|
|
91
|
+
when Prism::InterpolatedStringNode, Prism::InterpolatedSymbolNode
|
|
92
|
+
evaluate_interpolated(node)
|
|
93
|
+
when Prism::ConstantReadNode, Prism::ConstantPathNode
|
|
94
|
+
class_object(node)
|
|
95
|
+
else
|
|
96
|
+
evaluate_unsupported(node)
|
|
97
|
+
end
|
|
98
|
+
@value_facts[node] = value.fact if transfer?(value)
|
|
99
|
+
@value_facts[node] = value unless transfer?(value)
|
|
100
|
+
value
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def evaluate_statements(node)
|
|
104
|
+
value = ValueFact.unknown(:empty_statements)
|
|
105
|
+
node.body.each do |child|
|
|
106
|
+
value = evaluate(child)
|
|
107
|
+
break if transfer?(value)
|
|
108
|
+
end
|
|
109
|
+
value
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def evaluate_clause(node)
|
|
113
|
+
evaluate(node.statements)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def evaluate_if(node)
|
|
117
|
+
predicate = evaluate(node.predicate)
|
|
118
|
+
return predicate if transfer?(predicate)
|
|
119
|
+
|
|
120
|
+
branches = [evaluate_branch(node.statements)]
|
|
121
|
+
branches << (node.subsequent ? evaluate_branch(node.subsequent) : implicit_branch)
|
|
122
|
+
join_branch_results(branches)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def evaluate_case(node)
|
|
126
|
+
predicate = evaluate(node.predicate) if node.predicate
|
|
127
|
+
return predicate if transfer?(predicate)
|
|
128
|
+
|
|
129
|
+
branches = node.conditions.map { |condition| evaluate_branch(condition) }
|
|
130
|
+
branches << if node.else_clause
|
|
131
|
+
evaluate_branch(node.else_clause)
|
|
132
|
+
else
|
|
133
|
+
implicit_branch
|
|
134
|
+
end
|
|
135
|
+
join_branch_results(branches)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def evaluate_when(node)
|
|
139
|
+
node.conditions.each do |condition|
|
|
140
|
+
value = evaluate(condition)
|
|
141
|
+
return value if transfer?(value)
|
|
142
|
+
end
|
|
143
|
+
evaluate(node.statements)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def evaluate_branch(branch)
|
|
147
|
+
previous = @locals
|
|
148
|
+
@locals = previous.dup
|
|
149
|
+
value = evaluate(branch)
|
|
150
|
+
[value, @locals, transfer?(value) ? value.kind : nil]
|
|
151
|
+
ensure
|
|
152
|
+
@locals = previous
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def implicit_branch
|
|
156
|
+
[nil_fact, @locals.dup, nil]
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def join_branch_results(branches)
|
|
160
|
+
normal_branches = branches.reject { |branch| branch[2] }
|
|
161
|
+
return join_transfers(branches) if normal_branches.empty?
|
|
162
|
+
|
|
163
|
+
values = normal_branches.map(&:first)
|
|
164
|
+
branch_locals = normal_branches.map { |branch| branch[1] }
|
|
165
|
+
names = branch_locals.flat_map(&:keys).uniq
|
|
166
|
+
@locals = names.to_h do |name|
|
|
167
|
+
join = join_facts(branch_locals.map { |locals| locals.fetch(name, ValueFact.unknown(:branch_unbound)) })
|
|
168
|
+
[name, join]
|
|
169
|
+
end
|
|
170
|
+
join_facts(values)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def evaluate_logical(node)
|
|
174
|
+
left = evaluate(node.left)
|
|
175
|
+
return left if transfer?(left)
|
|
176
|
+
|
|
177
|
+
right_branch = evaluate_branch(node.right)
|
|
178
|
+
join_branch_results([[left, @locals.dup, nil], right_branch])
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def assign_local(node)
|
|
182
|
+
value = evaluate(node.value)
|
|
183
|
+
return value if transfer?(value)
|
|
184
|
+
|
|
185
|
+
@locals[node.name] = value
|
|
186
|
+
value
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def evaluate_call(node)
|
|
190
|
+
receiver_fact = evaluate(node.receiver) if node.receiver
|
|
191
|
+
return receiver_fact if transfer?(receiver_fact)
|
|
192
|
+
|
|
193
|
+
arguments = Array(node.arguments&.arguments)
|
|
194
|
+
arguments.each do |argument|
|
|
195
|
+
argument_fact = evaluate(argument)
|
|
196
|
+
return argument_fact if transfer?(argument_fact)
|
|
197
|
+
end
|
|
198
|
+
@receiver_facts[node.receiver] = receiver_fact if node.receiver
|
|
199
|
+
return ControlTransfer.new(kind: :raise, fact: ValueFact.unknown(:raised)) if node.name == :raise && !node.receiver
|
|
200
|
+
return transparent_wrapper(node) if transparent_wrapper?(node)
|
|
201
|
+
return container_lookup(receiver_fact, arguments.first) if node.name == :[] && receiver_fact
|
|
202
|
+
return callable_result(receiver_fact) if node.name == :call && receiver_fact
|
|
203
|
+
return direct_constructor(node) if node.name == :new && receiver_fact&.kind == :class_object
|
|
204
|
+
return concatenate_strings(receiver_fact, arguments.first) if node.name == :+ && receiver_fact
|
|
205
|
+
|
|
206
|
+
ValueFact.unknown(:call_result)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def concatenate_strings(receiver_fact, argument)
|
|
210
|
+
argument_fact = @value_facts[argument]
|
|
211
|
+
return ValueFact.unknown(:dynamic_string_concat) unless receiver_fact.exact && argument_fact&.exact
|
|
212
|
+
return ValueFact.unknown(:dynamic_string_concat) unless
|
|
213
|
+
[receiver_fact.kind, argument_fact.kind].all? { |kind| %i[string_set symbol_set].include?(kind) }
|
|
214
|
+
|
|
215
|
+
values = receiver_fact.values.product(argument_fact.values).map { |left, right| "#{left}#{right}" }
|
|
216
|
+
return ValueFact.unknown(:string_product_budget) if values.length > MAX_ATOMS
|
|
217
|
+
|
|
218
|
+
ValueFact.new(kind: :string_set, values: values, origin: :string_concat)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def evaluate_interpolated(node)
|
|
222
|
+
parts = node.parts.map do |part|
|
|
223
|
+
fact = evaluate(part)
|
|
224
|
+
if part.is_a?(Prism::StringNode)
|
|
225
|
+
[part.content.to_s]
|
|
226
|
+
elsif fact.exact && %i[string_set symbol_set].include?(fact.kind)
|
|
227
|
+
fact.values
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
return ValueFact.unknown(:dynamic_interpolation) if parts.any?(&:nil?)
|
|
231
|
+
|
|
232
|
+
values = parts.reduce(['']) do |prefixes, suffixes|
|
|
233
|
+
next [] if prefixes.length * suffixes.length > MAX_ATOMS
|
|
234
|
+
|
|
235
|
+
prefixes.product(suffixes).map { |prefix, suffix| "#{prefix}#{suffix}" }
|
|
236
|
+
end
|
|
237
|
+
return ValueFact.unknown(:string_product_budget) if values.empty? || values.length > MAX_ATOMS
|
|
238
|
+
|
|
239
|
+
ValueFact.new(kind: :string_set, values: values, origin: :interpolation)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def transparent_wrapper?(node)
|
|
243
|
+
node.receiver && constant_name(node.receiver) == 'T' && %i[let cast must unsafe].include?(node.name)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def transparent_wrapper(node)
|
|
247
|
+
argument = Array(node.arguments&.arguments).first
|
|
248
|
+
argument ? (@value_facts[argument] || evaluate(argument)) : ValueFact.unknown(:wrapper_argument)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def evaluate_return(node)
|
|
252
|
+
argument = Array(node.arguments&.arguments).first
|
|
253
|
+
value = argument ? evaluate(argument) : nil_fact
|
|
254
|
+
return value if transfer?(value)
|
|
255
|
+
|
|
256
|
+
@return_facts << value
|
|
257
|
+
ControlTransfer.new(kind: :return, fact: value)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def evaluate_transfer(node, kind)
|
|
261
|
+
argument = Array(node.arguments&.arguments).first
|
|
262
|
+
value = argument ? evaluate(argument) : nil_fact
|
|
263
|
+
return value if transfer?(value)
|
|
264
|
+
|
|
265
|
+
ControlTransfer.new(kind: kind, fact: value)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def evaluate_array(node)
|
|
269
|
+
elements = node.elements
|
|
270
|
+
return ValueFact.unknown(:array_splat) if node.contains_splat?
|
|
271
|
+
|
|
272
|
+
element_facts = elements.map do |element|
|
|
273
|
+
fact = evaluate(element)
|
|
274
|
+
return fact if transfer?(fact)
|
|
275
|
+
|
|
276
|
+
fact
|
|
277
|
+
end
|
|
278
|
+
return ValueFact.unknown(:array_budget) if elements.length > MAX_ATOMS
|
|
279
|
+
|
|
280
|
+
ValueFact.new(kind: :container, exact: true, origin: :literal_array,
|
|
281
|
+
summary: {
|
|
282
|
+
'type' => 'array',
|
|
283
|
+
'size' => elements.length,
|
|
284
|
+
'element_fact' => join_facts(element_facts).to_h
|
|
285
|
+
})
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def evaluate_hash(node)
|
|
289
|
+
elements = node.elements
|
|
290
|
+
return ValueFact.unknown(:hash_budget) if elements.length > MAX_ATOMS
|
|
291
|
+
|
|
292
|
+
complete = true
|
|
293
|
+
entries = { 'symbol' => {}, 'string' => {} }
|
|
294
|
+
elements.each do |element|
|
|
295
|
+
unless element.is_a?(Prism::AssocNode)
|
|
296
|
+
complete = false
|
|
297
|
+
evaluate_children(element)
|
|
298
|
+
next
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
key = literal_key(element.key)
|
|
302
|
+
value = evaluate(element.value)
|
|
303
|
+
return value if transfer?(value)
|
|
304
|
+
|
|
305
|
+
unless key
|
|
306
|
+
complete = false
|
|
307
|
+
evaluate(element.key)
|
|
308
|
+
next
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
kind, key_value = key
|
|
312
|
+
entries.fetch(kind)[key_value] = value.to_h
|
|
313
|
+
end
|
|
314
|
+
ValueFact.new(kind: :container, exact: complete, origin: :literal_hash,
|
|
315
|
+
summary: { 'type' => 'hash', 'size' => elements.length, 'entries' => entries })
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def evaluate_callable(node)
|
|
319
|
+
previous_returns = @return_facts
|
|
320
|
+
@return_facts = []
|
|
321
|
+
value, = evaluate_branch(node.body)
|
|
322
|
+
value = return_fact(value)
|
|
323
|
+
ValueFact.new(kind: :callable_set, values: ['block'], exact: true, origin: :literal_callable,
|
|
324
|
+
summary: { 'return_fact' => value.to_h })
|
|
325
|
+
ensure
|
|
326
|
+
@return_facts = previous_returns
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def container_lookup(receiver_fact, argument)
|
|
330
|
+
return ValueFact.unknown(:not_a_container) unless receiver_fact.kind == :container && receiver_fact.exact
|
|
331
|
+
|
|
332
|
+
key_fact = @value_facts[argument]
|
|
333
|
+
return ValueFact.unknown(:dynamic_container_key) unless
|
|
334
|
+
key_fact&.exact && %i[symbol_set string_set].include?(key_fact.kind)
|
|
335
|
+
|
|
336
|
+
kind = key_fact.kind == :symbol_set ? 'symbol' : 'string'
|
|
337
|
+
entries = receiver_fact.summary&.dig('entries', kind) || {}
|
|
338
|
+
values = key_fact.values.filter_map { |key| entries[key] }.map { |entry| ValueFact.from_h(entry) }
|
|
339
|
+
return ValueFact.unknown(:missing_container_key) unless values.length == key_fact.values.length
|
|
340
|
+
|
|
341
|
+
join_facts(values)
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def callable_result(receiver_fact)
|
|
345
|
+
return ValueFact.unknown(:not_callable) unless receiver_fact.kind == :callable_set
|
|
346
|
+
|
|
347
|
+
return_fact = receiver_fact.summary&.fetch('return_fact', nil)
|
|
348
|
+
return_fact.is_a?(Hash) ? ValueFact.from_h(return_fact) : ValueFact.unknown(:unknown_callable_return)
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def literal_key(node)
|
|
352
|
+
case node
|
|
353
|
+
when Prism::SymbolNode
|
|
354
|
+
['symbol', node.value.to_s]
|
|
355
|
+
when Prism::StringNode
|
|
356
|
+
['string', node.content.to_s]
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def evaluate_unsupported(node)
|
|
361
|
+
written_names = local_writes(node)
|
|
362
|
+
written_names.each { |name| @locals[name] = ValueFact.unknown(node.type) }
|
|
363
|
+
evaluate_children(node)
|
|
364
|
+
written_names.each { |name| @locals[name] = ValueFact.unknown(node.type) }
|
|
365
|
+
ValueFact.unknown(node.type)
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def evaluate_children(node)
|
|
369
|
+
node.child_nodes.compact.each do |child|
|
|
370
|
+
value = evaluate(child)
|
|
371
|
+
break if transfer?(value)
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def local_writes(node)
|
|
376
|
+
pending = [node]
|
|
377
|
+
names = []
|
|
378
|
+
until pending.empty?
|
|
379
|
+
child = pending.pop
|
|
380
|
+
names << child.name if child.type.to_s.match?(/\Alocal_variable_(?:.+_)?write_node\z/)
|
|
381
|
+
pending.concat(child.child_nodes.compact)
|
|
382
|
+
end
|
|
383
|
+
names.uniq
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def join_transfers(branches)
|
|
387
|
+
kinds = branches.map { |branch| branch[2] }.uniq
|
|
388
|
+
return ControlTransfer.new(kind: :unknown, fact: ValueFact.unknown(:mixed_control_transfer)) unless kinds.one?
|
|
389
|
+
|
|
390
|
+
ControlTransfer.new(kind: kinds.first, fact: join_facts(branches.map(&:first).map(&:fact)))
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def transfer?(value)
|
|
394
|
+
value.is_a?(ControlTransfer)
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def return_fact(value)
|
|
398
|
+
normal_value = transfer?(value) ? nil : value
|
|
399
|
+
facts = [*@return_facts, normal_value].compact
|
|
400
|
+
return value.fact if facts.empty? && transfer?(value)
|
|
401
|
+
return facts.first if facts.one?
|
|
402
|
+
|
|
403
|
+
join_facts(facts)
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def nil_fact
|
|
407
|
+
ValueFact.new(kind: :nil, exact: true, nilable: true, origin: :implicit_nil)
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def join_facts(facts)
|
|
411
|
+
facts = Array(facts).compact
|
|
412
|
+
return ValueFact.unknown(:empty_join) if facts.empty?
|
|
413
|
+
return ValueFact.unknown(:unknown_join) if facts.any? { |fact| fact.kind == :unknown || !fact.exact }
|
|
414
|
+
|
|
415
|
+
kinds = facts.map(&:kind).uniq
|
|
416
|
+
return ValueFact.unknown(:mixed_join) unless kinds.one?
|
|
417
|
+
|
|
418
|
+
values = facts.flat_map(&:values).uniq.sort
|
|
419
|
+
return ValueFact.unknown(:atom_budget) if values.length > MAX_ATOMS
|
|
420
|
+
|
|
421
|
+
ValueFact.new(
|
|
422
|
+
kind: kinds.first,
|
|
423
|
+
values: values,
|
|
424
|
+
exact: true,
|
|
425
|
+
nilable: facts.any?(&:nilable),
|
|
426
|
+
origin: :branch_join,
|
|
427
|
+
summary: facts.map(&:summary).compact.uniq.one? ? facts.first.summary : nil
|
|
428
|
+
)
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def class_object(node)
|
|
432
|
+
name = constant_name(node)
|
|
433
|
+
return ValueFact.unknown(:dynamic_constant) unless name
|
|
434
|
+
|
|
435
|
+
ValueFact.new(kind: :class_object, values: [@constant_resolver.call(name)], origin: :constant)
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def direct_constructor(node)
|
|
439
|
+
owner = constant_name(node.receiver)
|
|
440
|
+
return ValueFact.unknown(:dynamic_constructor) unless owner
|
|
441
|
+
|
|
442
|
+
ValueFact.instance_types([@constant_resolver.call(owner)])
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def constant_name(node)
|
|
446
|
+
case node
|
|
447
|
+
when Prism::ConstantReadNode
|
|
448
|
+
node.name.to_s
|
|
449
|
+
when Prism::ConstantPathNode
|
|
450
|
+
prefix = node.parent ? constant_name(node.parent) : ''
|
|
451
|
+
[prefix, node.name.to_s].join('::')
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def consume_step!
|
|
456
|
+
@steps += 1
|
|
457
|
+
raise StepBudgetExceeded if @steps > @max_steps
|
|
458
|
+
end
|
|
459
|
+
end
|
|
460
|
+
end
|