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.
- checksums.yaml +4 -4
- data/README.md +5 -5
- data/data/effects/core.yml +18 -1
- data/data/effects/registry.yml +31 -3
- data/data/gem_overlay/activesupport/core_ext.rbs +33 -0
- data/docs/handbook/02-everyday-types.md +1 -1
- data/docs/handbook/04-tuples-and-shapes.md +1 -1
- data/docs/handbook/08-understanding-errors.md +1 -1
- data/docs/handbook/09-plugins.md +2 -2
- data/docs/handbook/10-sorbet.md +1 -1
- data/docs/handbook/README.md +2 -2
- data/docs/handbook/appendix-go.md +1 -1
- data/docs/handbook/appendix-java-csharp.md +2 -2
- data/docs/handbook/appendix-mypy.md +1 -1
- data/docs/handbook/appendix-protocols-and-structural-typing.md +2 -2
- data/docs/handbook/appendix-rust.md +1 -1
- data/docs/handbook/appendix-type-theory.md +2 -2
- data/docs/handbook/appendix-typescript.md +4 -4
- data/docs/manual/02-cli-reference.md +73 -22
- data/docs/manual/03-configuration.md +19 -2
- data/docs/manual/04-diagnostics.md +15 -2
- data/docs/manual/08-skills.md +1 -1
- data/docs/manual/11-ci.md +38 -1
- data/docs/manual/12-caching.md +40 -1
- data/docs/manual/15-type-protection-coverage.md +1 -1
- data/docs/manual/16-rbs-extended-annotations.md +15 -2
- data/docs/manual/18-removing-dead-code.md +13 -4
- data/docs/manual/19-effect-labels.md +678 -0
- data/docs/manual/README.md +7 -2
- data/docs/manual/ci-templates/README.md +9 -0
- data/docs/manual/plugins/rigor-actionmailer.md +4 -4
- data/docs/manual/plugins/rigor-activejob.md +3 -3
- data/docs/manual/plugins/rigor-activerecord.md +4 -4
- data/docs/manual/plugins/rigor-rails-i18n.md +5 -5
- data/docs/manual/plugins/rigor-rspec.md +6 -2
- data/lib/rigor/analysis/diagnostic.rb +17 -6
- data/lib/rigor/analysis/effects_cache_probe.rb +132 -0
- data/lib/rigor/analysis/reachability/graph.rb +30 -11
- data/lib/rigor/analysis/reachability/plugin_roots.rb +11 -8
- data/lib/rigor/analysis/reachability/scan.rb +20 -4
- data/lib/rigor/analysis/reachability/scan_cache.rb +130 -0
- data/lib/rigor/analysis/rule_catalog.rb +10 -3
- data/lib/rigor/analysis/run_cache_key.rb +12 -0
- data/lib/rigor/analysis/run_cache_probe.rb +69 -1
- data/lib/rigor/analysis/runner/declaration_position.rb +8 -24
- data/lib/rigor/analysis/runner/effect_annotation_residual_pass.rb +28 -28
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +15 -9
- data/lib/rigor/analysis/runner/pool_coordinator.rb +25 -0
- data/lib/rigor/analysis/runner/run_snapshots.rb +7 -4
- data/lib/rigor/analysis/runner.rb +163 -31
- data/lib/rigor/analysis/worker_session.rb +3 -1
- data/lib/rigor/cache/file_digest.rb +20 -2
- data/lib/rigor/cli/check_command.rb +67 -48
- data/lib/rigor/cli/coverage_command.rb +5 -6
- data/lib/rigor/cli/doc_links.rb +100 -0
- data/lib/rigor/cli/docs_command.rb +32 -2
- data/lib/rigor/cli/effects_command.rb +149 -10
- data/lib/rigor/cli/effects_diff_renderer.rb +88 -14
- data/lib/rigor/cli/effects_explain_renderer.rb +6 -3
- data/lib/rigor/cli/effects_renderer.rb +41 -3
- data/lib/rigor/cli/effects_report.rb +116 -6
- data/lib/rigor/cli/effects_snapshot_command.rb +75 -11
- data/lib/rigor/cli/unused_command.rb +65 -16
- data/lib/rigor/cli.rb +33 -9
- data/lib/rigor/configuration.rb +37 -25
- data/lib/rigor/configuration_error.rb +20 -0
- data/lib/rigor/effects/collector.rb +38 -1
- data/lib/rigor/effects/definition_lines.rb +100 -0
- data/lib/rigor/effects/entry_points.rb +47 -1
- data/lib/rigor/effects/envelope_check.rb +18 -1
- data/lib/rigor/effects/file_collection.rb +18 -4
- data/lib/rigor/effects/framework_units.rb +68 -13
- data/lib/rigor/effects/inline_anchor.rb +134 -0
- data/lib/rigor/effects/liskov_check.rb +17 -8
- data/lib/rigor/effects/plugin_facts.rb +62 -13
- data/lib/rigor/effects/propagator.rb +79 -19
- data/lib/rigor/effects/registry.rb +10 -3
- data/lib/rigor/effects/scanner.rb +21 -9
- data/lib/rigor/effects/signature_sources.rb +29 -2
- data/lib/rigor/effects/snapshot.rb +74 -26
- data/lib/rigor/effects/snapshot_diff.rb +26 -3
- data/lib/rigor/effects/taint_cause.rb +1 -0
- data/lib/rigor/effects/unit_scan.rb +87 -12
- data/lib/rigor/inference/synthetic_method_scanner.rb +7 -0
- data/lib/rigor/plugin/base.rb +4 -0
- data/lib/rigor/plugin/box.rb +18 -2
- data/lib/rigor/plugin/effect_ancestry.rb +80 -0
- data/lib/rigor/plugin/manifest.rb +34 -10
- data/lib/rigor/plugin/registry.rb +10 -3
- data/lib/rigor/rbs_extended/envelope_scanner.rb +9 -0
- data/lib/rigor/rbs_extended.rb +22 -2
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +17 -2
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +27 -0
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +56 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/effects.rb +75 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +13 -1
- data/skills/rigor-ci-setup/SKILL.md +2 -2
- data/skills/rigor-editor-setup/SKILL.md +2 -2
- data/skills/rigor-mcp-setup/SKILL.md +2 -2
- data/skills/rigor-monkeypatch-resolve/SKILL.md +1 -1
- data/skills/rigor-plugin-review/SKILL.md +3 -3
- 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(
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
29
|
-
#
|
|
30
|
-
#
|
|
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
|
|
55
|
-
#
|
|
56
|
-
|
|
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
|
-
@
|
|
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: @
|
|
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:
|
|
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. **
|
|
34
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
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
|
|
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
|
|
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
|
|
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 ||=
|
|
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
|
|
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,
|
|
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
|