rigortype 0.3.4 → 0.3.6

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 (103) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +5 -5
  3. data/data/effects/core.yml +18 -1
  4. data/data/effects/registry.yml +31 -3
  5. data/data/gem_overlay/activesupport/core_ext.rbs +33 -0
  6. data/docs/handbook/02-everyday-types.md +1 -1
  7. data/docs/handbook/04-tuples-and-shapes.md +1 -1
  8. data/docs/handbook/08-understanding-errors.md +1 -1
  9. data/docs/handbook/09-plugins.md +2 -2
  10. data/docs/handbook/10-sorbet.md +1 -1
  11. data/docs/handbook/README.md +2 -2
  12. data/docs/handbook/appendix-go.md +1 -1
  13. data/docs/handbook/appendix-java-csharp.md +2 -2
  14. data/docs/handbook/appendix-mypy.md +1 -1
  15. data/docs/handbook/appendix-protocols-and-structural-typing.md +2 -2
  16. data/docs/handbook/appendix-rust.md +1 -1
  17. data/docs/handbook/appendix-type-theory.md +2 -2
  18. data/docs/handbook/appendix-typescript.md +4 -4
  19. data/docs/manual/02-cli-reference.md +73 -22
  20. data/docs/manual/03-configuration.md +19 -2
  21. data/docs/manual/04-diagnostics.md +15 -2
  22. data/docs/manual/08-skills.md +1 -1
  23. data/docs/manual/11-ci.md +38 -1
  24. data/docs/manual/12-caching.md +40 -1
  25. data/docs/manual/15-type-protection-coverage.md +1 -1
  26. data/docs/manual/16-rbs-extended-annotations.md +15 -2
  27. data/docs/manual/18-removing-dead-code.md +13 -4
  28. data/docs/manual/19-effect-labels.md +678 -0
  29. data/docs/manual/README.md +7 -2
  30. data/docs/manual/ci-templates/README.md +9 -0
  31. data/docs/manual/plugins/rigor-actionmailer.md +4 -4
  32. data/docs/manual/plugins/rigor-activejob.md +3 -3
  33. data/docs/manual/plugins/rigor-activerecord.md +4 -4
  34. data/docs/manual/plugins/rigor-rails-i18n.md +5 -5
  35. data/docs/manual/plugins/rigor-rspec.md +6 -2
  36. data/lib/rigor/analysis/diagnostic.rb +17 -6
  37. data/lib/rigor/analysis/effects_cache_probe.rb +132 -0
  38. data/lib/rigor/analysis/reachability/graph.rb +30 -11
  39. data/lib/rigor/analysis/reachability/plugin_roots.rb +11 -8
  40. data/lib/rigor/analysis/reachability/scan.rb +20 -4
  41. data/lib/rigor/analysis/reachability/scan_cache.rb +130 -0
  42. data/lib/rigor/analysis/rule_catalog.rb +10 -3
  43. data/lib/rigor/analysis/run_cache_key.rb +12 -0
  44. data/lib/rigor/analysis/run_cache_probe.rb +69 -1
  45. data/lib/rigor/analysis/runner/declaration_position.rb +8 -24
  46. data/lib/rigor/analysis/runner/effect_annotation_residual_pass.rb +28 -28
  47. data/lib/rigor/analysis/runner/effect_envelope_pass.rb +15 -9
  48. data/lib/rigor/analysis/runner/pool_coordinator.rb +25 -0
  49. data/lib/rigor/analysis/runner/run_snapshots.rb +7 -4
  50. data/lib/rigor/analysis/runner.rb +163 -31
  51. data/lib/rigor/analysis/worker_session.rb +3 -1
  52. data/lib/rigor/cache/file_digest.rb +20 -2
  53. data/lib/rigor/cli/check_command.rb +67 -48
  54. data/lib/rigor/cli/coverage_command.rb +5 -6
  55. data/lib/rigor/cli/doc_links.rb +100 -0
  56. data/lib/rigor/cli/docs_command.rb +32 -2
  57. data/lib/rigor/cli/effects_command.rb +149 -10
  58. data/lib/rigor/cli/effects_diff_renderer.rb +88 -14
  59. data/lib/rigor/cli/effects_explain_renderer.rb +6 -3
  60. data/lib/rigor/cli/effects_renderer.rb +41 -3
  61. data/lib/rigor/cli/effects_report.rb +116 -6
  62. data/lib/rigor/cli/effects_snapshot_command.rb +75 -11
  63. data/lib/rigor/cli/unused_command.rb +65 -16
  64. data/lib/rigor/cli.rb +33 -9
  65. data/lib/rigor/configuration.rb +37 -25
  66. data/lib/rigor/configuration_error.rb +20 -0
  67. data/lib/rigor/effects/collector.rb +38 -1
  68. data/lib/rigor/effects/definition_lines.rb +100 -0
  69. data/lib/rigor/effects/entry_points.rb +47 -1
  70. data/lib/rigor/effects/envelope_check.rb +18 -1
  71. data/lib/rigor/effects/file_collection.rb +18 -4
  72. data/lib/rigor/effects/framework_units.rb +68 -13
  73. data/lib/rigor/effects/inline_anchor.rb +134 -0
  74. data/lib/rigor/effects/liskov_check.rb +17 -8
  75. data/lib/rigor/effects/plugin_facts.rb +62 -13
  76. data/lib/rigor/effects/propagator.rb +79 -19
  77. data/lib/rigor/effects/registry.rb +10 -3
  78. data/lib/rigor/effects/scanner.rb +21 -9
  79. data/lib/rigor/effects/signature_sources.rb +29 -2
  80. data/lib/rigor/effects/snapshot.rb +74 -26
  81. data/lib/rigor/effects/snapshot_diff.rb +26 -3
  82. data/lib/rigor/effects/taint_cause.rb +1 -0
  83. data/lib/rigor/effects/unit_scan.rb +87 -12
  84. data/lib/rigor/inference/synthetic_method_scanner.rb +7 -0
  85. data/lib/rigor/plugin/base.rb +4 -0
  86. data/lib/rigor/plugin/box.rb +18 -2
  87. data/lib/rigor/plugin/effect_ancestry.rb +80 -0
  88. data/lib/rigor/plugin/manifest.rb +34 -10
  89. data/lib/rigor/plugin/registry.rb +10 -3
  90. data/lib/rigor/rbs_extended/envelope_scanner.rb +9 -0
  91. data/lib/rigor/rbs_extended.rb +22 -2
  92. data/lib/rigor/version.rb +1 -1
  93. data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +17 -2
  94. data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +27 -0
  95. data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +56 -1
  96. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/effects.rb +75 -0
  97. data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +13 -1
  98. data/skills/rigor-ci-setup/SKILL.md +2 -2
  99. data/skills/rigor-editor-setup/SKILL.md +2 -2
  100. data/skills/rigor-mcp-setup/SKILL.md +2 -2
  101. data/skills/rigor-monkeypatch-resolve/SKILL.md +1 -1
  102. data/skills/rigor-plugin-review/SKILL.md +3 -3
  103. metadata +10 -1
