branchproof 0.8.0 → 0.9.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 +18 -0
- data/README.md +43 -10
- data/doc/Branchproof/DefaultInstrumentation.md +13 -0
- data/doc/Branchproof/DefaultRuntime.md +11 -0
- data/doc/Branchproof/DefaultSyntax.md +7 -0
- data/doc/Branchproof/ExceptionInstrumentation.md +9 -0
- data/doc/Branchproof/ExceptionRuntime.md +27 -0
- data/doc/Branchproof/ExceptionSyntax.md +9 -0
- data/doc/Branchproof/ExtendedAlternativeRuntime.md +19 -0
- data/doc/Branchproof/Instrumenter.md +1 -1
- data/doc/Branchproof/IterationInstrumentation.md +7 -0
- data/doc/Branchproof/IterationRuntime.md +21 -0
- data/doc/Branchproof/IterationSyntax.md +11 -0
- data/doc/Branchproof/Runtime.md +56 -1
- data/doc/Branchproof/Source.md +7 -1
- data/doc/Branchproof/ValueInstrumentation.md +7 -0
- data/doc/Branchproof/ValueRuntime.md +26 -0
- data/doc/Branchproof/ValueSyntax.md +26 -0
- data/doc/Branchproof.md +17 -3
- data/doc/CHANGELOG.md +18 -0
- data/doc/README.md +43 -10
- data/lib/branchproof/decision_syntax.rb +19 -9
- data/lib/branchproof/default_instrumentation.rb +140 -0
- data/lib/branchproof/default_runtime.rb +16 -0
- data/lib/branchproof/default_syntax.rb +94 -0
- data/lib/branchproof/evidence.rb +83 -1
- data/lib/branchproof/exception_instrumentation.rb +96 -0
- data/lib/branchproof/exception_runtime.rb +41 -0
- data/lib/branchproof/exception_syntax.rb +157 -0
- data/lib/branchproof/extended_alternative_runtime.rb +23 -0
- data/lib/branchproof/flow_instrumentation.rb +36 -7
- data/lib/branchproof/instrumenter.rb +50 -5
- data/lib/branchproof/iteration_instrumentation.rb +66 -0
- data/lib/branchproof/iteration_runtime.rb +79 -0
- data/lib/branchproof/iteration_syntax.rb +75 -0
- data/lib/branchproof/loader.rb +6 -1
- data/lib/branchproof/runtime.rb +10 -0
- data/lib/branchproof/source.rb +59 -32
- data/lib/branchproof/value_instrumentation.rb +35 -0
- data/lib/branchproof/value_runtime.rb +73 -0
- data/lib/branchproof/value_syntax.rb +116 -0
- data/lib/branchproof/version.rb +1 -1
- data/llms.txt +17 -3
- data/sig/branchproof.rbs +17 -0
- metadata +27 -1
data/lib/branchproof/source.rb
CHANGED
|
@@ -4,12 +4,23 @@ require "digest"
|
|
|
4
4
|
require "pathname"
|
|
5
5
|
require "prism"
|
|
6
6
|
require_relative "decision_syntax"
|
|
7
|
+
require_relative "iteration_syntax"
|
|
8
|
+
require_relative "exception_syntax"
|
|
9
|
+
require_relative "default_syntax"
|
|
10
|
+
require_relative "value_syntax"
|
|
7
11
|
require_relative "constraints"
|
|
8
12
|
|
|
9
13
|
module Branchproof
|
|
10
14
|
# Inventories supported condition and decision occurrences from Ruby files.
|
|
11
15
|
class Source
|
|
12
16
|
include DecisionSyntax
|
|
17
|
+
# Contextual syntax modules are layered after the baseline classifier so
|
|
18
|
+
# each can extend discovery without coupling this walker to every syntax
|
|
19
|
+
# family. Additional modules can follow the same seam.
|
|
20
|
+
include IterationSyntax
|
|
21
|
+
include ExceptionSyntax
|
|
22
|
+
prepend DefaultSyntax
|
|
23
|
+
prepend ValueSyntax
|
|
13
24
|
|
|
14
25
|
attr_reader :root, :limits
|
|
15
26
|
|
|
@@ -114,8 +125,9 @@ module Branchproof
|
|
|
114
125
|
phase_one_nodes = []
|
|
115
126
|
phase_two_nodes = []
|
|
116
127
|
flow_nodes = []
|
|
128
|
+
value_nodes = []
|
|
117
129
|
|
|
118
|
-
|
|
130
|
+
walk_skipping_defined_operands(program) do |node|
|
|
119
131
|
if node.is_a?(Prism::DefinedNode)
|
|
120
132
|
defined_ranges << node.location
|
|
121
133
|
elsif node.is_a?(Prism::InNode)
|
|
@@ -123,12 +135,14 @@ module Branchproof
|
|
|
123
135
|
guard_patterns[pattern] = true if pattern.is_a?(Prism::IfNode) || pattern.is_a?(Prism::UnlessNode)
|
|
124
136
|
end
|
|
125
137
|
phase_one_nodes << node if decision_node?(node) || subjectless_case?(node)
|
|
126
|
-
phase_two_nodes << node if boolean_node?(node) || node.is_a?(Prism::MatchPredicateNode)
|
|
138
|
+
phase_two_nodes << node if boolean_node?(node) || node.is_a?(Prism::MatchPredicateNode) ||
|
|
139
|
+
node.is_a?(Prism::DefinedNode)
|
|
127
140
|
flow_nodes << node if flow_decision_node?(node)
|
|
141
|
+
value_nodes << node if respond_to?(:value_candidate_node?, true) && value_candidate_node?(node)
|
|
128
142
|
end
|
|
129
143
|
|
|
130
144
|
{ defined_ranges: defined_ranges, guard_patterns: guard_patterns, phase_one_nodes: phase_one_nodes,
|
|
131
|
-
phase_two_nodes: phase_two_nodes, flow_nodes: flow_nodes }
|
|
145
|
+
phase_two_nodes: phase_two_nodes, flow_nodes: flow_nodes, value_nodes: value_nodes }
|
|
132
146
|
end
|
|
133
147
|
|
|
134
148
|
def decisions_for(program, bytes, source_id, file_reasons = [], encoding = "UTF-8")
|
|
@@ -162,15 +176,17 @@ module Branchproof
|
|
|
162
176
|
# argument) are separate decisions when tree_for did not decompose them.
|
|
163
177
|
collected[:phase_two_nodes].each do |node|
|
|
164
178
|
next if inventoried_boolean_nodes[node]
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
179
|
+
next if within_defined_expression?(node, defined_ranges) && !node.is_a?(Prism::DefinedNode)
|
|
180
|
+
|
|
181
|
+
context = if node.is_a?(Prism::MatchPredicateNode)
|
|
182
|
+
"pattern_in"
|
|
183
|
+
elsif node.is_a?(Prism::DefinedNode)
|
|
184
|
+
"defined"
|
|
185
|
+
else
|
|
186
|
+
"short_circuit"
|
|
187
|
+
end
|
|
172
188
|
specs << { node: nil, predicate: node, context: context,
|
|
173
|
-
additional_reasons:
|
|
189
|
+
additional_reasons: [] }
|
|
174
190
|
mark_semantic_boolean_nodes(node, inventoried_boolean_nodes)
|
|
175
191
|
end
|
|
176
192
|
|
|
@@ -188,22 +204,24 @@ module Branchproof
|
|
|
188
204
|
predicate: predicate, context: spec[:context],
|
|
189
205
|
additional_reasons: spec[:additional_reasons] || [])
|
|
190
206
|
end
|
|
191
|
-
boolean_decisions + flow_decisions_for(program, bytes, source_id, file_reasons, encoding,
|
|
192
|
-
|
|
207
|
+
decisions = boolean_decisions + flow_decisions_for(program, bytes, source_id, file_reasons, encoding,
|
|
208
|
+
defined_ranges: defined_ranges,
|
|
209
|
+
nodes: collected[:flow_nodes])
|
|
210
|
+
decisions + additional_decisions_for(program, bytes, source_id, file_reasons, encoding,
|
|
211
|
+
collected: collected, decisions: decisions)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def additional_decisions_for(_program, _bytes, _source_id, _file_reasons, _encoding, **_options)
|
|
215
|
+
[]
|
|
193
216
|
end
|
|
194
217
|
|
|
195
218
|
def flow_decisions_for(program, bytes, source_id, file_reasons = [], encoding = "UTF-8",
|
|
196
219
|
defined_ranges: nil, nodes: nil)
|
|
197
220
|
defined_ranges ||= collect_defined_ranges(program)
|
|
198
221
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
decision.merge(
|
|
203
|
-
support_status: "UNSUPPORTED",
|
|
204
|
-
support_reasons: (Array(decision[:support_reasons]) + ["unsupported_defined_expression"]).uniq
|
|
205
|
-
)
|
|
206
|
-
end
|
|
222
|
+
nodes ||= collect_flow_decision_nodes(program)
|
|
223
|
+
nodes = nodes.reject { |node| within_defined_expression?(node, defined_ranges) }
|
|
224
|
+
super(program, bytes, source_id, file_reasons, encoding, nodes: nodes)
|
|
207
225
|
end
|
|
208
226
|
|
|
209
227
|
def collect_defined_ranges(program)
|
|
@@ -271,10 +289,14 @@ module Branchproof
|
|
|
271
289
|
literal_truth = leaf.delete(:_literal_truth)
|
|
272
290
|
constraint = leaf.delete(:_constraint)
|
|
273
291
|
constraint_safe = leaf.delete(:_constraint_safe)
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
292
|
+
contextual = leaf.delete(:_contextual)
|
|
293
|
+
condition = Records.build(id: nil, index: index, byte_start: location.start_offset,
|
|
294
|
+
byte_length: location.length,
|
|
295
|
+
line: location.start_line, column: location.start_column,
|
|
296
|
+
expression: expression, literal_truth: literal_truth, coupling: "unknown",
|
|
297
|
+
constraint: constraint,
|
|
298
|
+
constraint_safe: decision_constraint_safe && constraint_safe == true)
|
|
299
|
+
contextual ? condition.merge(contextual: contextual) : condition
|
|
278
300
|
end
|
|
279
301
|
decision_id = Records.decision_id(source_id: source_id, context: context, byte_start: start_offset,
|
|
280
302
|
byte_length: length, tree: tree)
|
|
@@ -319,7 +341,7 @@ module Branchproof
|
|
|
319
341
|
inventoried_boolean_nodes[node] = true
|
|
320
342
|
mark_semantic_boolean_nodes(node.left, inventoried_boolean_nodes)
|
|
321
343
|
mark_semantic_boolean_nodes(node.right, inventoried_boolean_nodes)
|
|
322
|
-
when Prism::MatchPredicateNode
|
|
344
|
+
when Prism::DefinedNode, Prism::MatchPredicateNode
|
|
323
345
|
inventoried_boolean_nodes[node] = true
|
|
324
346
|
when Prism::CallNode
|
|
325
347
|
mark_semantic_boolean_nodes(node.receiver, inventoried_boolean_nodes) if unary_not?(node)
|
|
@@ -355,6 +377,9 @@ module Branchproof
|
|
|
355
377
|
{ start: location.start_offset, length: location.length }
|
|
356
378
|
end
|
|
357
379
|
}
|
|
380
|
+
leaf[:_contextual] = "implicit_regexp" if node.is_a?(Prism::MatchLastLineNode) ||
|
|
381
|
+
node.is_a?(Prism::InterpolatedMatchLastLineNode)
|
|
382
|
+
leaf[:_contextual] = "flip_flop" if node.is_a?(Prism::FlipFlopNode)
|
|
358
383
|
leaves << leaf
|
|
359
384
|
Records.build(type: :atom, index: leaves.length - 1)
|
|
360
385
|
end
|
|
@@ -393,12 +418,7 @@ module Branchproof
|
|
|
393
418
|
|
|
394
419
|
def unsupported_reasons(predicate, bytes)
|
|
395
420
|
reasons = []
|
|
396
|
-
|
|
397
|
-
if node.is_a?(Prism::MatchLastLineNode) || node.is_a?(Prism::InterpolatedMatchLastLineNode)
|
|
398
|
-
reasons << "unsupported_implicit_regexp"
|
|
399
|
-
end
|
|
400
|
-
reasons << "unsupported_flip_flop" if node.is_a?(Prism::FlipFlopNode)
|
|
401
|
-
reasons << "unsupported_defined_expression" if node.is_a?(Prism::DefinedNode)
|
|
421
|
+
walk_skipping_defined_operands(predicate) do |node|
|
|
402
422
|
reasons << "unsupported_heredoc" if node.respond_to?(:opening_loc) && node.opening_loc &&
|
|
403
423
|
bytes.byteslice(node.opening_loc.start_offset,
|
|
404
424
|
node.opening_loc.length).start_with?("<<")
|
|
@@ -406,6 +426,13 @@ module Branchproof
|
|
|
406
426
|
reasons
|
|
407
427
|
end
|
|
408
428
|
|
|
429
|
+
def walk_skipping_defined_operands(node, &block)
|
|
430
|
+
yield node
|
|
431
|
+
return if node.is_a?(Prism::DefinedNode)
|
|
432
|
+
|
|
433
|
+
node.child_nodes.each { |child| walk_skipping_defined_operands(child, &block) if child }
|
|
434
|
+
end
|
|
435
|
+
|
|
409
436
|
def unwrap_predicate(node)
|
|
410
437
|
while node.is_a?(Prism::ParenthesesNode)
|
|
411
438
|
statements = node.body
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Branchproof
|
|
4
|
+
# Source rewrite for value observations; it preserves the wrapped value.
|
|
5
|
+
module ValueInstrumentation
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def render_flow(bytes, decision, nested, encloses)
|
|
9
|
+
return render_value(bytes, decision, nested, encloses) if decision.dig(:instrumentation, :type) == "value"
|
|
10
|
+
|
|
11
|
+
super
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# rubocop:disable-next Metrics/MethodLength -- success and exception traces differ.
|
|
15
|
+
def render_value(bytes, decision, nested, encloses)
|
|
16
|
+
expression = render_children(bytes, decision[:byte_start], decision[:byte_length], nested, encloses)
|
|
17
|
+
metadata = decision.fetch(:instrumentation)
|
|
18
|
+
runtime = self.class::RUNTIME
|
|
19
|
+
id = decision.fetch(:id).inspect
|
|
20
|
+
domain = metadata.fetch(:domain).to_sym.inspect
|
|
21
|
+
count = decision.fetch(:alternatives).length
|
|
22
|
+
enter = "#{runtime}.enter(#{id}); #{runtime}.set_alternative_count(#{id}, #{count})"
|
|
23
|
+
if metadata.fetch(:domain).to_sym == :dispatch
|
|
24
|
+
"(begin; #{enter}; begin; " \
|
|
25
|
+
"(begin; #{runtime}.dispatch_path(#{id}, (#{expression}), false); end); " \
|
|
26
|
+
"rescue ::Exception; #{runtime}.dispatch_path(#{id}, nil, true); raise; " \
|
|
27
|
+
"ensure; #{runtime}.leave(#{id}); end; end)"
|
|
28
|
+
else
|
|
29
|
+
"(begin; #{enter}; begin; " \
|
|
30
|
+
"#{runtime}.value_path(#{id}, (#{expression}), #{domain}); " \
|
|
31
|
+
"ensure; #{runtime}.leave(#{id}); end; end)"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Branchproof
|
|
4
|
+
# Runtime mapping for value observations.
|
|
5
|
+
module ValueRuntime
|
|
6
|
+
# rubocop:disable Style/CaseEquality, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
7
|
+
def comparison_index(value)
|
|
8
|
+
return 3 if nil.equal?(value)
|
|
9
|
+
return unless Integer === value || Float === value || Rational === value
|
|
10
|
+
return if Float === value && value.nan?
|
|
11
|
+
return 0 if value < 0 # rubocop:disable Style/NumericPredicate
|
|
12
|
+
return 1 if value == 0 # rubocop:disable Style/NumericPredicate
|
|
13
|
+
return 2 if value > 0 # rubocop:disable Style/NumericPredicate
|
|
14
|
+
|
|
15
|
+
nil
|
|
16
|
+
end
|
|
17
|
+
module_function :comparison_index
|
|
18
|
+
# rubocop:enable Style/CaseEquality, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
19
|
+
|
|
20
|
+
def set_alternative_count(decision_id, count)
|
|
21
|
+
frame = current_frame(decision_id)
|
|
22
|
+
frame[:alternative_count] = count if frame
|
|
23
|
+
nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# rubocop:disable-next Metrics/MethodLength -- trace state branches are explicit.
|
|
27
|
+
# rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity -- domain dispatch mirrors the observable value contract.
|
|
28
|
+
def value_path(decision_id, value, domain)
|
|
29
|
+
frame = current_frame(decision_id)
|
|
30
|
+
return value unless frame
|
|
31
|
+
return value if frame[:finished]
|
|
32
|
+
|
|
33
|
+
index = case domain.to_sym
|
|
34
|
+
when :truthiness, :defined then value ? 1 : 0
|
|
35
|
+
when :lookup
|
|
36
|
+
if nil.equal?(value)
|
|
37
|
+
2
|
|
38
|
+
elsif value
|
|
39
|
+
0
|
|
40
|
+
else
|
|
41
|
+
1
|
|
42
|
+
end
|
|
43
|
+
when :comparison then comparison_index(value)
|
|
44
|
+
when :dispatch then 0
|
|
45
|
+
end
|
|
46
|
+
if index
|
|
47
|
+
frame[:observations] = if frame.fetch(:alternative_count, index + 1) == 2
|
|
48
|
+
[[0, index.zero?], [1, index == 1]]
|
|
49
|
+
else
|
|
50
|
+
(0..index).map { |position| [position, position == index] }
|
|
51
|
+
end
|
|
52
|
+
frame[:outcome] = true
|
|
53
|
+
frame[:finished] = true
|
|
54
|
+
end
|
|
55
|
+
value
|
|
56
|
+
rescue NoMethodError, ArgumentError
|
|
57
|
+
# A user-defined <=> may return an incomparable object. Preserve it and
|
|
58
|
+
# leave the vector incomplete instead of inventing a domain bucket.
|
|
59
|
+
value
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def dispatch_path(decision_id, value, raised)
|
|
63
|
+
frame = current_frame(decision_id)
|
|
64
|
+
return value unless frame
|
|
65
|
+
|
|
66
|
+
index = raised ? 1 : 0
|
|
67
|
+
frame[:observations] = [[0, index.zero?], [1, index == 1]]
|
|
68
|
+
frame[:outcome] = true
|
|
69
|
+
frame[:finished] = true
|
|
70
|
+
value
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Branchproof
|
|
6
|
+
# Inventory for expressions whose result is itself the observable value.
|
|
7
|
+
# This deliberately excludes control-flow predicates already owned by Source.
|
|
8
|
+
module ValueSyntax
|
|
9
|
+
BOOLEAN_OPERATORS = %i[== != < <= > >= === =~ !~].freeze
|
|
10
|
+
DISPATCH_METHODS = %i[public_send send __send__].freeze
|
|
11
|
+
|
|
12
|
+
# rubocop:disable-next Metrics/ParameterLists -- source seam mirrors Source#decisions_for.
|
|
13
|
+
def additional_decisions_for(program, bytes, source_id, file_reasons, encoding, decisions:, collected:)
|
|
14
|
+
occupied = decisions.each_with_object({}) do |decision, ranges|
|
|
15
|
+
ranges[range_key(decision[:byte_start], decision[:byte_length])] = true
|
|
16
|
+
Array(decision[:conditions]).each do |condition|
|
|
17
|
+
ranges[range_key(condition[:byte_start], condition[:byte_length])] = true
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
value_decisions_for(
|
|
21
|
+
program, bytes, source_id, nodes: collected.fetch(:value_nodes), occupied_ranges: occupied,
|
|
22
|
+
file_reasons: file_reasons, encoding: encoding
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# `nodes:` is supplied by Source's fused AST walk. It remains optional for
|
|
27
|
+
# the standalone discovery API used by focused syntax tests.
|
|
28
|
+
# rubocop:disable-next Metrics/MethodLength, Metrics/ParameterLists -- source seam mirrors Source#decisions_for.
|
|
29
|
+
def value_decisions_for(program, bytes, source_id, nodes: nil, occupied_ranges: {}, file_reasons: [],
|
|
30
|
+
encoding: "UTF-8")
|
|
31
|
+
occupied = occupied_ranges.transform_keys do |range|
|
|
32
|
+
range.is_a?(Array) ? range_key(*range) : range
|
|
33
|
+
end
|
|
34
|
+
decisions = []
|
|
35
|
+
each_value_node(program, nodes) do |node|
|
|
36
|
+
spec = value_spec(node)
|
|
37
|
+
next unless spec
|
|
38
|
+
|
|
39
|
+
key = range_key(node.location.start_offset, node.location.length)
|
|
40
|
+
next if occupied[key]
|
|
41
|
+
|
|
42
|
+
decisions << build_value_decision(node, spec, bytes, source_id, file_reasons, encoding)
|
|
43
|
+
occupied[key] = true
|
|
44
|
+
end
|
|
45
|
+
decisions
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def value_candidate_node?(node)
|
|
51
|
+
!value_spec(node).nil?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def each_value_node(program, nodes, &)
|
|
55
|
+
return nodes.each(&) if nodes
|
|
56
|
+
|
|
57
|
+
walker = respond_to?(:walk_skipping_defined_operands, true) ? :walk_skipping_defined_operands : :walk
|
|
58
|
+
send(walker, program, &)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def range_key(start_offset, length)
|
|
62
|
+
(start_offset << 32) | length
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
66
|
+
def value_spec(node)
|
|
67
|
+
if Prism.const_defined?(:MatchWriteNode) && node.is_a?(Prism::MatchWriteNode)
|
|
68
|
+
return { context: "match_capture", domain: :truthiness, alternatives: %w[false true], type: "value" }
|
|
69
|
+
end
|
|
70
|
+
return nil unless node.is_a?(Prism::CallNode)
|
|
71
|
+
|
|
72
|
+
name = node.name.to_sym
|
|
73
|
+
if name == :<=>
|
|
74
|
+
return { context: "comparison", domain: :comparison, alternatives: %w[negative zero positive nil],
|
|
75
|
+
type: "value" }
|
|
76
|
+
end
|
|
77
|
+
return { context: "lookup", domain: :lookup, alternatives: %w[truthy false nil], type: "value" } if name == :[]
|
|
78
|
+
if DISPATCH_METHODS.include?(name)
|
|
79
|
+
return { context: "dispatch", domain: :dispatch, alternatives: %w[success exception],
|
|
80
|
+
type: "value" }
|
|
81
|
+
end
|
|
82
|
+
if BOOLEAN_OPERATORS.include?(name) || name.to_s.end_with?("?")
|
|
83
|
+
return { context: "predicate", domain: :truthiness, alternatives: %w[false true],
|
|
84
|
+
type: "value" }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/ParameterLists, Metrics/PerceivedComplexity -- record assembly is intentionally explicit.
|
|
91
|
+
def build_value_decision(node, spec, bytes, source_id, file_reasons, encoding)
|
|
92
|
+
location = node.location
|
|
93
|
+
expression = text_value(bytes.byteslice(location.start_offset, location.length), encoding)
|
|
94
|
+
alternatives = spec.fetch(:alternatives).each_with_index.map do |label, index|
|
|
95
|
+
{ index: index, id: nil, expression: label, byte_start: location.start_offset, byte_length: location.length }
|
|
96
|
+
end
|
|
97
|
+
id = Records.decision_id(source_id: source_id, context: spec.fetch(:context),
|
|
98
|
+
byte_start: location.start_offset, byte_length: location.length,
|
|
99
|
+
tree: nil)
|
|
100
|
+
alternatives = alternatives.map { |item| item.merge(id: Records.condition_id(id, item.fetch(:index))) }
|
|
101
|
+
kind = alternatives.length == 2 ? "implicit" : "multiway"
|
|
102
|
+
reasons = Array(file_reasons)
|
|
103
|
+
reasons += Array(unsupported_reasons(node, bytes)) if respond_to?(:unsupported_reasons, true)
|
|
104
|
+
limit = @limits[:conditions_per_decision] if defined?(@limits) && @limits.respond_to?(:[])
|
|
105
|
+
reasons << "alternative_limit_exceeded" if limit && alternatives.length > limit
|
|
106
|
+
Records.build(id: id, source_id: source_id, kind: kind, context: spec.fetch(:context),
|
|
107
|
+
byte_start: location.start_offset, byte_length: location.length,
|
|
108
|
+
line: location.start_line, column: location.start_column,
|
|
109
|
+
expression: expression, tree: nil, conditions: [],
|
|
110
|
+
alternatives: alternatives, discovered_condition_count: 0,
|
|
111
|
+
support_status: reasons.empty? ? "SUPPORTED" : "UNSUPPORTED",
|
|
112
|
+
support_reasons: reasons.uniq, opaque_ranges: [],
|
|
113
|
+
instrumentation: { type: "value", domain: spec.fetch(:domain) })
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
data/lib/branchproof/version.rb
CHANGED
data/llms.txt
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
| | |
|
|
4
4
|
| --- | --- |
|
|
5
|
-
| **Defined in** | lib/branchproof.rb, lib/branchproof/cli.rb, lib/branchproof/limits.rb, lib/branchproof/loader.rb, lib/branchproof/report.rb, lib/branchproof/source.rb, lib/branchproof/worker.rb, lib/branchproof/project.rb, lib/branchproof/records.rb, lib/branchproof/runtime.rb, lib/branchproof/version.rb, lib/branchproof/analyzer.rb, lib/branchproof/evidence.rb, lib/branchproof/minimizer.rb, lib/branchproof/comparison.rb, lib/branchproof/constraints.rb, lib/branchproof/instrumenter.rb, lib/branchproof/runtime_flow.rb, lib/branchproof/saved_report.rb, lib/branchproof/rails_support.rb, lib/branchproof/coverage_index.rb, lib/branchproof/decision_table.rb, lib/branchproof/focused_report.rb, lib/branchproof/decision_syntax.rb, lib/branchproof/minitest_adapter.rb, lib/branchproof/comparison_report.rb, lib/branchproof/flow_instrumentation.rb |
|
|
5
|
+
| **Defined in** | lib/branchproof.rb, lib/branchproof/cli.rb, lib/branchproof/limits.rb, lib/branchproof/loader.rb, lib/branchproof/report.rb, lib/branchproof/source.rb, lib/branchproof/worker.rb, lib/branchproof/project.rb, lib/branchproof/records.rb, lib/branchproof/runtime.rb, lib/branchproof/version.rb, lib/branchproof/analyzer.rb, lib/branchproof/evidence.rb, lib/branchproof/minimizer.rb, lib/branchproof/comparison.rb, lib/branchproof/constraints.rb, lib/branchproof/instrumenter.rb, lib/branchproof/runtime_flow.rb, lib/branchproof/saved_report.rb, lib/branchproof/value_syntax.rb, lib/branchproof/rails_support.rb, lib/branchproof/value_runtime.rb, lib/branchproof/coverage_index.rb, lib/branchproof/decision_table.rb, lib/branchproof/default_syntax.rb, lib/branchproof/focused_report.rb, lib/branchproof/decision_syntax.rb, lib/branchproof/default_runtime.rb, lib/branchproof/exception_syntax.rb, lib/branchproof/iteration_syntax.rb, lib/branchproof/minitest_adapter.rb, lib/branchproof/comparison_report.rb, lib/branchproof/exception_runtime.rb, lib/branchproof/iteration_runtime.rb, lib/branchproof/flow_instrumentation.rb, lib/branchproof/value_instrumentation.rb, lib/branchproof/default_instrumentation.rb, lib/branchproof/exception_instrumentation.rb, lib/branchproof/iteration_instrumentation.rb, lib/branchproof/extended_alternative_runtime.rb |
|
|
6
6
|
|
|
7
|
-
Keep
|
|
8
|
-
|
|
7
|
+
Keep source-boundary and parameter-binding rules together for auditing.
|
|
8
|
+
rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity,
|
|
9
|
+
Metrics/MethodLength, Metrics/ModuleLength, Metrics/PerceivedComplexity
|
|
9
10
|
|
|
10
11
|
## Constants
|
|
11
12
|
### `VERSION` <a id="constant-VERSION"></a> <a id="VERSION-constant"></a>
|
|
@@ -22,11 +23,21 @@ Not documented.
|
|
|
22
23
|
- [Branchproof/CoverageIndex.md](doc/Branchproof/CoverageIndex.md)
|
|
23
24
|
- [Branchproof/DecisionSyntax.md](doc/Branchproof/DecisionSyntax.md)
|
|
24
25
|
- [Branchproof/DecisionTable.md](doc/Branchproof/DecisionTable.md)
|
|
26
|
+
- [Branchproof/DefaultInstrumentation.md](doc/Branchproof/DefaultInstrumentation.md)
|
|
27
|
+
- [Branchproof/DefaultRuntime.md](doc/Branchproof/DefaultRuntime.md)
|
|
28
|
+
- [Branchproof/DefaultSyntax.md](doc/Branchproof/DefaultSyntax.md)
|
|
25
29
|
- [Branchproof/Error.md](doc/Branchproof/Error.md)
|
|
26
30
|
- [Branchproof/Evidence.md](doc/Branchproof/Evidence.md)
|
|
31
|
+
- [Branchproof/ExceptionInstrumentation.md](doc/Branchproof/ExceptionInstrumentation.md)
|
|
32
|
+
- [Branchproof/ExceptionRuntime.md](doc/Branchproof/ExceptionRuntime.md)
|
|
33
|
+
- [Branchproof/ExceptionSyntax.md](doc/Branchproof/ExceptionSyntax.md)
|
|
34
|
+
- [Branchproof/ExtendedAlternativeRuntime.md](doc/Branchproof/ExtendedAlternativeRuntime.md)
|
|
27
35
|
- [Branchproof/FlowInstrumentation.md](doc/Branchproof/FlowInstrumentation.md)
|
|
28
36
|
- [Branchproof/FocusedReport.md](doc/Branchproof/FocusedReport.md)
|
|
29
37
|
- [Branchproof/Instrumenter.md](doc/Branchproof/Instrumenter.md)
|
|
38
|
+
- [Branchproof/IterationInstrumentation.md](doc/Branchproof/IterationInstrumentation.md)
|
|
39
|
+
- [Branchproof/IterationRuntime.md](doc/Branchproof/IterationRuntime.md)
|
|
40
|
+
- [Branchproof/IterationSyntax.md](doc/Branchproof/IterationSyntax.md)
|
|
30
41
|
- [Branchproof/Limits.md](doc/Branchproof/Limits.md)
|
|
31
42
|
- [Branchproof/Loader.md](doc/Branchproof/Loader.md)
|
|
32
43
|
- [Branchproof/Minimizer.md](doc/Branchproof/Minimizer.md)
|
|
@@ -40,6 +51,9 @@ Not documented.
|
|
|
40
51
|
- [Branchproof/RuntimeFlow.md](doc/Branchproof/RuntimeFlow.md)
|
|
41
52
|
- [Branchproof/SavedReport.md](doc/Branchproof/SavedReport.md)
|
|
42
53
|
- [Branchproof/Source.md](doc/Branchproof/Source.md)
|
|
54
|
+
- [Branchproof/ValueInstrumentation.md](doc/Branchproof/ValueInstrumentation.md)
|
|
55
|
+
- [Branchproof/ValueRuntime.md](doc/Branchproof/ValueRuntime.md)
|
|
56
|
+
- [Branchproof/ValueSyntax.md](doc/Branchproof/ValueSyntax.md)
|
|
43
57
|
- [Branchproof/Worker.md](doc/Branchproof/Worker.md)
|
|
44
58
|
- [CHANGELOG.md](doc/CHANGELOG.md)
|
|
45
59
|
- [README.md](doc/README.md)
|
data/sig/branchproof.rbs
CHANGED
|
@@ -68,6 +68,23 @@ module Branchproof
|
|
|
68
68
|
def self.flow_path: (String decision_id, Integer index) -> nil
|
|
69
69
|
def self.flow_finish: (String decision_id, untyped value, ?Integer? default_path) -> untyped
|
|
70
70
|
def self.flow_receiver: (String decision_id, untyped receiver) -> untyped
|
|
71
|
+
def self.flow_assignment_receiver: (String decision_id, untyped receiver) -> untyped
|
|
72
|
+
def self.flow_assignment_path: (String decision_id, Integer index) -> nil
|
|
73
|
+
def self.flow_assignment_finish: (String decision_id, untyped value, ?Integer? default_path) -> untyped
|
|
74
|
+
def self.exception_enter: (String decision_id, Integer unhandled_index) -> nil
|
|
75
|
+
def self.exception_path: (String decision_id, Integer index) -> nil
|
|
76
|
+
def self.exception_value: (String decision_id, untyped value, Integer index) -> untyped
|
|
77
|
+
def self.exception_finish: (String decision_id, untyped value) -> untyped
|
|
78
|
+
def self.exception_unhandled: (String decision_id) -> nil
|
|
79
|
+
def self.exception_leave: (String decision_id) -> nil
|
|
80
|
+
def self.default_binding: (String decision_id, Integer index) -> nil
|
|
81
|
+
def self.set_alternative_count: (String decision_id, Integer count) -> nil
|
|
82
|
+
def self.value_path: (String decision_id, untyped value, Symbol domain) -> untyped
|
|
83
|
+
def self.dispatch_path: (String decision_id, untyped value, bool raised) -> untyped
|
|
84
|
+
def self.flow_iteration_begin: (String decision_id, untyped receiver, ?Integer alternative_count) -> untyped
|
|
85
|
+
def self.flow_iteration_callback: (String decision_id, ?Integer alternative_count) -> nil
|
|
86
|
+
def self.flow_iteration_finish: (String decision_id, untyped value, ?Integer? default_path) -> untyped
|
|
87
|
+
def self.flow_iteration_leave: (String decision_id) -> nil
|
|
71
88
|
def self.context: (?test_id: String?, ?phase: String?) -> nil
|
|
72
89
|
def self.snapshot: () -> record
|
|
73
90
|
def self.diagnostics: () -> Array[record]
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: branchproof
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Lucian Ghinda
|
|
@@ -75,11 +75,21 @@ files:
|
|
|
75
75
|
- doc/Branchproof/CoverageIndex.md
|
|
76
76
|
- doc/Branchproof/DecisionSyntax.md
|
|
77
77
|
- doc/Branchproof/DecisionTable.md
|
|
78
|
+
- doc/Branchproof/DefaultInstrumentation.md
|
|
79
|
+
- doc/Branchproof/DefaultRuntime.md
|
|
80
|
+
- doc/Branchproof/DefaultSyntax.md
|
|
78
81
|
- doc/Branchproof/Error.md
|
|
79
82
|
- doc/Branchproof/Evidence.md
|
|
83
|
+
- doc/Branchproof/ExceptionInstrumentation.md
|
|
84
|
+
- doc/Branchproof/ExceptionRuntime.md
|
|
85
|
+
- doc/Branchproof/ExceptionSyntax.md
|
|
86
|
+
- doc/Branchproof/ExtendedAlternativeRuntime.md
|
|
80
87
|
- doc/Branchproof/FlowInstrumentation.md
|
|
81
88
|
- doc/Branchproof/FocusedReport.md
|
|
82
89
|
- doc/Branchproof/Instrumenter.md
|
|
90
|
+
- doc/Branchproof/IterationInstrumentation.md
|
|
91
|
+
- doc/Branchproof/IterationRuntime.md
|
|
92
|
+
- doc/Branchproof/IterationSyntax.md
|
|
83
93
|
- doc/Branchproof/Limits.md
|
|
84
94
|
- doc/Branchproof/Loader.md
|
|
85
95
|
- doc/Branchproof/Minimizer.md
|
|
@@ -93,6 +103,9 @@ files:
|
|
|
93
103
|
- doc/Branchproof/RuntimeFlow.md
|
|
94
104
|
- doc/Branchproof/SavedReport.md
|
|
95
105
|
- doc/Branchproof/Source.md
|
|
106
|
+
- doc/Branchproof/ValueInstrumentation.md
|
|
107
|
+
- doc/Branchproof/ValueRuntime.md
|
|
108
|
+
- doc/Branchproof/ValueSyntax.md
|
|
96
109
|
- doc/Branchproof/Worker.md
|
|
97
110
|
- doc/CHANGELOG.md
|
|
98
111
|
- doc/README.md
|
|
@@ -107,10 +120,20 @@ files:
|
|
|
107
120
|
- lib/branchproof/coverage_index.rb
|
|
108
121
|
- lib/branchproof/decision_syntax.rb
|
|
109
122
|
- lib/branchproof/decision_table.rb
|
|
123
|
+
- lib/branchproof/default_instrumentation.rb
|
|
124
|
+
- lib/branchproof/default_runtime.rb
|
|
125
|
+
- lib/branchproof/default_syntax.rb
|
|
110
126
|
- lib/branchproof/evidence.rb
|
|
127
|
+
- lib/branchproof/exception_instrumentation.rb
|
|
128
|
+
- lib/branchproof/exception_runtime.rb
|
|
129
|
+
- lib/branchproof/exception_syntax.rb
|
|
130
|
+
- lib/branchproof/extended_alternative_runtime.rb
|
|
111
131
|
- lib/branchproof/flow_instrumentation.rb
|
|
112
132
|
- lib/branchproof/focused_report.rb
|
|
113
133
|
- lib/branchproof/instrumenter.rb
|
|
134
|
+
- lib/branchproof/iteration_instrumentation.rb
|
|
135
|
+
- lib/branchproof/iteration_runtime.rb
|
|
136
|
+
- lib/branchproof/iteration_syntax.rb
|
|
114
137
|
- lib/branchproof/limits.rb
|
|
115
138
|
- lib/branchproof/loader.rb
|
|
116
139
|
- lib/branchproof/minimizer.rb
|
|
@@ -123,6 +146,9 @@ files:
|
|
|
123
146
|
- lib/branchproof/runtime_flow.rb
|
|
124
147
|
- lib/branchproof/saved_report.rb
|
|
125
148
|
- lib/branchproof/source.rb
|
|
149
|
+
- lib/branchproof/value_instrumentation.rb
|
|
150
|
+
- lib/branchproof/value_runtime.rb
|
|
151
|
+
- lib/branchproof/value_syntax.rb
|
|
126
152
|
- lib/branchproof/version.rb
|
|
127
153
|
- lib/branchproof/worker.rb
|
|
128
154
|
- lib/mcdc.rb
|