branchproof 0.6.0 → 0.8.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 +45 -0
- data/README.md +279 -27
- data/doc/Branchproof/Analyzer.md +2 -2
- data/doc/Branchproof/CLI.md +4 -0
- data/doc/Branchproof/ComparisonReport.md +4 -0
- data/doc/Branchproof/Constraints/Solver.md +21 -0
- data/doc/Branchproof/Constraints.md +98 -0
- data/doc/Branchproof/CoverageIndex.md +6 -0
- data/doc/Branchproof/DecisionSyntax.md +22 -0
- data/doc/Branchproof/DecisionTable.md +153 -0
- data/doc/Branchproof/FlowInstrumentation.md +7 -0
- data/doc/Branchproof/FocusedReport.md +5 -2
- data/doc/Branchproof/Instrumenter.md +1 -0
- data/doc/Branchproof/Loader.md +0 -4
- data/doc/Branchproof/MinitestAdapter.md +0 -3
- data/doc/Branchproof/Report.md +18 -0
- data/doc/Branchproof/Runtime.md +23 -0
- data/doc/Branchproof/RuntimeFlow.md +27 -0
- data/doc/Branchproof/SavedReport.md +21 -0
- data/doc/Branchproof/Source.md +1 -0
- data/doc/Branchproof.md +9 -2
- data/doc/CHANGELOG.md +45 -0
- data/doc/README.md +279 -27
- data/lib/branchproof/analyzer.rb +222 -54
- data/lib/branchproof/cli.rb +28 -19
- data/lib/branchproof/comparison.rb +207 -19
- data/lib/branchproof/comparison_report.rb +49 -1
- data/lib/branchproof/constraints.rb +363 -0
- data/lib/branchproof/coverage_index.rb +120 -3
- data/lib/branchproof/decision_syntax.rb +310 -0
- data/lib/branchproof/decision_table.rb +377 -0
- data/lib/branchproof/evidence.rb +101 -36
- data/lib/branchproof/flow_instrumentation.rb +107 -0
- data/lib/branchproof/focused_report.rb +161 -26
- data/lib/branchproof/instrumenter.rb +65 -39
- data/lib/branchproof/limits.rb +4 -1
- data/lib/branchproof/loader.rb +18 -6
- data/lib/branchproof/minimizer.rb +18 -13
- data/lib/branchproof/minitest_adapter.rb +11 -16
- data/lib/branchproof/records.rb +2 -0
- data/lib/branchproof/report.rb +470 -66
- data/lib/branchproof/runtime.rb +25 -30
- data/lib/branchproof/runtime_flow.rb +58 -0
- data/lib/branchproof/saved_report.rb +438 -13
- data/lib/branchproof/source.rb +239 -41
- data/lib/branchproof/version.rb +1 -1
- data/lib/branchproof/worker.rb +1 -4
- data/lib/branchproof.rb +2 -0
- data/llms.txt +9 -2
- data/sig/branchproof.rbs +54 -1
- metadata +12 -1
data/lib/branchproof/source.rb
CHANGED
|
@@ -3,10 +3,14 @@
|
|
|
3
3
|
require "digest"
|
|
4
4
|
require "pathname"
|
|
5
5
|
require "prism"
|
|
6
|
+
require_relative "decision_syntax"
|
|
7
|
+
require_relative "constraints"
|
|
6
8
|
|
|
7
9
|
module Branchproof
|
|
8
10
|
# Inventories supported condition and decision occurrences from Ruby files.
|
|
9
11
|
class Source
|
|
12
|
+
include DecisionSyntax
|
|
13
|
+
|
|
10
14
|
attr_reader :root, :limits
|
|
11
15
|
|
|
12
16
|
def initialize(root:, limits:)
|
|
@@ -55,11 +59,13 @@ module Branchproof
|
|
|
55
59
|
end
|
|
56
60
|
|
|
57
61
|
def read_unit(path)
|
|
62
|
+
absolute_path = File.expand_path(path)
|
|
63
|
+
relative_path = relative(path)
|
|
58
64
|
bytes = File.binread(path)
|
|
59
65
|
parsed = Prism.parse(bytes)
|
|
60
66
|
encoding = source_encoding(bytes, parsed)
|
|
61
|
-
|
|
62
|
-
|
|
67
|
+
digest = Digest::SHA256.hexdigest(bytes)
|
|
68
|
+
source_id = Records.source_id(relative_path: relative_path, digest: digest, encoding: encoding)
|
|
63
69
|
diagnostics = parsed.errors.map do |error|
|
|
64
70
|
Records.diagnostic(code: "parse_error", severity: "error", message: error.message, source_id: source_id,
|
|
65
71
|
details: { byte_start: error.location.start_offset, byte_length: error.location.length })
|
|
@@ -75,11 +81,11 @@ module Branchproof
|
|
|
75
81
|
file_reasons << "unsupported_data_section" if parsed.respond_to?(:data_loc) && parsed.data_loc
|
|
76
82
|
file_reasons << "parse_error" unless parsed.errors.empty?
|
|
77
83
|
decisions = parsed.value ? decisions_for(parsed.value, bytes, source_id, file_reasons, encoding) : []
|
|
78
|
-
Records.build(source_id: source_id, relative_path:
|
|
79
|
-
real_path: File.realpath(path), digest:
|
|
84
|
+
Records.build(source_id: source_id, relative_path: relative_path, absolute_path: absolute_path,
|
|
85
|
+
real_path: File.realpath(path), digest: digest, encoding: encoding,
|
|
80
86
|
original_bytes: bytes, decisions: decisions, diagnostics: diagnostics)
|
|
81
87
|
rescue SystemCallError => e
|
|
82
|
-
Records.build(source_id: nil, relative_path:
|
|
88
|
+
Records.build(source_id: nil, relative_path: relative_path, absolute_path: absolute_path,
|
|
83
89
|
real_path: nil, digest: nil, encoding: nil, original_bytes: nil, decisions: [],
|
|
84
90
|
diagnostics: [Records.diagnostic(code: "source_unreadable", severity: "error", message: e.message)])
|
|
85
91
|
end
|
|
@@ -97,12 +103,118 @@ module Branchproof
|
|
|
97
103
|
Encoding::UTF_8.name
|
|
98
104
|
end
|
|
99
105
|
|
|
106
|
+
# Walks the whole program AST exactly once, sorting every node into the
|
|
107
|
+
# buckets the two classification phases below need. Phase one (decision
|
|
108
|
+
# and case-when nodes) must run to completion before phase two (bare
|
|
109
|
+
# boolean/pattern nodes) because phase two skips nodes phase one already
|
|
110
|
+
# inventoried via mark_semantic_boolean_nodes.
|
|
111
|
+
def collect_ast_nodes(program)
|
|
112
|
+
defined_ranges = []
|
|
113
|
+
guard_patterns = {}.compare_by_identity
|
|
114
|
+
phase_one_nodes = []
|
|
115
|
+
phase_two_nodes = []
|
|
116
|
+
flow_nodes = []
|
|
117
|
+
|
|
118
|
+
walk(program) do |node|
|
|
119
|
+
if node.is_a?(Prism::DefinedNode)
|
|
120
|
+
defined_ranges << node.location
|
|
121
|
+
elsif node.is_a?(Prism::InNode)
|
|
122
|
+
pattern = node.pattern
|
|
123
|
+
guard_patterns[pattern] = true if pattern.is_a?(Prism::IfNode) || pattern.is_a?(Prism::UnlessNode)
|
|
124
|
+
end
|
|
125
|
+
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)
|
|
127
|
+
flow_nodes << node if flow_decision_node?(node)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
{ 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 }
|
|
132
|
+
end
|
|
133
|
+
|
|
100
134
|
def decisions_for(program, bytes, source_id, file_reasons = [], encoding = "UTF-8")
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
135
|
+
specs = []
|
|
136
|
+
inventoried_boolean_nodes = {}.compare_by_identity
|
|
137
|
+
collected = collect_ast_nodes(program)
|
|
138
|
+
defined_ranges = collected[:defined_ranges]
|
|
139
|
+
guard_patterns = collected[:guard_patterns]
|
|
140
|
+
|
|
141
|
+
collected[:phase_one_nodes].each do |node|
|
|
142
|
+
if decision_node?(node)
|
|
143
|
+
predicate = node.predicate
|
|
144
|
+
next unless predicate
|
|
145
|
+
|
|
146
|
+
predicate = unwrap_predicate(predicate)
|
|
147
|
+
context = guard_patterns[node] ? "pattern_guard" : decision_context(node, bytes)
|
|
148
|
+
specs << { node: node, predicate: predicate, context: context }
|
|
149
|
+
mark_semantic_boolean_nodes(predicate, inventoried_boolean_nodes)
|
|
150
|
+
else
|
|
151
|
+
conditions_for(node).each do |when_node|
|
|
152
|
+
conditions_for(when_node).each do |predicate|
|
|
153
|
+
predicate = unwrap_predicate(predicate)
|
|
154
|
+
specs << { node: when_node, predicate: predicate, context: "case_when" }
|
|
155
|
+
mark_semantic_boolean_nodes(predicate, inventoried_boolean_nodes)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Boolean expressions nested in an atomic expression (for example, a call
|
|
162
|
+
# argument) are separate decisions when tree_for did not decompose them.
|
|
163
|
+
collected[:phase_two_nodes].each do |node|
|
|
164
|
+
next if inventoried_boolean_nodes[node]
|
|
165
|
+
|
|
166
|
+
additional_reasons = if within_defined_expression?(node, defined_ranges)
|
|
167
|
+
["unsupported_defined_expression"]
|
|
168
|
+
else
|
|
169
|
+
[]
|
|
170
|
+
end
|
|
171
|
+
context = node.is_a?(Prism::MatchPredicateNode) ? "pattern_in" : "short_circuit"
|
|
172
|
+
specs << { node: nil, predicate: node, context: context,
|
|
173
|
+
additional_reasons: additional_reasons }
|
|
174
|
+
mark_semantic_boolean_nodes(node, inventoried_boolean_nodes)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
ordered_specs = specs.each_with_index.sort_by do |(spec, index)|
|
|
178
|
+
[spec[:predicate].location.start_offset, index]
|
|
179
|
+
end.map(&:first)
|
|
180
|
+
seen_ranges = {}
|
|
181
|
+
boolean_decisions = ordered_specs.filter_map do |spec|
|
|
182
|
+
predicate = spec[:predicate]
|
|
183
|
+
range = [predicate.location.start_offset, predicate.location.length]
|
|
184
|
+
next if seen_ranges[range]
|
|
185
|
+
|
|
186
|
+
seen_ranges[range] = true
|
|
187
|
+
build_decision(spec[:node], bytes, source_id, file_reasons, encoding,
|
|
188
|
+
predicate: predicate, context: spec[:context],
|
|
189
|
+
additional_reasons: spec[:additional_reasons] || [])
|
|
105
190
|
end
|
|
191
|
+
boolean_decisions + flow_decisions_for(program, bytes, source_id, file_reasons, encoding,
|
|
192
|
+
defined_ranges: defined_ranges, nodes: collected[:flow_nodes])
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def flow_decisions_for(program, bytes, source_id, file_reasons = [], encoding = "UTF-8",
|
|
196
|
+
defined_ranges: nil, nodes: nil)
|
|
197
|
+
defined_ranges ||= collect_defined_ranges(program)
|
|
198
|
+
|
|
199
|
+
super(program, bytes, source_id, file_reasons, encoding, nodes: nodes).map do |decision|
|
|
200
|
+
next decision unless range_within_defined_expression?(decision, defined_ranges)
|
|
201
|
+
|
|
202
|
+
decision.merge(
|
|
203
|
+
support_status: "UNSUPPORTED",
|
|
204
|
+
support_reasons: (Array(decision[:support_reasons]) + ["unsupported_defined_expression"]).uniq
|
|
205
|
+
)
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def collect_defined_ranges(program)
|
|
210
|
+
defined_ranges = []
|
|
211
|
+
walk(program) { |node| defined_ranges << node.location if node.is_a?(Prism::DefinedNode) }
|
|
212
|
+
defined_ranges
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def range_within_defined_expression?(decision, defined_ranges)
|
|
216
|
+
start_offset = decision[:byte_start]
|
|
217
|
+
offsets_within_defined_expression?(start_offset, start_offset + decision[:byte_length], defined_ranges)
|
|
106
218
|
end
|
|
107
219
|
|
|
108
220
|
def walk(node, &block)
|
|
@@ -111,39 +223,67 @@ module Branchproof
|
|
|
111
223
|
end
|
|
112
224
|
|
|
113
225
|
def decision_node?(node)
|
|
114
|
-
node.is_a?(Prism::IfNode) || node.is_a?(Prism::UnlessNode)
|
|
226
|
+
node.is_a?(Prism::IfNode) || node.is_a?(Prism::UnlessNode) ||
|
|
227
|
+
node.is_a?(Prism::WhileNode) || node.is_a?(Prism::UntilNode)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def boolean_node?(node)
|
|
231
|
+
node.is_a?(Prism::AndNode) || node.is_a?(Prism::OrNode)
|
|
115
232
|
end
|
|
116
233
|
|
|
117
|
-
def
|
|
118
|
-
|
|
234
|
+
def subjectless_case?(node)
|
|
235
|
+
node.is_a?(Prism::CaseNode) && node.predicate.nil?
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def conditions_for(node)
|
|
239
|
+
conditions = node.conditions
|
|
240
|
+
if conditions.is_a?(Array)
|
|
241
|
+
conditions
|
|
242
|
+
else
|
|
243
|
+
(conditions.respond_to?(:body) ? conditions.body : [])
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def decision_context(node, bytes)
|
|
248
|
+
return "unless" if node.is_a?(Prism::UnlessNode)
|
|
249
|
+
return "while" if node.is_a?(Prism::WhileNode)
|
|
250
|
+
return "until" if node.is_a?(Prism::UntilNode)
|
|
251
|
+
return "ternary" if node.if_keyword_loc.nil?
|
|
252
|
+
|
|
253
|
+
token = bytes.byteslice(node.if_keyword_loc.start_offset, node.if_keyword_loc.length)
|
|
254
|
+
token == "elsif" ? "elsif" : "if"
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def build_decision(node, bytes, source_id, file_reasons = [], encoding = "UTF-8", predicate: nil,
|
|
258
|
+
context: nil, additional_reasons: [])
|
|
259
|
+
original_predicate = node.respond_to?(:predicate) ? node.predicate : nil
|
|
260
|
+
predicate ||= unwrap_predicate(original_predicate)
|
|
261
|
+
context ||= decision_context(node, bytes)
|
|
119
262
|
leaves = []
|
|
120
263
|
tree = tree_for(predicate, bytes, leaves)
|
|
264
|
+
decision_constraint_safe = constraint_safe_expression?(predicate)
|
|
121
265
|
start_offset = predicate.location.start_offset
|
|
122
266
|
length = predicate.location.length
|
|
123
|
-
context = if node.is_a?(Prism::UnlessNode)
|
|
124
|
-
"unless"
|
|
125
|
-
elsif node.if_keyword_loc.nil?
|
|
126
|
-
"ternary"
|
|
127
|
-
else
|
|
128
|
-
token = bytes.byteslice(node.if_keyword_loc.start_offset, node.if_keyword_loc.length)
|
|
129
|
-
token == "elsif" ? "elsif" : "if"
|
|
130
|
-
end
|
|
131
267
|
opaque_ranges = leaves.filter_map { |leaf| leaf.delete(:_opaque_range) }
|
|
132
268
|
conditions = leaves.each_with_index.map do |leaf, index|
|
|
133
269
|
expression = text_value(leaf.delete(:_expression), "UTF-8")
|
|
134
270
|
location = leaf.delete(:_location)
|
|
135
271
|
literal_truth = leaf.delete(:_literal_truth)
|
|
272
|
+
constraint = leaf.delete(:_constraint)
|
|
273
|
+
constraint_safe = leaf.delete(:_constraint_safe)
|
|
136
274
|
Records.build(id: nil, index: index, byte_start: location.start_offset, byte_length: location.length,
|
|
137
275
|
line: location.start_line, column: location.start_column,
|
|
138
|
-
expression: expression, literal_truth: literal_truth, coupling: "unknown"
|
|
276
|
+
expression: expression, literal_truth: literal_truth, coupling: "unknown",
|
|
277
|
+
constraint: constraint, constraint_safe: decision_constraint_safe && constraint_safe == true)
|
|
139
278
|
end
|
|
140
279
|
decision_id = Records.decision_id(source_id: source_id, context: context, byte_start: start_offset,
|
|
141
280
|
byte_length: length, tree: tree)
|
|
142
281
|
conditions = conditions.map do |condition|
|
|
143
282
|
condition.merge(id: Records.condition_id(decision_id, condition[:index]))
|
|
144
283
|
end
|
|
145
|
-
reasons = unsupported_reasons(predicate, bytes) + file_reasons
|
|
146
|
-
reasons << "
|
|
284
|
+
reasons = unsupported_reasons(predicate, bytes) + additional_reasons + file_reasons
|
|
285
|
+
reasons << "unsupported_case_splat" if predicate.is_a?(Prism::SplatNode)
|
|
286
|
+
reasons << "unsupported_control_expression" if ambiguous_parentheses?(original_predicate)
|
|
147
287
|
reasons << "condition_limit_exceeded" if conditions.length > @limits[:conditions_per_decision]
|
|
148
288
|
discovered_condition_count = conditions.length
|
|
149
289
|
if discovered_condition_count > @limits[:conditions_per_decision]
|
|
@@ -152,14 +292,40 @@ module Branchproof
|
|
|
152
292
|
end
|
|
153
293
|
support = reasons.empty? ? "SUPPORTED" : "UNSUPPORTED"
|
|
154
294
|
expression = text_value(bytes.byteslice(predicate.location.start_offset, predicate.location.length), encoding)
|
|
155
|
-
Records.build(id: decision_id, source_id: source_id,
|
|
156
|
-
|
|
295
|
+
Records.build(id: decision_id, source_id: source_id, kind: "boolean", context: context,
|
|
296
|
+
byte_start: start_offset, byte_length: length,
|
|
297
|
+
line: predicate.location.start_line, column: predicate.location.start_column,
|
|
157
298
|
expression: expression,
|
|
158
299
|
tree: tree,
|
|
159
300
|
conditions: conditions, discovered_condition_count: discovered_condition_count,
|
|
160
301
|
support_status: support, support_reasons: reasons.uniq, opaque_ranges: opaque_ranges)
|
|
161
302
|
end
|
|
162
303
|
|
|
304
|
+
def within_defined_expression?(node, defined_ranges)
|
|
305
|
+
location = node.location
|
|
306
|
+
offsets_within_defined_expression?(location.start_offset, location.end_offset, defined_ranges)
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def offsets_within_defined_expression?(start_offset, end_offset, defined_ranges)
|
|
310
|
+
defined_ranges.any? do |defined_location|
|
|
311
|
+
defined_location.start_offset <= start_offset && defined_location.end_offset >= end_offset
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def mark_semantic_boolean_nodes(node, inventoried_boolean_nodes)
|
|
316
|
+
node = unwrap_predicate(node)
|
|
317
|
+
case node
|
|
318
|
+
when Prism::AndNode, Prism::OrNode
|
|
319
|
+
inventoried_boolean_nodes[node] = true
|
|
320
|
+
mark_semantic_boolean_nodes(node.left, inventoried_boolean_nodes)
|
|
321
|
+
mark_semantic_boolean_nodes(node.right, inventoried_boolean_nodes)
|
|
322
|
+
when Prism::MatchPredicateNode
|
|
323
|
+
inventoried_boolean_nodes[node] = true
|
|
324
|
+
when Prism::CallNode
|
|
325
|
+
mark_semantic_boolean_nodes(node.receiver, inventoried_boolean_nodes) if unary_not?(node)
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
|
|
163
329
|
def tree_for(node, bytes, leaves)
|
|
164
330
|
node = unwrap_predicate(node)
|
|
165
331
|
case node
|
|
@@ -167,18 +333,57 @@ module Branchproof
|
|
|
167
333
|
Records.build(type: :and, left: tree_for(node.left, bytes, leaves), right: tree_for(node.right, bytes, leaves))
|
|
168
334
|
when Prism::OrNode
|
|
169
335
|
Records.build(type: :or, left: tree_for(node.left, bytes, leaves), right: tree_for(node.right, bytes, leaves))
|
|
336
|
+
when Prism::CallNode
|
|
337
|
+
if unary_not?(node)
|
|
338
|
+
Records.build(type: :not, child: tree_for(node.receiver, bytes, leaves))
|
|
339
|
+
else
|
|
340
|
+
leaf_for(node, bytes, leaves)
|
|
341
|
+
end
|
|
170
342
|
else
|
|
171
|
-
|
|
172
|
-
leaf = {
|
|
173
|
-
_expression: bytes.byteslice(location.start_offset, location.length), _location: location,
|
|
174
|
-
_literal_truth: literal_truth(node),
|
|
175
|
-
_opaque_range: opaque?(node) ? { start: location.start_offset, length: location.length } : nil
|
|
176
|
-
}
|
|
177
|
-
leaves << leaf
|
|
178
|
-
Records.build(type: :atom, index: leaves.length - 1)
|
|
343
|
+
leaf_for(node, bytes, leaves)
|
|
179
344
|
end
|
|
180
345
|
end
|
|
181
346
|
|
|
347
|
+
def leaf_for(node, bytes, leaves)
|
|
348
|
+
location = node.location
|
|
349
|
+
leaf = {
|
|
350
|
+
_expression: bytes.byteslice(location.start_offset, location.length), _location: location,
|
|
351
|
+
_literal_truth: literal_truth(node),
|
|
352
|
+
_constraint: (constraint = Constraints.for_node(node)),
|
|
353
|
+
_constraint_safe: safe_constraint_node?(constraint),
|
|
354
|
+
_opaque_range: if node.is_a?(Prism::CallNode) && node.name == :!
|
|
355
|
+
{ start: location.start_offset, length: location.length }
|
|
356
|
+
end
|
|
357
|
+
}
|
|
358
|
+
leaves << leaf
|
|
359
|
+
Records.build(type: :atom, index: leaves.length - 1)
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# Source-derived comparisons are deliberately untrusted. Ruby permits
|
|
363
|
+
# mutation between leaves and user-defined operators, so only repeated
|
|
364
|
+
# truthiness reads of local variables in an entirely side-effect-free
|
|
365
|
+
# decision may be used by the table solver.
|
|
366
|
+
def constraint_safe_expression?(node)
|
|
367
|
+
node = unwrap_predicate(node)
|
|
368
|
+
case node
|
|
369
|
+
when Prism::AndNode, Prism::OrNode
|
|
370
|
+
constraint_safe_expression?(node.left) && constraint_safe_expression?(node.right)
|
|
371
|
+
when Prism::LocalVariableReadNode, Prism::TrueNode, Prism::FalseNode, Prism::NilNode,
|
|
372
|
+
Prism::IntegerNode, Prism::FloatNode, Prism::SymbolNode, Prism::StringNode
|
|
373
|
+
true
|
|
374
|
+
else
|
|
375
|
+
false
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def safe_constraint_node?(constraint)
|
|
380
|
+
constraint && constraint[:operator] == "truthy" && constraint[:subject][:kind] == "local"
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def unary_not?(node)
|
|
384
|
+
node.is_a?(Prism::CallNode) && node.name == :! && node.receiver && node.call_operator_loc.nil?
|
|
385
|
+
end
|
|
386
|
+
|
|
182
387
|
def literal_truth(node)
|
|
183
388
|
return true if node.is_a?(Prism::TrueNode)
|
|
184
389
|
return false if node.is_a?(Prism::FalseNode) || node.is_a?(Prism::NilNode)
|
|
@@ -186,10 +391,6 @@ module Branchproof
|
|
|
186
391
|
nil
|
|
187
392
|
end
|
|
188
393
|
|
|
189
|
-
def opaque?(node)
|
|
190
|
-
node.is_a?(Prism::CallNode) && node.name == :!
|
|
191
|
-
end
|
|
192
|
-
|
|
193
394
|
def unsupported_reasons(predicate, bytes)
|
|
194
395
|
reasons = []
|
|
195
396
|
walk(predicate) do |node|
|
|
@@ -197,13 +398,10 @@ module Branchproof
|
|
|
197
398
|
reasons << "unsupported_implicit_regexp"
|
|
198
399
|
end
|
|
199
400
|
reasons << "unsupported_flip_flop" if node.is_a?(Prism::FlipFlopNode)
|
|
401
|
+
reasons << "unsupported_defined_expression" if node.is_a?(Prism::DefinedNode)
|
|
200
402
|
reasons << "unsupported_heredoc" if node.respond_to?(:opening_loc) && node.opening_loc &&
|
|
201
403
|
bytes.byteslice(node.opening_loc.start_offset,
|
|
202
404
|
node.opening_loc.length).start_with?("<<")
|
|
203
|
-
if (node.is_a?(Prism::AndNode) || node.is_a?(Prism::OrNode)) && node.respond_to?(:operator_loc)
|
|
204
|
-
operator = bytes.byteslice(node.operator_loc.start_offset, node.operator_loc.length)
|
|
205
|
-
reasons << "unsupported_keyword_boolean" if %w[and or].include?(operator)
|
|
206
|
-
end
|
|
207
405
|
end
|
|
208
406
|
reasons
|
|
209
407
|
end
|
data/lib/branchproof/version.rb
CHANGED
data/lib/branchproof/worker.rb
CHANGED
|
@@ -14,12 +14,9 @@ module Branchproof
|
|
|
14
14
|
|
|
15
15
|
def child_process(config_path)
|
|
16
16
|
payload = JSON.parse(File.binread(config_path))
|
|
17
|
-
project = symbolize(payload.fetch("project"
|
|
17
|
+
project = symbolize(payload.fetch("project"))
|
|
18
18
|
prepend_load_paths(project)
|
|
19
19
|
inventory = symbolize(payload.fetch("inventory"))
|
|
20
|
-
Array(inventory[:source_units]).each do |unit|
|
|
21
|
-
unit[:original_bytes] = File.binread(unit[:absolute_path]) if unit[:absolute_path]
|
|
22
|
-
end
|
|
23
20
|
limits = symbolize(payload.fetch("limits"))
|
|
24
21
|
require "minitest"
|
|
25
22
|
require "minitest/test"
|
data/lib/branchproof.rb
CHANGED
|
@@ -16,6 +16,8 @@ module Branchproof
|
|
|
16
16
|
autoload :MinitestAdapter, "branchproof/minitest_adapter"
|
|
17
17
|
autoload :Evidence, "branchproof/evidence"
|
|
18
18
|
autoload :Analyzer, "branchproof/analyzer"
|
|
19
|
+
autoload :Constraints, "branchproof/constraints"
|
|
20
|
+
autoload :DecisionTable, "branchproof/decision_table"
|
|
19
21
|
autoload :Minimizer, "branchproof/minimizer"
|
|
20
22
|
autoload :Report, "branchproof/report"
|
|
21
23
|
autoload :CoverageIndex, "branchproof/coverage_index"
|
data/llms.txt
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
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/instrumenter.rb, lib/branchproof/saved_report.rb, lib/branchproof/rails_support.rb, lib/branchproof/coverage_index.rb, lib/branchproof/focused_report.rb, lib/branchproof/minitest_adapter.rb, lib/branchproof/comparison_report.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/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 |
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Keep each bounded source rewrite together so its evaluation order can be
|
|
8
|
+
audited. rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
8
9
|
|
|
9
10
|
## Constants
|
|
10
11
|
### `VERSION` <a id="constant-VERSION"></a> <a id="VERSION-constant"></a>
|
|
@@ -16,9 +17,14 @@ Not documented.
|
|
|
16
17
|
- [Branchproof/CLI.md](doc/Branchproof/CLI.md)
|
|
17
18
|
- [Branchproof/Comparison.md](doc/Branchproof/Comparison.md)
|
|
18
19
|
- [Branchproof/ComparisonReport.md](doc/Branchproof/ComparisonReport.md)
|
|
20
|
+
- [Branchproof/Constraints/Solver.md](doc/Branchproof/Constraints/Solver.md)
|
|
21
|
+
- [Branchproof/Constraints.md](doc/Branchproof/Constraints.md)
|
|
19
22
|
- [Branchproof/CoverageIndex.md](doc/Branchproof/CoverageIndex.md)
|
|
23
|
+
- [Branchproof/DecisionSyntax.md](doc/Branchproof/DecisionSyntax.md)
|
|
24
|
+
- [Branchproof/DecisionTable.md](doc/Branchproof/DecisionTable.md)
|
|
20
25
|
- [Branchproof/Error.md](doc/Branchproof/Error.md)
|
|
21
26
|
- [Branchproof/Evidence.md](doc/Branchproof/Evidence.md)
|
|
27
|
+
- [Branchproof/FlowInstrumentation.md](doc/Branchproof/FlowInstrumentation.md)
|
|
22
28
|
- [Branchproof/FocusedReport.md](doc/Branchproof/FocusedReport.md)
|
|
23
29
|
- [Branchproof/Instrumenter.md](doc/Branchproof/Instrumenter.md)
|
|
24
30
|
- [Branchproof/Limits.md](doc/Branchproof/Limits.md)
|
|
@@ -31,6 +37,7 @@ Not documented.
|
|
|
31
37
|
- [Branchproof/Records.md](doc/Branchproof/Records.md)
|
|
32
38
|
- [Branchproof/Report.md](doc/Branchproof/Report.md)
|
|
33
39
|
- [Branchproof/Runtime.md](doc/Branchproof/Runtime.md)
|
|
40
|
+
- [Branchproof/RuntimeFlow.md](doc/Branchproof/RuntimeFlow.md)
|
|
34
41
|
- [Branchproof/SavedReport.md](doc/Branchproof/SavedReport.md)
|
|
35
42
|
- [Branchproof/Source.md](doc/Branchproof/Source.md)
|
|
36
43
|
- [Branchproof/Worker.md](doc/Branchproof/Worker.md)
|
data/sig/branchproof.rbs
CHANGED
|
@@ -62,6 +62,12 @@ module Branchproof
|
|
|
62
62
|
def self.condition: (String decision_id, Integer index, untyped value) -> untyped
|
|
63
63
|
def self.finish: (String decision_id, untyped value) -> untyped
|
|
64
64
|
def self.leave: (String decision_id) -> nil
|
|
65
|
+
def self.flow_candidate: (String decision_id, Integer index) -> nil
|
|
66
|
+
def self.flow_selected: (String decision_id) -> nil
|
|
67
|
+
def self.flow_select: (String decision_id, Integer index) -> nil
|
|
68
|
+
def self.flow_path: (String decision_id, Integer index) -> nil
|
|
69
|
+
def self.flow_finish: (String decision_id, untyped value, ?Integer? default_path) -> untyped
|
|
70
|
+
def self.flow_receiver: (String decision_id, untyped receiver) -> untyped
|
|
65
71
|
def self.context: (?test_id: String?, ?phase: String?) -> nil
|
|
66
72
|
def self.snapshot: () -> record
|
|
67
73
|
def self.diagnostics: () -> Array[record]
|
|
@@ -83,12 +89,53 @@ module Branchproof
|
|
|
83
89
|
|
|
84
90
|
class Analyzer
|
|
85
91
|
CRITERION: String
|
|
86
|
-
def initialize: (inventory: record, evidence: record, limits: limits) -> void
|
|
92
|
+
def initialize: (inventory: record, evidence: record, limits: limits, ?reachability: bool) -> void
|
|
87
93
|
def call: () -> record
|
|
88
94
|
def pair?: (decision_id: String, condition_index: Integer, left: record, right: record) -> bool
|
|
89
95
|
def missing: (decision_id: String, condition_index: Integer) -> record?
|
|
90
96
|
end
|
|
91
97
|
|
|
98
|
+
module Constraints
|
|
99
|
+
VERSION: Integer
|
|
100
|
+
NUMERIC_OPERATORS: Array[String]
|
|
101
|
+
COMPARISON_OPERATORS: Array[String]
|
|
102
|
+
OPERATORS: Array[String]
|
|
103
|
+
LITERAL_TYPES: Array[String]
|
|
104
|
+
SUBJECT_KINDS: Array[String]
|
|
105
|
+
REASONS: Array[String]
|
|
106
|
+
|
|
107
|
+
class Solver
|
|
108
|
+
def initialize: () -> void
|
|
109
|
+
def add: (record? constraint, bool truth) -> String?
|
|
110
|
+
def add_prepared: (record constraint, bool truth) -> String?
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def self.for_node: (untyped node) -> record?
|
|
114
|
+
def self.usable?: (untyped constraint) -> bool
|
|
115
|
+
def self.message: (untyped reason) -> String
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
module DecisionTable
|
|
119
|
+
SCHEMA_VERSION: Integer
|
|
120
|
+
CONSTRAINT_ANALYSIS_VERSION: Integer
|
|
121
|
+
TRUE_VALUE: String
|
|
122
|
+
FALSE_VALUE: String
|
|
123
|
+
DONT_CARE: String
|
|
124
|
+
CONDITION_VALUES: Array[String]
|
|
125
|
+
VALUE_LABELS: Hash[String, String]
|
|
126
|
+
REACHABILITY_STATUSES: Array[String]
|
|
127
|
+
COVERAGE_STATUSES: Array[String]
|
|
128
|
+
TABLE_STATUSES: Array[String]
|
|
129
|
+
NOT_CALCULATED_REASONS: Array[String]
|
|
130
|
+
DEFAULT_MAX_CONDITIONS: Integer
|
|
131
|
+
DEFAULT_MAX_RULES: Integer
|
|
132
|
+
|
|
133
|
+
def self.build: (decision: record, ?vectors: Array[record], ?limits: limits, ?reachability: bool) -> record
|
|
134
|
+
def self.rule_id: (String decision_id, Array[String] values, bool outcome) -> String
|
|
135
|
+
def self.coverage_entry: (record table) -> record
|
|
136
|
+
def self.unsupported_coverage_entry: () -> record
|
|
137
|
+
end
|
|
138
|
+
|
|
92
139
|
class Minimizer
|
|
93
140
|
def initialize: (analysis: record, evidence: record, limits: limits) -> void
|
|
94
141
|
def call: (objective: Symbol, decision_ids: Array[String]) -> record
|
|
@@ -103,10 +150,14 @@ module Branchproof
|
|
|
103
150
|
class Report
|
|
104
151
|
SCHEMA_VERSION: String
|
|
105
152
|
CRITERION_VERSION: String
|
|
153
|
+
VIEWS: Array[Symbol]
|
|
106
154
|
def initialize: (inventory: document, evidence: document, analysis: document?, minima: Array[document], baseline: document, diagnostics: Array[document], ?level: Integer, ?missing_only: bool, ?view: Symbol, ?run_metadata: document, ?saved_document: document?) -> void
|
|
107
155
|
def self.from_document: (document: document, ?level: Integer?, ?view: Symbol, ?missing_only: bool) -> Report
|
|
108
156
|
def diagnostic_message: (Hash[Symbol | String, untyped] diagnostic) -> String
|
|
109
157
|
def condition_explanation: (decision_id: String, condition_id: String) -> String
|
|
158
|
+
def decision_table_requirement: (untyped value) -> String?
|
|
159
|
+
def decision_table_reachability: (document rule) -> String
|
|
160
|
+
def decision_table_expected_heading: (document decision) -> String
|
|
110
161
|
def write: (io: _WriteIO, format: Symbol | String) -> nil
|
|
111
162
|
def exit_code: () -> Integer
|
|
112
163
|
end
|
|
@@ -114,7 +165,9 @@ module Branchproof
|
|
|
114
165
|
class CoverageIndex
|
|
115
166
|
def initialize: (document: document) -> void
|
|
116
167
|
attr_reader conditions: Array[record]
|
|
168
|
+
attr_reader alternatives: Array[record]
|
|
117
169
|
attr_reader tests: Array[record]
|
|
170
|
+
attr_reader decision_tables: Array[record]
|
|
118
171
|
end
|
|
119
172
|
|
|
120
173
|
class FocusedReport
|
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.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Lucian Ghinda
|
|
@@ -70,9 +70,14 @@ files:
|
|
|
70
70
|
- doc/Branchproof/CLI.md
|
|
71
71
|
- doc/Branchproof/Comparison.md
|
|
72
72
|
- doc/Branchproof/ComparisonReport.md
|
|
73
|
+
- doc/Branchproof/Constraints.md
|
|
74
|
+
- doc/Branchproof/Constraints/Solver.md
|
|
73
75
|
- doc/Branchproof/CoverageIndex.md
|
|
76
|
+
- doc/Branchproof/DecisionSyntax.md
|
|
77
|
+
- doc/Branchproof/DecisionTable.md
|
|
74
78
|
- doc/Branchproof/Error.md
|
|
75
79
|
- doc/Branchproof/Evidence.md
|
|
80
|
+
- doc/Branchproof/FlowInstrumentation.md
|
|
76
81
|
- doc/Branchproof/FocusedReport.md
|
|
77
82
|
- doc/Branchproof/Instrumenter.md
|
|
78
83
|
- doc/Branchproof/Limits.md
|
|
@@ -85,6 +90,7 @@ files:
|
|
|
85
90
|
- doc/Branchproof/Records.md
|
|
86
91
|
- doc/Branchproof/Report.md
|
|
87
92
|
- doc/Branchproof/Runtime.md
|
|
93
|
+
- doc/Branchproof/RuntimeFlow.md
|
|
88
94
|
- doc/Branchproof/SavedReport.md
|
|
89
95
|
- doc/Branchproof/Source.md
|
|
90
96
|
- doc/Branchproof/Worker.md
|
|
@@ -97,8 +103,12 @@ files:
|
|
|
97
103
|
- lib/branchproof/cli.rb
|
|
98
104
|
- lib/branchproof/comparison.rb
|
|
99
105
|
- lib/branchproof/comparison_report.rb
|
|
106
|
+
- lib/branchproof/constraints.rb
|
|
100
107
|
- lib/branchproof/coverage_index.rb
|
|
108
|
+
- lib/branchproof/decision_syntax.rb
|
|
109
|
+
- lib/branchproof/decision_table.rb
|
|
101
110
|
- lib/branchproof/evidence.rb
|
|
111
|
+
- lib/branchproof/flow_instrumentation.rb
|
|
102
112
|
- lib/branchproof/focused_report.rb
|
|
103
113
|
- lib/branchproof/instrumenter.rb
|
|
104
114
|
- lib/branchproof/limits.rb
|
|
@@ -110,6 +120,7 @@ files:
|
|
|
110
120
|
- lib/branchproof/records.rb
|
|
111
121
|
- lib/branchproof/report.rb
|
|
112
122
|
- lib/branchproof/runtime.rb
|
|
123
|
+
- lib/branchproof/runtime_flow.rb
|
|
113
124
|
- lib/branchproof/saved_report.rb
|
|
114
125
|
- lib/branchproof/source.rb
|
|
115
126
|
- lib/branchproof/version.rb
|