@@ -4,8 +4,10 @@ require_relative "result"
4
4
  require_relative "run_cache_key"
5
5
  require_relative "path_expansion"
6
6
  require_relative "severity_stamp"
7
+ require_relative "runner/effect_annotation_residual_pass"
7
8
  require_relative "../cache/store"
8
9
  require_relative "../cache/file_digest"
10
+ require_relative "../effects/signature_sources"
9
11
 
10
12
  module Rigor
11
13
  module Analysis
@@ -23,6 +25,31 @@ module Rigor
23
25
  # sound because their inputs are in that same validated set. The key is built through the shared
24
26
  # {RunCacheKey} — a project whose plugins synthesise virtual RBS produces a probe key that omits that
25
27
  # entry, so it simply misses and the full path takes over (never a wrong hit).
28
+ #
29
+ # ## What the slot does NOT contain (#428)
30
+ #
31
+ # ADR-103 puts the two effect diagnostics OUTSIDE the cached run assembly on purpose: the `effects:`
32
+ # block is deliberately absent from the diagnostics cache identity, so a finding written into that
33
+ # entry would outlive the configuration that produced it. `Runner#run_analysis` therefore appends them
34
+ # after `#compute_run_diagnostics` — which is exactly the code a served hit skips. Serving the slot
35
+ # verbatim silently dropped both of them on every warm run, and a check that only ever fires on a cold
36
+ # cache is worse than one that never fires at all.
37
+ #
38
+ # A probe that serves a slot has to answer for what the slot omits, so this one does, per pass:
39
+ #
40
+ # - `effect.annotations-unchecked` is **reproduced here**. It was built to be free (a glob and a regex
41
+ # over the project's own signature tree — {Effects::SignatureSources}), so the probe simply runs it,
42
+ # with no virtual RBS: the inline stratum is the documented fail-quiet direction of that pass, and it
43
+ # is the stratum a run without an environment never had. #441 — that omission is unreachable rather
44
+ # than merely tolerated, because a project whose plugins synthesise virtual RBS is exactly the one
45
+ # whose probe key omits the `rbs.virtual_rbs` entry (§ above), so it misses here and the full path —
46
+ # which carries the stratum in every run mode — answers instead.
47
+ # - `effect.envelope-exceeded` / `effect.liskov-widened` / `effect.unknown-label` cannot be: they read
48
+ # the propagated effect graph and the cross-file discovery tables, i.e. the engine this path exists
49
+ # to skip. So the probe **declines** for a project that could earn one ({#envelope_lane_live?}) and
50
+ # the full path — which caches both halves in its own two slots, and re-judges them every run —
51
+ # serves it instead. The decline is measured against the declarations alone, never against what they
52
+ # would judge to, so it costs one glob and never a wrong answer.
26
53
  class RunCacheProbe
