rigortype 0.3.7 → 0.3.8
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/README.md +1 -1
- data/docs/handbook/11-sig-gen.md +24 -14
- data/docs/manual/02-cli-reference.md +7 -0
- data/docs/manual/04-diagnostics.md +1 -1
- data/lib/rigor/analysis/crash_signature.rb +78 -6
- data/lib/rigor/analysis/incremental_session.rb +19 -7
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +142 -17
- data/lib/rigor/analysis/runner/pool_coordinator.rb +232 -47
- data/lib/rigor/analysis/runner/run_snapshots.rb +7 -2
- data/lib/rigor/analysis/runner.rb +66 -7
- data/lib/rigor/analysis/worker_session.rb +28 -3
- data/lib/rigor/builtins/imported_refinements.rb +11 -9
- data/lib/rigor/cache/annotation_location.rb +72 -0
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +39 -0
- data/lib/rigor/cache/store.rb +8 -1
- data/lib/rigor/cli/sig_gen_command.rb +29 -0
- data/lib/rigor/environment/failure_slot.rb +28 -0
- data/lib/rigor/environment/rbs_loader.rb +228 -57
- data/lib/rigor/environment.rb +87 -10
- data/lib/rigor/inference/hkt_registry.rb +16 -4
- data/lib/rigor/inference/hkt_sugar_translator.rb +15 -21
- data/lib/rigor/plugin/registry.rb +25 -2
- data/lib/rigor/rbs_extended/envelope_scanner.rb +8 -7
- data/lib/rigor/rbs_extended/hkt_directives.rb +16 -1
- data/lib/rigor/rbs_extended/reporter.rb +93 -13
- data/lib/rigor/rbs_extended.rb +6 -1
- data/lib/rigor/sig_gen/generator.rb +66 -80
- data/lib/rigor/sig_gen/renderer.rb +13 -6
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/environment.rbs +6 -0
- metadata +3 -1
|
@@ -16,6 +16,7 @@ require_relative "../effects/envelope_index"
|
|
|
16
16
|
require_relative "../inference/scope_indexer"
|
|
17
17
|
require_relative "../inference/method_dispatcher/file_folding"
|
|
18
18
|
require_relative "check_rules"
|
|
19
|
+
require_relative "crash_signature"
|
|
19
20
|
require_relative "dependency_recorder"
|
|
20
21
|
require_relative "dependency_source_inference"
|
|
21
22
|
require_relative "diagnostic"
|
|
@@ -228,7 +229,7 @@ module Rigor
|
|
|
228
229
|
rescue Errno::ENOENT => e
|
|
229
230
|
[analyzer_error(path, e.message)]
|
|
230
231
|
rescue StandardError => e
|
|
231
|
-
[analyzer_error(path,
|
|
232
|
+
[analyzer_error(path, CrashSignature.check_rule_message(e))]
|
|
232
233
|
end
|
|
233
234
|
private :analyze_body
|
|
234
235
|
|
|
@@ -243,15 +244,39 @@ module Rigor
|
|
|
243
244
|
# how you ran it" defect the diagnostic exists to end. Draining it out of the workers is what makes
|
|
244
245
|
# the two paths say the same thing. The payload is `[String, String, String, Array<String>]` tuples —
|
|
245
246
|
# Marshal-clean for the fork backend and shareable for the Ractor one.
|
|
247
|
+
#
|
|
248
|
+
# Issue #784 — `hkt_scan_failure` rides the same channel for the same reason: the PARENT's own
|
|
249
|
+
# Environment never demands `#hkt_registry` under the pool, so its slot is always nil, and draining it
|
|
250
|
+
# out of the workers is the only way `--workers=N` says what `--workers=0` says.
|
|
251
|
+
#
|
|
252
|
+
# Issue #805 — the Marshal-clean requirement binds every stream here, not just the ones written for it.
|
|
253
|
+
# The `RbsExtended::Reporter`'s unresolved / lossy-projection entries used to carry the `RBS::Location`
|
|
254
|
+
# itself, and `Marshal.dump` on one raises `TypeError` (a C-extension object with no `_dump`), so a
|
|
255
|
+
# project whose `sig/` produced a single such event killed each worker HERE — after its files were
|
|
256
|
+
# analysed — and the run degraded to in-process re-analysis with a `pool-degraded` row. All three
|
|
257
|
+
# streams now carry `(path, line, column)` primitives.
|
|
246
258
|
def drain_reporters
|
|
259
|
+
# Issue #784 — demand the registry once per worker before reading the slot, so a worker whose share
|
|
260
|
+
# of files happened to contain no `Klass.method` call still reports the run's outcome (the same
|
|
261
|
+
# reason `PoolCoordinator#hkt_scan_outcome` demands it on the sequential path). The Environment
|
|
262
|
+
# object is built in the constructor, but its RBS env is lazy: on a worker that analysed a file this
|
|
263
|
+
# is a memoised read; on one that demanded nothing it is the same cache-served env load that file
|
|
264
|
+
# would have paid. The fork backend never hands a worker an empty slice, so no idle worker pays it.
|
|
265
|
+
@environment&.hkt_registry
|
|
247
266
|
{
|
|
248
267
|
rbs_extended: {
|
|
249
268
|
unresolved_payloads: @rbs_extended_reporter.unresolved_payloads,
|
|
250
|
-
lossy_projections: @rbs_extended_reporter.lossy_projections
|
|
269
|
+
lossy_projections: @rbs_extended_reporter.lossy_projections,
|
|
270
|
+
# Issue #785 — the HKT directive stream rides the same channel for the same reason
|
|
271
|
+
# `hkt_scan_failure` does: the directives are read by the registry scan, and under the pool the
|
|
272
|
+
# PARENT never demands that scan, so a diagnostic wired off the parent's reporter would appear
|
|
273
|
+
# at `--workers=0` and vanish at `--workers=N`. The demand above is what fills this.
|
|
274
|
+
hkt_directive_errors: @rbs_extended_reporter.hkt_directive_errors
|
|
251
275
|
},
|
|
252
276
|
boundary_cross: @boundary_cross_reporter.entries,
|
|
253
277
|
source_rbs_synthesis: @source_rbs_synthesis_reporter.entries,
|
|
254
|
-
definition_build_failures: @environment&.rbs_loader&.definition_build_failures || []
|
|
278
|
+
definition_build_failures: @environment&.rbs_loader&.definition_build_failures || [],
|
|
279
|
+
hkt_scan_failure: @environment&.hkt_scan_failure
|
|
255
280
|
}
|
|
256
281
|
end
|
|
257
282
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "strscan"
|
|
4
4
|
|
|
5
|
+
require_relative "../rbs_extended/reporter"
|
|
5
6
|
require_relative "../type"
|
|
6
7
|
require_relative "../type_node"
|
|
7
8
|
|
|
@@ -558,11 +559,14 @@ module Rigor
|
|
|
558
559
|
result
|
|
559
560
|
end
|
|
560
561
|
|
|
561
|
-
# ADR-13 slice 3b — record one `dynamic.shape.lossy-projection` event per (head,
|
|
562
|
-
#
|
|
563
|
-
#
|
|
564
|
-
# `
|
|
565
|
-
#
|
|
562
|
+
# ADR-13 slice 3b — record one `dynamic.shape.lossy-projection` event per (head, position) pair when
|
|
563
|
+
# the projection actually degraded. The builders return the source carrier unchanged on
|
|
564
|
+
# non-HashShape / non-Tuple receivers (see `Type::Combinator.pick_of` / `omit_of` and the
|
|
565
|
+
# HashShape-only `partial_of` / `required_of` / `readonly_of`), so detection is "first arg was lossy".
|
|
566
|
+
#
|
|
567
|
+
# Issue #805 — the `RBS::Location` is flattened to `(path, line, column)` here, at record time: the
|
|
568
|
+
# entry is drained out of every fork-pool worker, and a location survives neither the drain's
|
|
569
|
+
# `Marshal.dump` nor its cross-worker dedup (see {Rigor::RbsExtended::Reporter}).
|
|
566
570
|
def record_lossy_projection_if_applicable(node, args, result)
|
|
567
571
|
return if @reporter.nil?
|
|
568
572
|
return if result.nil?
|
|
@@ -570,10 +574,8 @@ module Rigor
|
|
|
570
574
|
return if args.empty?
|
|
571
575
|
return unless Type::Combinator.shape_projection_lossy?(args.first)
|
|
572
576
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
source_location: @source_location
|
|
576
|
-
)
|
|
577
|
+
path, line, column = RbsExtended::Reporter.position_of(@source_location)
|
|
578
|
+
@reporter.record_lossy_projection(head: node.head, path: path, line: line, column: column)
|
|
577
579
|
end
|
|
578
580
|
|
|
579
581
|
def try_parametric_int_builder(node)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rbs"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Cache
|
|
7
|
+
# The `RBS::AST::Annotation` location carry (issue #799): the two halves of the `marshal_dump` /
|
|
8
|
+
# `marshal_load` pair {file:lib/rigor/cache/rbs_environment_marshal_patch.rb the env-cache Marshal patch}
|
|
9
|
+
# installs on `RBS::AST::Annotation`, plus the buffer a reconstructed location points at. That file's
|
|
10
|
+
# header carries the why; this one is the how.
|
|
11
|
+
#
|
|
12
|
+
# Nothing here reconstructs the annotation's SOURCE — the cached env has no file contents, and a
|
|
13
|
+
# consumer that wants the text already has `Annotation#string`. What it reconstructs is the pair of
|
|
14
|
+
# `(line, column)` answers `RBS::Location` gives, so a diagnostic positioned on the directive reads the
|
|
15
|
+
# same line warm as it does cold.
|
|
16
|
+
module AnnotationLocation
|
|
17
|
+
# The two positions {Buffer} distinguishes. `RBS::Location` addresses its buffer by character
|
|
18
|
+
# offset, and the reconstruction has no content to offset into, so the offsets are used as tags:
|
|
19
|
+
# 0 is "the start pair", 1 is "the end pair".
|
|
20
|
+
START_POS = 0
|
|
21
|
+
END_POS = 1
|
|
22
|
+
|
|
23
|
+
# A content-less `RBS::Buffer` that answers `pos_to_loc` from the pairs the cold parse had.
|
|
24
|
+
#
|
|
25
|
+
# Answering there is all it takes: `RBS::Location#start_line` and its three siblings are C methods
|
|
26
|
+
# that resolve the offset through `buffer.pos_to_loc`, so the reconstructed Location reports the real
|
|
27
|
+
# position without the buffer having to hold (or synthesize) the file it came from. The alternative —
|
|
28
|
+
# a synthetic content string of `line - 1` newlines — is exact only while an annotation stays on one
|
|
29
|
+
# line, and `%a{...}` may span two.
|
|
30
|
+
class Buffer < ::RBS::Buffer
|
|
31
|
+
def initialize(name:, start_loc:, end_loc:)
|
|
32
|
+
super(name: name, content: "")
|
|
33
|
+
@start_loc = start_loc
|
|
34
|
+
@end_loc = end_loc
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def pos_to_loc(pos)
|
|
38
|
+
pos == START_POS ? @start_loc : @end_loc
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
module_function
|
|
43
|
+
|
|
44
|
+
# @param location [RBS::Location, nil]
|
|
45
|
+
# @return [Array, nil] `[name, start_line, start_column, end_line, end_column]`, or nil when there is
|
|
46
|
+
# no location to carry. Fail-soft: a location whose buffer cannot answer is dumped as nil rather
|
|
47
|
+
# than failing the whole environment's dump.
|
|
48
|
+
def dump(location)
|
|
49
|
+
return nil if location.nil?
|
|
50
|
+
|
|
51
|
+
buffer = location.buffer
|
|
52
|
+
name = buffer.respond_to?(:name) ? buffer.name.to_s : nil
|
|
53
|
+
[name, location.start_line, location.start_column, location.end_line, location.end_column]
|
|
54
|
+
rescue StandardError
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @param payload [Array, nil] what {dump} produced.
|
|
59
|
+
# @return [RBS::Location, nil]
|
|
60
|
+
def load(payload)
|
|
61
|
+
return nil if payload.nil?
|
|
62
|
+
|
|
63
|
+
name, start_line, start_column, end_line, end_column = payload
|
|
64
|
+
name = ::RBS::Location::CACHED_BUFFER_NAME if name.nil? || name.empty?
|
|
65
|
+
buffer = Buffer.new(name: name, start_loc: [start_line, start_column], end_loc: [end_line, end_column])
|
|
66
|
+
::RBS::Location.new(buffer: buffer, start_pos: START_POS, end_pos: END_POS)
|
|
67
|
+
rescue StandardError
|
|
68
|
+
nil
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require "rbs"
|
|
4
4
|
|
|
5
|
+
require_relative "annotation_location"
|
|
6
|
+
|
|
5
7
|
# Adds `_dump` / `_load` to {RBS::Location} so an `RBS::Environment` (and its transitive AST nodes, all of
|
|
6
8
|
# which carry Locations) round-trips through `Marshal`. The rbs gem's C-extension `RBS::Location` ships
|
|
7
9
|
# without the Marshal hooks; until rbs grows them upstream this patch is the minimal monkey-patch the v0.0.9
|
|
@@ -31,6 +33,25 @@ require "rbs"
|
|
|
31
33
|
# than a cold run does. ADR-6's store never evicts, so that would persist. `Cache::Store::FORMAT_VERSION`
|
|
32
34
|
# is therefore bumped to 3: `PAYLOAD_ABI_VERSION` already rebuilds across a release, and the bump closes
|
|
33
35
|
# the same-version window too.
|
|
36
|
+
# - `RBS::AST::Annotation` carries its own `marshal_dump` / `marshal_load`, which keep the annotation's
|
|
37
|
+
# POSITION as well as its file (issue #799). "Nothing reads positions" was never quite true: the two
|
|
38
|
+
# `conforms-to` rows — `rbs_extended.unsatisfied-conformance` and `dynamic.rbs-extended.unresolved` —
|
|
39
|
+
# are reported AT the directive, because there is no Ruby `def` a missing interface method could be
|
|
40
|
+
# reported at, so both read `start_line` / `start_column` off an annotation's location. (They are the
|
|
41
|
+
# only readers: {Rigor::RbsExtended::EnvelopeScanner} positions `effect.unknown-label` at an annotation
|
|
42
|
+
# too, but reaches it by parsing the project's own `.rbs` rather than the built env — refusing this very
|
|
43
|
+
# loss is one of the two reasons it does.) Cold the row is `sig/buffer.rbs:6:1`; through a `_dump`ed
|
|
44
|
+
# location it collapsed to `1:1`, which made `--verify-incremental` fail outright on any project carrying
|
|
45
|
+
# an unsatisfied directive (the replica normalises rows by position, and only one side of the comparison
|
|
46
|
+
# runs against the cached env) and made a warm `--incremental` run move a row on a tree that had not
|
|
47
|
+
# changed. The carry is scoped to annotations rather than done in `_dump` for the reason the paragraph
|
|
48
|
+
# above gives: a Location hangs off every AST node, and carrying its position measured +2.9% on Rigor's
|
|
49
|
+
# own env (9,941K to 10,229K) on top of the names. An annotation is rare by comparison — 18 of them in
|
|
50
|
+
# that same env — so the same carry costs +1.4K there, which is the whole argument for scoping it here
|
|
51
|
+
# rather than widening `_dump`. `Cache::Store::FORMAT_VERSION` is bumped to 4 for the same reason it was
|
|
52
|
+
# bumped to 3: a pre-change blob still loads (Marshal encodes an ivar dump and a `marshal_dump` payload
|
|
53
|
+
# differently, and only the latter reaches `marshal_load`), so without the bump a stale blob would keep
|
|
54
|
+
# reporting the moved position indefinitely.
|
|
34
55
|
#
|
|
35
56
|
# Idempotent: the guard checks `method_defined?(:_dump)` so requiring this file twice (or against an upstream
|
|
36
57
|
# rbs that adds Marshal hooks itself) is a no-op.
|
|
@@ -67,6 +88,24 @@ module RBS
|
|
|
67
88
|
end
|
|
68
89
|
end
|
|
69
90
|
|
|
91
|
+
module AST
|
|
92
|
+
class Annotation
|
|
93
|
+
# Carries the annotation's POSITION across the cache, not just its file. See the header's fourth
|
|
94
|
+
# bullet for why this one AST node opts out of the position-dropping `RBS::Location#_dump` above.
|
|
95
|
+
unless method_defined?(:marshal_dump)
|
|
96
|
+
def marshal_dump
|
|
97
|
+
[string, Rigor::Cache::AnnotationLocation.dump(location)]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def marshal_load(payload)
|
|
101
|
+
dumped_string, dumped_location = payload
|
|
102
|
+
@string = dumped_string
|
|
103
|
+
@location = Rigor::Cache::AnnotationLocation.load(dumped_location)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
70
109
|
class Namespace
|
|
71
110
|
unless method_defined?(:_dump)
|
|
72
111
|
def _dump(_)
|
data/lib/rigor/cache/store.rb
CHANGED
|
@@ -38,7 +38,14 @@ module Rigor
|
|
|
38
38
|
# `PAYLOAD_ABI_VERSION` already rebuilds across a RELEASE, so the exposure is a same-version tree; this
|
|
39
39
|
# closes that window too, because "the same project reports differently depending on how you ran it" is
|
|
40
40
|
# the defect the diagnostic exists to end and a stale blob reintroduces it.
|
|
41
|
-
|
|
41
|
+
#
|
|
42
|
+
# v4 (issue #799): `RBS::AST::Annotation` now carries its own `marshal_dump` / `marshal_load`, which
|
|
43
|
+
# keep the annotation's POSITION as well as its file, so `rbs_extended.unsatisfied-conformance` points
|
|
44
|
+
# at the `%a{rigor:v1:conforms-to …}` line warm as well as cold. A pre-change blob loads perfectly well
|
|
45
|
+
# — Marshal encodes an ivar dump and a `marshal_dump` payload differently, and only the latter reaches
|
|
46
|
+
# `marshal_load` — which is exactly the problem: without a bump it would keep reporting the row at
|
|
47
|
+
# `1:1` forever, and `--verify-incremental` would keep failing on that project.
|
|
48
|
+
FORMAT_VERSION = 4
|
|
42
49
|
|
|
43
50
|
# Payload ABI version. Store values are mostly Marshal blobs of Rigor/RBS objects, so a Rigor release
|
|
44
51
|
# upgrade is an ABI boundary even when the byte layout and descriptor schema are unchanged. Folding the
|
|
@@ -33,6 +33,12 @@ module Rigor
|
|
|
33
33
|
VALID_PARAM_POLICIES = %w[untyped observed observed-strict].freeze
|
|
34
34
|
VALID_FORMATS = %w[text json].freeze
|
|
35
35
|
|
|
36
|
+
# The skip reasons {#report_skipped} counts. The two left out each have a detailed report of their own
|
|
37
|
+
# ({#report_unrenderable}, {#report_unresolvable_superclasses}), so a method never shows up in two tallies.
|
|
38
|
+
SUMMARISED_SKIP_REASONS = (SigGen::Classification::SKIP_DIAGNOSTIC_IDS.keys -
|
|
39
|
+
%i[unrenderable_rbs unresolvable_superclass]).freeze
|
|
40
|
+
private_constant :SUMMARISED_SKIP_REASONS
|
|
41
|
+
|
|
36
42
|
# @return [Integer] CLI exit status.
|
|
37
43
|
def run
|
|
38
44
|
options = parse_options
|
|
@@ -54,6 +60,7 @@ module Rigor
|
|
|
54
60
|
dispatch_print_or_diff(candidates, mode, options)
|
|
55
61
|
0
|
|
56
62
|
end
|
|
63
|
+
report_skipped(candidates, options)
|
|
57
64
|
report_unrenderable(generator.unrenderable)
|
|
58
65
|
report_unresolvable_superclasses(generator.unresolvable_superclasses)
|
|
59
66
|
status
|
|
@@ -61,6 +68,28 @@ module Rigor
|
|
|
61
68
|
|
|
62
69
|
private
|
|
63
70
|
|
|
71
|
+
# Issue #778 — one stderr line per run saying how many methods the generator declined and why, so a
|
|
72
|
+
# method missing from the output is never a silent absence. Text mode only: under `--format=json` every
|
|
73
|
+
# skipped row is already in the payload with its `skip_reason`, and stderr stays clean for the consumer.
|
|
74
|
+
# Per-method lines would be noise at project scale; the JSON payload is where each one is named.
|
|
75
|
+
def report_skipped(candidates, options)
|
|
76
|
+
return unless options.fetch(:format) == "text"
|
|
77
|
+
|
|
78
|
+
counts = candidates.each_with_object(Hash.new(0)) do |candidate, acc|
|
|
79
|
+
next unless candidate.classification == SigGen::Classification::SKIPPED
|
|
80
|
+
next unless SUMMARISED_SKIP_REASONS.include?(candidate.skip_reason)
|
|
81
|
+
|
|
82
|
+
acc[candidate.skip_reason] += 1
|
|
83
|
+
end
|
|
84
|
+
return if counts.empty?
|
|
85
|
+
|
|
86
|
+
breakdown = counts.map { |reason, n| "#{SigGen::Classification::SKIP_DIAGNOSTIC_IDS.fetch(reason)}: #{n}" }
|
|
87
|
+
@err.puts(
|
|
88
|
+
"rigor sig-gen: skipped #{counts.values.sum} method(s) it could not type or would not overwrite " \
|
|
89
|
+
"(#{breakdown.join(', ')}). Run with --format=json to see each one with its skip_reason."
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
|
|
64
93
|
# A method whose rendered RBS does not parse is a Rigor rendering defect, not a fact about the user's
|
|
65
94
|
# code — the generator skipped it (so the rest of the signatures are still usable and still valid), but
|
|
66
95
|
# staying silent would leave the user with a quietly incomplete `sig/` and us with an unreported bug.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
class Environment
|
|
5
|
+
# Issue #784 — a mutable, first-write-wins record of one shared-build failure, held by the otherwise
|
|
6
|
+
# frozen {Environment} the way {HktRegistryHolder} holds a memoised value. A lazy build that raises
|
|
7
|
+
# during per-file analysis records here at its seam instead of raising into every file's
|
|
8
|
+
# `analyze_body` rescue; the coordinator snapshots the slot after the file loop (the build is lazy, so
|
|
9
|
+
# a snapshot taken before the loop reads nothing) and the aggregator surfaces it once for the run.
|
|
10
|
+
#
|
|
11
|
+
# First-write-wins: the build is memoised, so a second failure can only be the same one re-observed.
|
|
12
|
+
#
|
|
13
|
+
# Concurrency: single-threaded use only, the same discipline as {HktRegistryHolder}.
|
|
14
|
+
class FailureSlot
|
|
15
|
+
def initialize
|
|
16
|
+
@value = nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @param value [Array] a Marshal-clean tuple — the fork pool ships it back from the worker.
|
|
20
|
+
def record(value)
|
|
21
|
+
@value = value.freeze if @value.nil?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @return [Array, nil] the recorded tuple, or nil when the build never failed.
|
|
25
|
+
attr_reader :value
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|