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.
@@ -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, "internal analyzer error: #{e.class}: #{e.message}")]
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
- # source_location) pair when the projection actually degraded. The builders return
563
- # the source carrier unchanged on non-HashShape / non-Tuple receivers (see
564
- # `Type::Combinator.pick_of` / `omit_of` and the HashShape-only `partial_of` /
565
- # `required_of` / `readonly_of`), so detection is "first arg was lossy".
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
- @reporter.record_lossy_projection(
574
- head: node.head,
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(_)
@@ -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
- FORMAT_VERSION = 3
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