27
54
  # @param configuration [Rigor::Configuration]
28
55
  # @param cache_root [String]
@@ -48,14 +75,55 @@ module Rigor
48
75
 
49
76
  diagnostics = validated_diagnostics(key)
50
77
  return nil if diagnostics.nil?
78
+ # #428 — asked only after the peek, so a run that was going to miss anyway never pays the walk.
79
+ return nil if envelope_lane_live?
51
80
 
52
- Result.new(diagnostics: SeverityStamp.apply(diagnostics, @configuration), stats: nil)
81
+ Result.new(
82
+ diagnostics: SeverityStamp.apply(diagnostics + residual_diagnostics, @configuration), stats: nil
83
+ )
53
84
  rescue StandardError
54
85
  nil
55
86
  end
56
87
 
57
88
  private
58
89
 
90
+ # #428 — whether this project could earn one of the three diagnostics {Runner::EffectEnvelopePass}
91
+ # produces, which are the ones no cached slot carries and no engine-free path can recompute.
92
+ #
93
+ # Read off the DECLARATIONS, because a declaration is the whole of what the engine-free side can
94
+ # see: the four `.rigor.yml` policy lists, and whether the project's own signature tree carries an
95
+ # effect annotation at all ({Effects::SignatureSources::ANNOTATION_HINT}, the same one-regex-per-file
96
+ # pre-filter the envelope reader routes on). Over-declining is free — it forgoes a fast lane for a
97
+ # run the full path still serves out of the same two warm slots — while under-declining is the bug
98
+ # this method exists for, so anything ambiguous answers true.
99
+ def envelope_lane_live?
100
+ return false unless @configuration.effects_check?
101
+
102
+ declared_in_config? || !Effects::SignatureSources.first_annotated(signature_sources).nil?
103
+ end
104
+
105
+ # The `effects:` policy surface an `effect.unknown-label` can be read off, plus the envelopes an
106
+ # `effect.envelope-exceeded` / `effect.liskov-widened` is judged against. A project that opted into
107
+ # effects and declared none of them has nothing for the pass to say.
108
+ def declared_in_config?
109
+ !@configuration.effects_envelopes.empty? || !@configuration.effects_tolerated.empty? ||
110
+ !@configuration.effects_labels.empty? || !@configuration.effects_attribution.empty?
111
+ end
112
+
113
+ # `effect.annotations-unchecked`, reproduced verbatim — the pass self-gates on `effects_enabled?`,
114
+ # so exactly one of it and {#envelope_lane_live?} is ever non-empty, as on the full path.
115
+ def residual_diagnostics
116
+ Runner::EffectAnnotationResidualPass.new(configuration: @configuration).diagnostics
117
+ end
118
+
119
+ # One walk, whichever of the two lanes asks: the residual pass collects its own (it is the one that
120
+ # runs when effects are off), so the memo only ever serves the envelope-lane decline.
121
+ def signature_sources
122
+ @signature_sources ||= Effects::SignatureSources.collect(
123
+ signature_paths: @configuration.signature_paths
124
+ )
125
+ end
126
+
59
127
  def validated_diagnostics(key)
