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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +45 -0
  3. data/README.md +279 -27
  4. data/doc/Branchproof/Analyzer.md +2 -2
  5. data/doc/Branchproof/CLI.md +4 -0
  6. data/doc/Branchproof/ComparisonReport.md +4 -0
  7. data/doc/Branchproof/Constraints/Solver.md +21 -0
  8. data/doc/Branchproof/Constraints.md +98 -0
  9. data/doc/Branchproof/CoverageIndex.md +6 -0
  10. data/doc/Branchproof/DecisionSyntax.md +22 -0
  11. data/doc/Branchproof/DecisionTable.md +153 -0
  12. data/doc/Branchproof/FlowInstrumentation.md +7 -0
  13. data/doc/Branchproof/FocusedReport.md +5 -2
  14. data/doc/Branchproof/Instrumenter.md +1 -0
  15. data/doc/Branchproof/Loader.md +0 -4
  16. data/doc/Branchproof/MinitestAdapter.md +0 -3
  17. data/doc/Branchproof/Report.md +18 -0
  18. data/doc/Branchproof/Runtime.md +23 -0
  19. data/doc/Branchproof/RuntimeFlow.md +27 -0
  20. data/doc/Branchproof/SavedReport.md +21 -0
  21. data/doc/Branchproof/Source.md +1 -0
  22. data/doc/Branchproof.md +9 -2
  23. data/doc/CHANGELOG.md +45 -0
  24. data/doc/README.md +279 -27
  25. data/lib/branchproof/analyzer.rb +222 -54
  26. data/lib/branchproof/cli.rb +28 -19
  27. data/lib/branchproof/comparison.rb +207 -19
  28. data/lib/branchproof/comparison_report.rb +49 -1
  29. data/lib/branchproof/constraints.rb +363 -0
  30. data/lib/branchproof/coverage_index.rb +120 -3
  31. data/lib/branchproof/decision_syntax.rb +310 -0
  32. data/lib/branchproof/decision_table.rb +377 -0
  33. data/lib/branchproof/evidence.rb +101 -36
  34. data/lib/branchproof/flow_instrumentation.rb +107 -0
  35. data/lib/branchproof/focused_report.rb +161 -26
  36. data/lib/branchproof/instrumenter.rb +65 -39
  37. data/lib/branchproof/limits.rb +4 -1
  38. data/lib/branchproof/loader.rb +18 -6
  39. data/lib/branchproof/minimizer.rb +18 -13
  40. data/lib/branchproof/minitest_adapter.rb +11 -16
  41. data/lib/branchproof/records.rb +2 -0
  42. data/lib/branchproof/report.rb +470 -66
  43. data/lib/branchproof/runtime.rb +25 -30
  44. data/lib/branchproof/runtime_flow.rb +58 -0
  45. data/lib/branchproof/saved_report.rb +438 -13
  46. data/lib/branchproof/source.rb +239 -41
  47. data/lib/branchproof/version.rb +1 -1
  48. data/lib/branchproof/worker.rb +1 -4
  49. data/lib/branchproof.rb +2 -0
  50. data/llms.txt +9 -2
  51. data/sig/branchproof.rbs +54 -1
  52. metadata +12 -1
@@ -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
- source_id = Records.source_id(relative_path: relative(path), digest: Digest::SHA256.hexdigest(bytes),
62
- encoding: encoding)
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: relative(path), absolute_path: File.expand_path(path),
79
- real_path: File.realpath(path), digest: Digest::SHA256.hexdigest(bytes), encoding: encoding,
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: relative(path), absolute_path: File.expand_path(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
- nodes = []
102
- walk(program) { |node| nodes << node if decision_node?(node) }
103
- nodes.sort_by { |node| node.location.start_offset }.map.with_index do |node, _|
104
- build_decision(node, bytes, source_id, file_reasons, encoding)
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 build_decision(node, bytes, source_id, file_reasons = [], encoding = "UTF-8")
118
- predicate = unwrap_predicate(node.predicate)
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 << "unsupported_control_expression" if ambiguous_parentheses?(node.predicate)
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, context: context, byte_start: start_offset,
156
- byte_length: length, line: predicate.location.start_line, column: predicate.location.start_column,
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
- location = node.location
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Branchproof
4
- VERSION = "0.6.0"
4
+ VERSION = "0.8.0"
5
5
  end
@@ -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", legacy_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
- Public namespace for source inventory and one-run MC/DC reporting.
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.6.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