60
128
  store = Cache::Store.new(root: @cache_root, max_bytes: @configuration.cache_max_bytes)
61
129
  # The digest fallback + `cache.validation` / RIGOR_STRICT_VALIDATION escape hatch route through the
@@ -10,17 +10,19 @@ module Rigor
10
10
  # one (the `rbs_extended.unsatisfied-conformance` precedent) — there is no Ruby `def` that could
11
11
  # carry the typo. A value written in configuration has no location at all and lands at
12
12
  # `.rigor.yml:1`, the `rbs.coverage.quarantined-signature` precedent.
13
+ # A declaration's `location` is already the position a reader can open, `.rbs` and rbs-inline
14
+ # alike: the synthesized-buffer line is re-anchored onto the Ruby file where the envelope is
15
+ # built ({Rigor::Effects::InlineAnchor}, #432), so this module only has to split it. Doing the
16
+ # re-anchoring here as well used to be the fix, and it could not tell two identically-spelled
17
+ # annotations in one file apart — every finding in it landed on the first.
13
18
  module DeclarationPosition
14
19
  CONFIG_PATH = ".rigor.yml"
15
- RUBY_EXTENSION = ".rb"
16
- private_constant :RUBY_EXTENSION
17
20
 
18
21
  module_function
19
22
 
20
- # @param finding [#location, #spelling]
21
- # @param sources [Hash{String => String}] in-memory sources, for the buffer-backed run path
23
+ # @param finding [#location]
22
24
  # @return [Array(String, Integer)] `[path, line]`
23
- def of(finding, sources: {})
25
+ def of(finding)
24
26
  location = finding.location
25
27
  return [CONFIG_PATH, 1] if location.nil?
26
28
 
@@ -28,25 +30,7 @@ module Rigor
28
30
  return [CONFIG_PATH, 1] if path.empty?
29
31
 
30
32
  line = raw_line.to_i
31
- line = 1 unless line.positive?
32
- return [path, line] unless path.end_with?(RUBY_EXTENSION)
33
-
34
- [path, inline_line(path, finding.spelling, sources) || line]
35
- end
36
-
37
- # rbs-inline's writer re-emits the author's own comment block ABOVE the annotation it generates,
38
- # so a line number read out of the synthesized buffer drifts from the `.rb` line the author
39
- # actually wrote — by the length of every method body above it. The annotation's own text is
40
- # unique enough to find again, so the Ruby file is what answers. One read, and only when a
41
- # finding already exists.
42
- def inline_line(path, spelling, sources)
43
- return nil if spelling.nil?
44
-
45
- source = sources[path] || File.read(path)
46
- source.each_line.with_index(1) { |line, number| return number if line.include?(spelling) }
47
- nil
48
- rescue StandardError
49
- nil
33
+ [path, line.positive? ? line : 1]
50
34
  end
51
35
  end
52
36
  end
@@ -1,7 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../diagnostic"
4
- require_relative "../check_rules"
4
+ # ADR-87 WD4 — the pure-data rule-id table, never the engine-heavy `check_rules.rb` that reopens the
5
+ # same module. This pass is one of the two the boot-slimming {Analysis::RunCacheProbe} has to run for
6
+ # itself on a cache hit (#428), and requiring the full rule set would put `rigor/inference` back into
7
+ # a hit's `$LOADED_FEATURES`.
8
+ require_relative "../check_rules/rule_ids"
9
+ require_relative "../../effects/inline_anchor"
5
10
  require_relative "../../effects/signature_sources"
6
11
 
7
12
  module Rigor
@@ -25,9 +30,15 @@ module Rigor
25
30
  # parse, no analysis, no environment build, and nothing at all when the tree does not exist.
26
31
  # - It consults the run's virtual RBS — rbs-inline's `# @rbs %a{…}` — only when the run ALREADY
27
32
  # resolved an environment. Building one here to find an `:info` would cost more than the
28
- # `:info` is worth, so an inline-only annotation is reported when the run happens to have a
29
- # loader at hand and not otherwise. Under-reporting is the fail-quiet direction; the check
30
- # itself never depends on this pass.
33
+ # `:info` is worth, so the pass never reaches for a loader; every caller hands it what the run
34
+ # had. #441 is why the caller side matters: an ANALYZING run always has an environment, so the
35
+ # stratum is carried out of the analysis path as
36
+ # {Runner::RunSnapshots#effect_annotation_carrier} whatever its mode, and the two lanes of
37
+ # chapter 16 report the same annotation. The one shape that genuinely has no environment is a
38
+ # warm ADR-87 WD4 cache hit served by {Analysis::RunCacheProbe} — an engine-free path whose
39
+ # whole point is not building one — so there, and only there, an inline-only annotation goes
40
+ # unreported. Under-reporting is the fail-quiet direction; the check itself never depends on
41
+ # this pass.
31
42
  #
32
43
  # It is computed OUTSIDE the cached run assembly for the same reason `EffectEnvelopePass` is:
33
44
  # the `effects:` block is deliberately absent from the diagnostics cache identity, so a residual
@@ -38,9 +49,6 @@ module Rigor
38
49
 
39
50
  RULE = CheckRules::RULE_EFFECT_ANNOTATIONS_UNCHECKED
40
51
 
41
- RUBY_EXTENSION = ".rb"
42
- private_constant :RUBY_EXTENSION
43
-
44
52
  MESSAGE = "Effect annotations (`%a{pure}` / `%a{rigor:v1:effect …}`) are present in your " \
45
53
  "project's signatures, but `.rigor.yml` carries no `effects:` block, so effect " \
46
54
  "collection never runs and nothing checks them — they are documentation, not a " \
@@ -51,11 +59,12 @@ module Rigor
51
59
  private_constant :MESSAGE
52
60
 
53
61
  # @param configuration [Rigor::Configuration]
54
- # @param rbs_loader [Rigor::Environment::RbsLoader, nil] a loader the run ALREADY has; never
55
- # one built for this pass. `nil` simply drops the virtual-RBS stratum.
56
- def initialize(configuration:, rbs_loader: nil)
62
+ # @param virtual_rbs [Array<Array(String, String)>, nil] `[buffer name, RBS source]` pairs the
63
+ # run ALREADY resolved; never a loader built for this pass. Empty / nil simply drops the
64
+ # virtual-RBS stratum.
65
+ def initialize(configuration:, virtual_rbs: nil)
57
66
  @configuration = configuration
58
- @rbs_loader = rbs_loader
67
+ @virtual_rbs = virtual_rbs
59
68
  end
60
69
 
61
70
  # @return [Array<Diagnostic>] zero or one.
@@ -64,7 +73,7 @@ module Rigor
64
73
  return NO_DIAGNOSTICS if @configuration.disabled_rules.include?(RULE)
65
74
 
66
75
  sources = Effects::SignatureSources.collect(
67
- signature_paths: @configuration.signature_paths, virtual_rbs: @rbs_loader&.virtual_rbs
76
+ signature_paths: @configuration.signature_paths, virtual_rbs: @virtual_rbs
68
77
  )
69
78
  return NO_DIAGNOSTICS if sources.empty?
70
79
 
@@ -80,28 +89,19 @@ module Rigor
80
89
  # Positioned at the first annotation itself rather than at `.rigor.yml:1`: the fix is a config
81
90
  # edit, but the thing being reported is something the author wrote, and pointing at it is what
82
91
  # tells them WHICH declaration is inert.
92
+ #
93
+ # A `virtual:` buffer's line numbers are the synthesized RBS's, not the `.rb`'s, so the position
94
+ # is re-anchored onto the Ruby file the same way the envelope reader anchors a declaration
95
+ # ({Effects::InlineAnchor}) — one mapping, so the advisory and the check can never name two
96
+ # different lines for one annotation.
83
97
  def build_diagnostic(found)
84
98
  name, content, line = found
85
99
  path = Effects::SignatureSources.source_path(name)
86
- line = inline_line(path, content, line) if path.end_with?(RUBY_EXTENSION)
87
100
  Diagnostic.new(
88
- path: path, line: line, column: 1, message: MESSAGE,
89
- severity: :info, rule: RULE, source_family: :builtin
101
+ path: path, line: Effects::InlineAnchor.ruby_line(path: path, buffer: content, buffer_line: line),
102
+ column: 1, message: MESSAGE, severity: :info, rule: RULE, source_family: :builtin
90
103
  )
91
104
  end
92
-
93
- # A `virtual:` buffer's line numbers are the synthesized RBS's, not the `.rb`'s — rbs-inline
94
- # re-emits the author's comment block above each generated member, so the two drift apart by
95
- # the length of every body above. Find the annotation's own text in the Ruby file instead.
96
- def inline_line(path, content, line)
97
- spelling = content.each_line.to_a[line - 1].to_s[/%a\{[^}]*\}/]
98
- return line if spelling.nil?
99
-
100
- File.foreach(path).with_index(1) { |source_line, number| return number if source_line.include?(spelling) }
101
- line
102
- rescue StandardError
103
- line
104
- end
105
105
  end
106
106
  end
107
107
  end
@@ -30,11 +30,13 @@ module Rigor
30
30
  #
31
31
  # 1. **Read the envelopes** off the project's own RBS ({RbsExtended::EnvelopeScanner}). A project
32
32
  # with `effects:` but no envelope pays this walk and stops here.
33
- # 2. **Force cross-file discovery**, but only when step 1 found something — the diagnostic is
34
- # positioned at the Ruby `def`, and the discovery tables are what map a method key to one.
35
- # 3. **Judge** ({Effects::EnvelopeCheck}) and render, then run the findings through the ordinary
36
- # suppression filter so `# rigor:disable effect.envelope-exceeded` on the `def` line and
37
- # `disable:` in `.rigor.yml` work exactly as they do for a per-file rule.
33
+ # 2. **Judge** ({Effects::EnvelopeCheck}). Cross-file discovery — the diagnostic is positioned
34
+ # at the Ruby `def`, and the discovery tables are what map a method key to one — is forced
35
+ # from inside the judgment, on the first finding built, so an envelope nothing exceeds costs
36
+ # no discovery parse at all.
37
+ # 3. **Render**, then run the findings through the ordinary suppression filter so
38
+ # `# rigor:disable effect.envelope-exceeded` on the `def` line and `disable:` in `.rigor.yml`
39
+ # work exactly as they do for a per-file rule.
38
40
  #
39
41
  # **Why this is not part of the cached run assembly.** ADR-103 WD12 says envelope diagnostics are
40
42
  # recomputed every run from the (possibly cached) summaries and never stored. The `effects:` block
@@ -80,7 +82,8 @@ module Rigor
80
82
  # @param rbs_loader [Rigor::Environment::RbsLoader, nil] the run's loader; nil disables the pass.
81
83
  # @param effect_table [Rigor::Effects::EffectTable] the propagated graph.
82
84
  # @param discovery [#call] forces and returns the cross-file discovery tables as
83
- # `[def_sources, singleton_def_sources, class_sources]`. Called only when an envelope exists.
85
+ # `[def_sources, singleton_def_sources, class_sources]`. Called only when a finding needs a
86
+ # position — a judged-clean envelope never forces it.
84
87
  # @param sources [Hash{String => String}] in-memory sources, for the buffer-backed run path.
85
88
  # @param unit_sources [Hash{String => Array<String>}] `Runner#effect_sources` — where each effect
86
89
  # unit is defined, which is what an `effects.envelopes[].match:` path glob selects on. Its paths
@@ -220,7 +223,7 @@ module Rigor
220
223
  end
221
224
 
222
225
  def position_of(finding)
223
- DeclarationPosition.of(finding, sources: @sources)
226
+ DeclarationPosition.of(finding)
224
227
  end
225
228
 
226
229
  # The vocabulary an unknown label is judged against: the shipped registry plus whatever
@@ -272,9 +275,12 @@ module Rigor
272
275
  )
273
276
  end
274
277
 
275
- # The discovery tables both judgments position their findings from, forced once.
278
+ # The discovery tables both judgments position their findings from behind a deferred value,
279
+ # forced at most once, on the first finding either judgment builds. A clean run (an envelope
280
+ # nothing exceeds — the common CI case) therefore never calls `@discovery` at all, which on a
281
+ # warm run is the difference between a no-op and a Prism parse of every project file.
276
282
  def positions
277
- @positions ||= begin
283
+ @positions ||= Effects::EnvelopeCheck::DeferredPositions.new do
278
284
  def_sources, singleton_def_sources, class_sources = @discovery.call
279
285
  Effects::EnvelopeCheck::Positions.build(
280
286
  def_sources: def_sources, singleton_def_sources: singleton_def_sources,
@@ -6,6 +6,7 @@ require_relative "../../environment"
6
6
  require_relative "../diagnostic"
7
7
  require_relative "../worker_session"
8
8
  require_relative "../run_stats"
9
+ require_relative "../../effects/signature_sources"
9
10
  require_relative "../../rbs_extended/conformance_checker"
10
11
  require_relative "../../runtime/jit"
11
12
 
@@ -123,6 +124,7 @@ module Rigor
123
124
 
124
125
  def analyze_files_sequentially(files, environment)
125
126
  snapshot_project_signature_state(environment)
127
+ snapshot_effect_annotation_carrier(environment&.rbs_loader)
126
128
  result = files.flat_map { |path| @analyze_file.call(path, environment) }
127
129
  if @collect_stats
128
130
  loader = environment.rbs_loader
@@ -157,6 +159,27 @@ module Rigor
157
159
  @snapshots.conformance_results = RbsExtended::ConformanceChecker.scan(loader)
158
160
  end
159
161
 
162
+ # #441 — the inline stratum of `effect.annotations-unchecked`, carried out of the analysis path as
163
+ # small plain data, for the same reason the block above is: the environment stays a LOCAL so it goes
164
+ # GC-eligible when the path returns, and the residual pass runs after it.
165
+ #
166
+ # {Runner#effect_annotation_residual_diagnostics} used to read the loader off `@run_environment`,
167
+ # which only the ADR-45 result-cacheable path ever assigns — so a `--no-cache`, `--workers N` or
168
+ # `--incremental` run reported a `.rbs` annotation and stayed silent about the identical one written
169
+ # as an rbs-inline comment, while a default run reported both. The environment was there either way;
170
+ # nothing was carrying it. {Effects::SignatureSources.annotated_carrier} reduces it to at most ONE
171
+ # synthesized buffer, which is all a one-`:info`-per-run pass can spend.
172
+ #
173
+ # Skipped when collection is on — then {Runner::EffectEnvelopePass} reads the loader directly and
174
+ # this pass is gated off — so a collecting run pays nothing, and a project with no synthesizing
175
+ # plugin answers with an empty array and no regex at all.
176
+ def snapshot_effect_annotation_carrier(loader)
177
+ virtual = @record_effects ? nil : loader&.virtual_rbs
178
+ @snapshots.effect_annotation_carrier = Effects::SignatureSources.annotated_carrier(virtual)
179
+ rescue StandardError
180
+ @snapshots.effect_annotation_carrier = [].freeze
181
+ end
182
+
160
183
  # Sequential-mode environment resolver. Returns the supplied `environment:` override (with the
161
184
  # runner's fresh per-run reporter pair attached so dispatcher events route to THIS runner's
162
185
  # diagnostics) when present; otherwise builds a fresh Environment per-call via
@@ -408,6 +431,7 @@ module Rigor
408
431
  # Force the full RBS load on the parent so children copy-on-write inherit a warm Environment
409
432
  # rather than each rebuilding it after the fork.
410
433
  session.environment.rbs_loader&.prewarm
434
+ snapshot_effect_annotation_carrier(session.environment.rbs_loader)
411
435
  snapshot_fork_pool_stats(session) if @collect_stats
412
436
 
413
437
  worker_count = [@workers, files.size].min
@@ -540,6 +564,7 @@ module Rigor
540
564
  # per-file analysis runs on the coordinator, identical to the default sequential path.
541
565
  def analyze_files_sequentially_fallback(files, reason:)
542
566
  environment = build_runner_environment
567
+ snapshot_effect_annotation_carrier(environment.rbs_loader)
543
568
  diagnostics = files.flat_map { |path| @analyze_file.call(path, environment) }
544
569
  loader = environment.rbs_loader
545
570
  @snapshots.class_decl_paths = loader&.class_decl_paths || {}.freeze
@@ -3,10 +3,11 @@
3
3
  module Rigor
4
4
  module Analysis
5
5
  class Runner
6
- # Mutable per-run holder for the four end-of-pass snapshots that the analysis paths compute as a side
6
+ # Mutable per-run holder for the end-of-pass snapshots that the analysis paths compute as a side
7
7
  # effect and the rest of the run reads back: the RBS `class_decl_paths` / `signature_paths` tables
8
- # (consumed by {RunStats}), the synthesized-namespace name list, and the `conforms-to` scan results
9
- # (consumed by the diagnostic aggregator).
8
+ # (consumed by {RunStats}), the synthesized-namespace name list, the `conforms-to` scan results
9
+ # (consumed by the diagnostic aggregator), and the effect-annotation carrier (#441 — consumed by
10
+ # {EffectAnnotationResidualPass} on the runs that never store the environment).
10
11
  #
11
12
  # The snapshots are written by whichever analysis path ran ({PoolCoordinator} sequential / fork-pool /
12
13
  # fallback) and read by the {Runner} and {DiagnosticAggregator}. A shared mutable holder keeps the
@@ -16,7 +17,7 @@ module Rigor
16
17
  class RunSnapshots
17
18
  attr_accessor :class_decl_paths, :signature_paths,
18
19
  :synthesized_namespaces, :quarantined_signatures, :conformance_results,
19
- :env_build_failure
20
+ :env_build_failure, :effect_annotation_carrier
20
21
 
21
22
  # Constructor defaults match the {Runner} constructor: the pre-seed values `build_run_stats` /
22
23
  # `pre_file_diagnostics` read before the first analysis path runs are frozen empties. The
@@ -29,6 +30,7 @@ module Rigor
29
30
  @quarantined_signatures = [].freeze
30
31
  @conformance_results = [].freeze
31
32
  @env_build_failure = nil
33
+ @effect_annotation_carrier = [].freeze
32
34
  end
33
35
 
34
36
  # Per-`#run` reset. Mirrors the original `#run` body, which reset these to NON-frozen empties (distinct
@@ -40,6 +42,7 @@ module Rigor
40
42
  @quarantined_signatures = []
41
43
  @conformance_results = []
42
44
  @env_build_failure = nil
45
+ @effect_annotation_carrier = [].freeze
43
46
  end
44
47
  end
45
48
  end