rigortype 0.3.0 → 0.3.2
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 +10 -10
- data/data/builtins/ruby_core/array.yml +416 -392
- data/data/builtins/ruby_core/file.yml +42 -42
- data/data/builtins/ruby_core/hash.yml +302 -302
- data/data/builtins/ruby_core/io.yml +191 -191
- data/data/builtins/ruby_core/numeric.yml +321 -366
- data/data/builtins/ruby_core/proc.yml +124 -124
- data/data/builtins/ruby_core/range.yml +21 -21
- data/data/builtins/ruby_core/rational.yml +39 -39
- data/data/builtins/ruby_core/re.yml +65 -65
- data/data/builtins/ruby_core/set.yml +106 -106
- data/data/builtins/ruby_core/struct.yml +14 -14
- data/data/core_overlay/pathname.rbs +5 -0
- data/data/core_overlay/resolv.rbs +31 -0
- data/data/core_overlay/string_scanner.rbs +11 -5
- data/data/gem_overlay/activesupport/core_ext.rbs +10 -6
- data/data/vendored_gem_sigs/bundler/bundler.rbs +39 -27
- data/data/vendored_gem_sigs/cgi/cgi_extras.rbs +6 -0
- data/data/vendored_gem_sigs/prism/prism_supplement.rbs +7 -0
- data/data/vendored_gem_sigs/racc/racc.rbs +67 -0
- data/data/vendored_gem_sigs/rubygems/rubygems_extras.rbs +20 -9
- data/docs/handbook/01-getting-started.md +22 -34
- data/docs/handbook/06-classes.md +1 -1
- data/docs/handbook/07-rbs-and-extended.md +76 -101
- data/docs/handbook/08-understanding-errors.md +114 -247
- data/docs/handbook/09-plugins.md +54 -144
- data/docs/handbook/README.md +5 -3
- data/docs/handbook/appendix-liskov.md +4 -2
- data/docs/handbook/appendix-phpstan.md +14 -7
- data/docs/handbook/appendix-steep.md +4 -2
- data/docs/handbook/appendix-type-theory.md +3 -1
- data/docs/manual/02-cli-reference.md +103 -11
- data/docs/manual/03-configuration.md +16 -1
- data/docs/manual/04-diagnostics.md +36 -4
- data/docs/manual/06-baseline.md +35 -1
- data/docs/manual/08-skills.md +6 -1
- data/docs/manual/09-editor-integration.md +3 -2
- data/docs/manual/12-caching.md +17 -5
- data/docs/manual/15-type-protection-coverage.md +122 -1
- data/docs/manual/plugins/rigor-actioncable.md +32 -0
- data/docs/manual/plugins/rigor-devise.md +4 -2
- data/docs/manual/plugins/rigor-rbs-inline.md +22 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +1 -0
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +21 -2
- data/lib/rigor/analysis/check_rules.rb +79 -19
- data/lib/rigor/analysis/incremental_session.rb +156 -11
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +14 -0
- data/lib/rigor/analysis/run_cache_key.rb +27 -1
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +201 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +28 -10
- data/lib/rigor/analysis/runner/pool_coordinator.rb +7 -10
- data/lib/rigor/analysis/runner/project_pre_passes.rb +3 -1
- data/lib/rigor/analysis/runner.rb +80 -8
- data/lib/rigor/bleeding_edge.rb +132 -7
- data/lib/rigor/cache/descriptor.rb +6 -1
- data/lib/rigor/cache/engine_source.rb +162 -0
- data/lib/rigor/cache/incremental_snapshot.rb +87 -18
- data/lib/rigor/cache/rbs_cache_producer.rb +11 -1
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +38 -0
- data/lib/rigor/cache/store.rb +99 -24
- data/lib/rigor/cli/check_command.rb +61 -34
- data/lib/rigor/cli/check_invocation.rb +84 -0
- data/lib/rigor/cli/coverage_command.rb +15 -2
- data/lib/rigor/cli/coverage_mutation.rb +242 -11
- data/lib/rigor/cli/doctor_command.rb +6 -8
- data/lib/rigor/cli/fused_protection_renderer.rb +10 -0
- data/lib/rigor/cli/fused_protection_report.rb +12 -3
- data/lib/rigor/cli/mutation_fork_scan.rb +64 -0
- data/lib/rigor/cli/mutation_protection_renderer.rb +12 -0
- data/lib/rigor/cli/mutation_protection_report.rb +26 -5
- data/lib/rigor/cli/protection_renderer.rb +13 -0
- data/lib/rigor/cli/protection_report.rb +11 -3
- data/lib/rigor/cli/show_bleedingedge_command.rb +17 -4
- data/lib/rigor/cli/sig_gen_command.rb +5 -4
- data/lib/rigor/cli/skill_command.rb +21 -1
- data/lib/rigor/cli/skill_deep_probe.rb +172 -0
- data/lib/rigor/cli/skill_describe.rb +75 -9
- data/lib/rigor/configuration.rb +45 -6
- data/lib/rigor/environment/bundle_sig_discovery.rb +2 -1
- data/lib/rigor/environment/default_libraries.rb +5 -4
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +434 -75
- data/lib/rigor/environment.rb +38 -14
- data/lib/rigor/inference/expression_typer.rb +15 -0
- data/lib/rigor/inference/fork_map.rb +9 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +50 -2
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +35 -1
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +6 -1
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +16 -0
- data/lib/rigor/inference/method_dispatcher/regexp_folding.rb +64 -5
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +137 -9
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +3 -2
- data/lib/rigor/inference/method_dispatcher/uri_folding.rb +124 -1
- data/lib/rigor/inference/mutation_widening.rb +30 -25
- data/lib/rigor/inference/narrowing.rb +3 -1
- data/lib/rigor/inference/optimistic_origin.rb +58 -0
- data/lib/rigor/inference/protection_scanner.rb +18 -2
- data/lib/rigor/inference/receiver_alias.rb +57 -0
- data/lib/rigor/inference/scope_indexer.rb +78 -27
- data/lib/rigor/inference/statement_evaluator.rb +40 -2
- data/lib/rigor/language_server/buffer_resolution.rb +6 -3
- data/lib/rigor/language_server/buffer_table.rb +68 -6
- data/lib/rigor/language_server/diagnostic_publisher.rb +207 -4
- data/lib/rigor/language_server/incremental_sync.rb +159 -0
- data/lib/rigor/language_server/project_context.rb +45 -0
- data/lib/rigor/language_server/publish_batcher.rb +82 -0
- data/lib/rigor/language_server/server.rb +38 -9
- data/lib/rigor/language_server.rb +2 -0
- data/lib/rigor/plugin/base.rb +29 -2
- data/lib/rigor/plugin/source_rbs_synthesis_reporter.rb +9 -3
- data/lib/rigor/protection/closure_kill_oracle.rb +171 -0
- data/lib/rigor/protection/dependency_closure.rb +59 -0
- data/lib/rigor/protection/diagnostic_oracle.rb +12 -8
- data/lib/rigor/protection/discovery_seed.rb +116 -0
- data/lib/rigor/protection/kill_signature.rb +31 -0
- data/lib/rigor/protection/mutation_cache.rb +355 -0
- data/lib/rigor/protection/mutation_scanner.rb +55 -11
- data/lib/rigor/protection/mutator.rb +26 -4
- data/lib/rigor/runtime/jit.rb +63 -1
- data/lib/rigor/scope/discovery_index.rb +9 -0
- data/lib/rigor/scope.rb +52 -8
- data/lib/rigor/sig_gen/generator.rb +130 -31
- data/lib/rigor/sig_gen/layout_index.rb +6 -0
- data/lib/rigor/sig_gen/meta_class_shape.rb +84 -0
- data/lib/rigor/sig_gen/renderer.rb +20 -1
- data/lib/rigor/sig_gen/write_result.rb +6 -4
- data/lib/rigor/sig_gen/writer.rb +195 -47
- data/lib/rigor/sig_gen.rb +1 -0
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +51 -1
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +6 -6
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/result_shape.rb +146 -0
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema/schema_scanner.rb +177 -12
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +74 -7
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/contract_scanner.rb +257 -0
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation/params_shape.rb +69 -0
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +87 -11
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +34 -4
- data/sig/rigor/cache.rbs +6 -0
- data/sig/rigor/inference/void_origin.rbs +21 -0
- data/sig/rigor/plugin/base.rbs +4 -3
- data/sig/rigor/scope.rbs +8 -0
- metadata +23 -3
|
@@ -4,6 +4,8 @@ require "fileutils"
|
|
|
4
4
|
require "digest"
|
|
5
5
|
require "zlib"
|
|
6
6
|
|
|
7
|
+
require_relative "engine_source"
|
|
8
|
+
|
|
7
9
|
module Rigor
|
|
8
10
|
module Cache
|
|
9
11
|
# ADR-46 — disk persistence for the incremental analyzer's per-file state, so a `--incremental` session
|
|
@@ -14,8 +16,16 @@ module Rigor
|
|
|
14
16
|
# this snapshot is loaded UNCONDITIONALLY when the global fingerprint matches — the per-file digests
|
|
15
17
|
# *inside* it drive the incremental re-analysis decision; they do not gate the load. The fingerprint
|
|
16
18
|
# captures the inputs whose change requires a full rebuild — the resolved configuration, the RBS
|
|
17
|
-
# environment, the engine version
|
|
18
|
-
# gem / version change) drops the snapshot and
|
|
19
|
+
# environment, the engine version and (on a checkout) the engine's own source — but NOT the analyzed
|
|
20
|
+
# source contents. A fingerprint mismatch (config / gem / version / engine change) drops the snapshot and
|
|
21
|
+
# forces a full re-analysis, the conservative direction.
|
|
22
|
+
#
|
|
23
|
+
# An engine change drops the WHOLE snapshot rather than part of it, which is a soundness point before it
|
|
24
|
+
# is a simplicity one: every section here except `digests` is a value the analyzer computed, so a changed
|
|
25
|
+
# engine can move any of it — including the dependency edges, where a new engine recording an edge the old
|
|
26
|
+
# one missed would let a recheck skip the very file that needed re-analysing. Retaining the one
|
|
27
|
+
# engine-independent section would not pay either: `digests` is 2.5% of a 2.5 MB snapshot of this repo,
|
|
28
|
+
# and re-deriving it is a file-digest walk costing ~0.2% of the full run it would be saving.
|
|
19
29
|
#
|
|
20
30
|
# Every operation is fault-tolerant: a missing, unreadable, schema-mismatched, fingerprint-mismatched, or
|
|
21
31
|
# corrupt snapshot loads as nil (→ a cold full run), and a write failure is swallowed (→ the next run is
|
|
@@ -38,8 +48,11 @@ module Rigor
|
|
|
38
48
|
# bundle (the per-def parameter-shape / visibility / ancestry surface the declaration-stability gate
|
|
39
49
|
# compares) and WD2 adds `return_summaries` (per-def observed-key return descriptors + mutation-effect
|
|
40
50
|
# sets the behavioural-stability gate compares); a pre-10 blob mismatches the SCHEMA gate and loads as
|
|
41
|
-
# nil (a clean cold rebuild — no migration).
|
|
42
|
-
|
|
51
|
+
# nil (a clean cold rebuild — no migration). 11: ADR-67 WD6c lift adds `param_table` (the inferred-param
|
|
52
|
+
# seed table the run's diagnostics were computed under, diffed on the next recheck to invalidate a
|
|
53
|
+
# callee whose seeds moved because a caller changed); a pre-11 blob mismatches the SCHEMA gate and
|
|
54
|
+
# loads as nil (a clean cold rebuild — no migration).
|
|
55
|
+
SCHEMA = 11
|
|
43
56
|
|
|
44
57
|
# The persisted per-file state.
|
|
45
58
|
# `cache` maps an analyzed file to its diagnostics.
|
|
@@ -68,20 +81,35 @@ module Rigor
|
|
|
68
81
|
# (Marshal-clean type tuples), their `describe(:short)` return descriptors, and the content-mutated
|
|
69
82
|
# parameter positions. A recheck re-evaluates a declaration-stable changed callee at these keys and,
|
|
70
83
|
# when every return + the effects are unchanged, skips its symbol dependents.
|
|
84
|
+
# ADR-67 WD6c lift:
|
|
85
|
+
# `param_table` is the `parameter_inference:` seed table (`[class, method, kind] => {param => Type}`)
|
|
86
|
+
# the run that wrote the snapshot seeded its analysis from — `{}` when the gate is off. A recheck
|
|
87
|
+
# recomputes the table fresh (the pre-pass is whole-project by design) and diffs it against this copy;
|
|
88
|
+
# a changed entry invalidates the callee's file and its symbol dependents. The types are Marshal-clean
|
|
89
|
+
# by the session's per-entry filter; a dropped entry re-checks its callee, the conservative direction.
|
|
71
90
|
Payload = Data.define(:cache, :sources, :digests, :analyzed,
|
|
72
91
|
:symbol_sources, :ancestry_sources, :symbol_fingerprints,
|
|
73
92
|
:missing, :class_decls, :seed_bundles, :plugin_fact_digest,
|
|
74
|
-
:return_summaries)
|
|
93
|
+
:return_summaries, :param_table)
|
|
75
94
|
|
|
76
95
|
# The global fingerprint that gates a snapshot load: a digest of the inputs whose change requires a full
|
|
77
|
-
# rebuild — the engine version + schema, the
|
|
78
|
-
# arguments, e.g. `["lib"]`, NOT the expanded
|
|
79
|
-
#
|
|
80
|
-
# the
|
|
81
|
-
#
|
|
82
|
-
#
|
|
83
|
-
# under-capture (it would surface as an
|
|
84
|
-
# caller falls back to a non-persisted run.
|
|
96
|
+
# rebuild — the engine version + schema, the engine's own SOURCE when the version does not pin it, the
|
|
97
|
+
# resolved configuration, the analysis **roots** (the path arguments, e.g. `["lib"]`, NOT the expanded
|
|
98
|
+
# file list — so a snapshot is keyed to an invocation's roots but adding / removing a file under them is
|
|
99
|
+
# handled incrementally by the session, not a full rebuild), the resolved gem set (`Gemfile.lock` /
|
|
100
|
+
# `rbs_collection`), and the project's own RBS (`signature_paths` file contents). Built WITHOUT
|
|
101
|
+
# constructing the RBS environment so the warm path can gate the load cheaply, before the costly env
|
|
102
|
+
# build. The `--verify-incremental` gate is the safety net for any under-capture (it would surface as an
|
|
103
|
+
# incremental-vs-full mismatch). Returns nil on any error → the caller falls back to a non-persisted run.
|
|
104
|
+
#
|
|
105
|
+
# Issue #285, wired here by #289 — every value in this snapshot is something the ANALYZER computed, so
|
|
106
|
+
# `Rigor::VERSION` alone is not enough to identify what produced it. A version pins the engine's bytes
|
|
107
|
+
# for a RubyGems install and for nothing else, so on a checkout a warm recheck served diagnostics a
|
|
108
|
+
# pre-edit analyzer had computed: editing `lib/rigor/inference/*.rb` moved no ANALYZED file, the changed
|
|
109
|
+
# set came back empty, and 357 unchanged files replayed their old answers.
|
|
110
|
+
# {EngineSource.process_identity} closes it, and answers nil for a version-pinned tree — which adds no
|
|
111
|
+
# part, so a released gem's fingerprint is byte-identical to the pre-#289 one and its warm snapshots
|
|
112
|
+
# survive the upgrade untouched.
|
|
85
113
|
def self.fingerprint(configuration:, roots:)
|
|
86
114
|
parts = [
|
|
87
115
|
"engine:#{Rigor::VERSION}:#{SCHEMA}",
|
|
@@ -91,8 +119,15 @@ module Rigor
|
|
|
91
119
|
"rbs_collection:#{digest_file_if_present('rbs_collection.lock.yaml')}",
|
|
92
120
|
"sig:#{digest_signature_paths(configuration.signature_paths)}"
|
|
93
121
|
]
|
|
122
|
+
identity = EngineSource.process_identity
|
|
123
|
+
parts << "engine-source:#{identity}" if identity
|
|
94
124
|
Digest::SHA256.hexdigest(parts.join("\x00"))
|
|
95
125
|
rescue StandardError
|
|
126
|
+
# {EngineSource::Unavailable} lands here too, and nil is the answer it requires rather than one it
|
|
127
|
+
# merely tolerates: an engine we cannot identify must DISABLE the snapshot, never fall back to the
|
|
128
|
+
# version-only key that is the blind spot above. Nil does disable it on both sides —
|
|
129
|
+
# {Analysis::IncrementalSession} guards the load AND the save on the fingerprint, so nothing stale is
|
|
130
|
+
# read and no nil-keyed blob is written for the next equally-unidentifiable run to match against.
|
|
96
131
|
nil
|
|
97
132
|
end
|
|
98
133
|
|
|
@@ -124,9 +159,42 @@ module Rigor
|
|
|
124
159
|
# The stored {Payload}, or nil when absent / unreadable / schema or fingerprint mismatch / corrupt.
|
|
125
160
|
# Never raises.
|
|
126
161
|
def load(fingerprint:)
|
|
162
|
+
data = read_data
|
|
163
|
+
return nil unless data && data[:fingerprint] == fingerprint
|
|
164
|
+
|
|
165
|
+
payload_from(data)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Issue #134 slice 2 — the same load against SEVERAL acceptable fingerprints, reading the blob once.
|
|
169
|
+
# A reader that did not itself write the snapshot cannot know which analysis ROOTS it was written under
|
|
170
|
+
# (`rigor check --incremental lib` and a bare `rigor check --incremental` produce different fingerprints
|
|
171
|
+
# for the same project), and `#load` would have to re-inflate + re-unmarshal the whole blob per candidate.
|
|
172
|
+
# The fingerprint that matched is returned alongside the payload so the caller can mix it into ITS own
|
|
173
|
+
# cache key — the snapshot's identity is exactly what "these dependency edges came from that world" means.
|
|
174
|
+
#
|
|
175
|
+
# @param fingerprints [Array<String>] candidates, most-specific first.
|
|
176
|
+
# @return [Array(String, Payload), nil] `[matched fingerprint, payload]`, or nil on any miss.
|
|
177
|
+
def load_any(fingerprints:)
|
|
178
|
+
data = read_data
|
|
179
|
+
return nil if data.nil?
|
|
180
|
+
|
|
181
|
+
matched = Array(fingerprints).compact.find { |candidate| data[:fingerprint] == candidate }
|
|
182
|
+
return nil if matched.nil?
|
|
183
|
+
|
|
184
|
+
[matched, payload_from(data)]
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# The raw stored Hash when it is present, readable, and schema-current; nil otherwise. Never raises —
|
|
188
|
+
# a missing, corrupt, or stale-schema snapshot is a cold run, not an error (the ADR-45 invariant).
|
|
189
|
+
def read_data
|
|
127
190
|
data = Marshal.load(Zlib::Inflate.inflate(File.binread(@path))) # rubocop:disable Security/MarshalLoad
|
|
128
|
-
|
|
191
|
+
data.is_a?(Hash) && data[:schema] == SCHEMA ? data : nil
|
|
192
|
+
rescue StandardError
|
|
193
|
+
nil
|
|
194
|
+
end
|
|
195
|
+
private :read_data
|
|
129
196
|
|
|
197
|
+
def payload_from(data)
|
|
130
198
|
Payload.new(
|
|
131
199
|
cache: data[:cache], sources: data[:sources],
|
|
132
200
|
digests: data[:digests], analyzed: data[:analyzed],
|
|
@@ -137,11 +205,11 @@ module Rigor
|
|
|
137
205
|
class_decls: data[:class_decls] || {},
|
|
138
206
|
seed_bundles: data[:seed_bundles] || {},
|
|
139
207
|
plugin_fact_digest: data[:plugin_fact_digest],
|
|
140
|
-
return_summaries: data[:return_summaries] || {}
|
|
208
|
+
return_summaries: data[:return_summaries] || {},
|
|
209
|
+
param_table: data[:param_table] || {}
|
|
141
210
|
)
|
|
142
|
-
rescue StandardError
|
|
143
|
-
nil
|
|
144
211
|
end
|
|
212
|
+
private :payload_from
|
|
145
213
|
|
|
146
214
|
# Persist `payload` under `fingerprint`. Writes via a temp file + atomic rename so a concurrent reader
|
|
147
215
|
# never sees a half-written snapshot. Returns true on success, false on any failure (never raises).
|
|
@@ -158,7 +226,8 @@ module Rigor
|
|
|
158
226
|
class_decls: payload.class_decls,
|
|
159
227
|
seed_bundles: payload.seed_bundles,
|
|
160
228
|
plugin_fact_digest: payload.plugin_fact_digest,
|
|
161
|
-
return_summaries: payload.return_summaries
|
|
229
|
+
return_summaries: payload.return_summaries,
|
|
230
|
+
param_table: payload.param_table
|
|
162
231
|
)
|
|
163
232
|
blob = Zlib::Deflate.deflate(raw)
|
|
164
233
|
tmp = "#{@path}.#{Process.pid}.tmp"
|
|
@@ -15,12 +15,22 @@ module Rigor
|
|
|
15
15
|
# `self::PRODUCER_ID` resolves the constant on the concrete subclass, and `compute(loader)` dispatches to
|
|
16
16
|
# its private class method. See the `_CacheProducer` RBS interface for the structural contract.
|
|
17
17
|
class RbsCacheProducer
|
|
18
|
+
# Every RBS producer is whole-project and content-keyed: one entry is live and any older generation is
|
|
19
|
+
# unreachable, so `Cache::Store#evict!` keeps a small number of them (one spare for the still-warm
|
|
20
|
+
# previous signature state — the RBS environment blob alone runs to ~1.8 MB, ADR-54 WD3). Declared on
|
|
21
|
+
# the base, so a producer added by subclassing inherits a cap instead of being silently uncapped; a
|
|
22
|
+
# subclass with different economics overrides this method.
|
|
23
|
+
def self.generation_cap
|
|
24
|
+
2
|
|
25
|
+
end
|
|
26
|
+
|
|
18
27
|
def self.fetch(loader:, store:)
|
|
19
28
|
# ADR-54 WD4 — the descriptor is identical for every producer consulting the same loader (same sig
|
|
20
29
|
# files, same libraries), so the loader memoises one build per process instead of re-digesting every
|
|
21
30
|
# .rbs file once per producer.
|
|
22
31
|
descriptor = loader.rbs_cache_descriptor
|
|
23
|
-
store.fetch_or_compute(producer_id: self::PRODUCER_ID, params: {}, descriptor: descriptor
|
|
32
|
+
store.fetch_or_compute(producer_id: self::PRODUCER_ID, params: {}, descriptor: descriptor,
|
|
33
|
+
generation_cap: generation_cap) do
|
|
24
34
|
compute(loader)
|
|
25
35
|
end
|
|
26
36
|
end
|
|
@@ -17,6 +17,20 @@ require "rbs"
|
|
|
17
17
|
#
|
|
18
18
|
# Idempotent: the guard checks `method_defined?(:_dump)` so requiring this file twice (or against an upstream
|
|
19
19
|
# rbs that adds Marshal hooks itself) is a no-op.
|
|
20
|
+
#
|
|
21
|
+
# `RBS::TypeName` / `RBS::Namespace` carry a second, subtler Marshal hazard, introduced by rbs 4.1's
|
|
22
|
+
# flyweight interning (ruby/rbs#2957): both memoise their `#hash` into an `@hash` ivar, and that value is
|
|
23
|
+
# derived from `Array#hash` / `Symbol#hash`, which Ruby seeds PER PROCESS. Marshal round-trips the ivar
|
|
24
|
+
# verbatim, so a cached `TypeName` loaded in a later process answers `hash` with the *writing* process's
|
|
25
|
+
# value while a freshly parsed `TypeName.parse("::String")` answers with this process's. The two are still
|
|
26
|
+
# `eql?`, so nothing raises — every Hash keyed by a TypeName simply misses. `RBS::Environment#class_decls`
|
|
27
|
+
# is exactly such a Hash, so a warm cache silently reported every core class as unknown.
|
|
28
|
+
#
|
|
29
|
+
# `_dump` / `_load` on both classes fixes it at the representation: the name is dumped as its source string
|
|
30
|
+
# and reconstructed through `.parse`, which routes back through the flyweight interner. That drops the stale
|
|
31
|
+
# `@hash` (it is re-memoised on demand, under this process's seed), and restores the flyweight identity the
|
|
32
|
+
# interner exists to provide — a cached env now shares one object per distinct name rather than one per
|
|
33
|
+
# reference. Both classes are value objects fully described by `to_s`, so the round-trip is lossless.
|
|
20
34
|
module RBS
|
|
21
35
|
class Location
|
|
22
36
|
unless method_defined?(:_dump)
|
|
@@ -29,4 +43,28 @@ module RBS
|
|
|
29
43
|
end
|
|
30
44
|
end
|
|
31
45
|
end
|
|
46
|
+
|
|
47
|
+
class Namespace
|
|
48
|
+
unless method_defined?(:_dump)
|
|
49
|
+
def _dump(_)
|
|
50
|
+
to_s
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self._load(string)
|
|
54
|
+
parse(string)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
class TypeName
|
|
60
|
+
unless method_defined?(:_dump)
|
|
61
|
+
def _dump(_)
|
|
62
|
+
to_s
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self._load(string)
|
|
66
|
+
parse(string)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
32
70
|
end
|
data/lib/rigor/cache/store.rb
CHANGED
|
@@ -37,18 +37,10 @@ module Rigor
|
|
|
37
37
|
# blob whose class layout still happens to unmarshal.
|
|
38
38
|
PAYLOAD_ABI_VERSION = Rigor::VERSION
|
|
39
39
|
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
|
|
44
|
-
GENERATION_CAP_BY_PRODUCER = {
|
|
45
|
-
"analysis.run-diagnostics" => 16,
|
|
46
|
-
"rbs.class_ancestor_table" => 2,
|
|
47
|
-
"rbs.class_type_param_names" => 2,
|
|
48
|
-
"rbs.constant_type_table" => 2,
|
|
49
|
-
"rbs.environment" => 2,
|
|
50
|
-
"rbs.known_class_names" => 2
|
|
51
|
-
}.freeze
|
|
40
|
+
# The `generation_cap:` value declaring that a producer keeps MANY live entries under one id at once
|
|
41
|
+
# (per-file and per-plugin producers): a generation count is not a staleness proxy there, so the
|
|
42
|
+
# compaction pass in {#evict!} leaves the producer alone and only the size-based LRU pass can touch it.
|
|
43
|
+
UNBOUNDED_GENERATIONS = :unbounded
|
|
52
44
|
|
|
53
45
|
STALE_TEMP_FILE_AGE_SECONDS = 60 * 60
|
|
54
46
|
|
|
@@ -76,6 +68,14 @@ module Rigor
|
|
|
76
68
|
@misses = 0
|
|
77
69
|
@writes = 0
|
|
78
70
|
@by_producer = Hash.new { |h, k| h[k] = { hits: 0, misses: 0, writes: 0 } }
|
|
71
|
+
# `producer_id => generation cap`, populated from the `generation_cap:` every fetch call declares (see
|
|
72
|
+
# {#declare_generation_cap}). This is the bridge between the id STRINGS the fetch API takes and the
|
|
73
|
+
# directory names {#evict!} walks: a producer id the Store never saw declared stays uncapped, which
|
|
74
|
+
# errs toward under-evicting. Per-instance rather than process-global on purpose — a global mutable
|
|
75
|
+
# registry would be a `Ractor` shareability hazard for pool mode, and the Store instance that wrote a
|
|
76
|
+
# generation is the one whose `evict!` compacts it (`CLI::CheckCommand` calls
|
|
77
|
+
# `runner.cache_store&.evict!`).
|
|
78
|
+
@generation_caps = {}
|
|
79
79
|
# Process-level in-memory layer keyed by `(producer_id, cache_key)`. Avoids the disk read +
|
|
80
80
|
# `Marshal.load` cost (the dominant share of repeated cache-hit calls per stackprof) when many
|
|
81
81
|
# short-lived `Analysis::Runner` instances share one `Store` — the spec process, the LSP daemon's
|
|
@@ -161,6 +161,12 @@ module Rigor
|
|
|
161
161
|
private_class_method :collect_producers
|
|
162
162
|
|
|
163
163
|
# @param producer_id [String] stable cache namespace; only `[a-z][a-z0-9._-]*` is accepted.
|
|
164
|
+
# @param generation_cap [Integer, Symbol] how many generations of this producer survive a compaction
|
|
165
|
+
# pass — a positive `Integer` for a whole-project producer (one live entry, older ones unreachable),
|
|
166
|
+
# or {UNBOUNDED_GENERATIONS} for a producer with many simultaneously-live entries. REQUIRED, and
|
|
167
|
+
# sourced from the producer's own declaration (`RbsCacheProducer.generation_cap`,
|
|
168
|
+
# `RunCacheKey::GENERATION_CAP`, `Plugin::Base.producer generation_cap:`) rather than invented at the
|
|
169
|
+
# call site. See {#evict!}.
|
|
164
170
|
# @param params [Hash] producer inputs; mixed into the cache key via {Descriptor#cache_key_for}.
|
|
165
171
|
# @param descriptor [Rigor::Cache::Descriptor] the invalidation descriptor for the value being cached.
|
|
166
172
|
# @param serialize [#call, nil] optional callable that turns the producer's return value into a binary
|
|
@@ -175,9 +181,10 @@ module Rigor
|
|
|
175
181
|
# path.
|
|
176
182
|
# @yieldreturn the value to cache.
|
|
177
183
|
# @return the cached value (loaded from disk on hit; produced by the block on miss).
|
|
178
|
-
def fetch_or_compute(producer_id:, params:, descriptor:,
|
|
184
|
+
def fetch_or_compute(producer_id:, params:, descriptor:, generation_cap:,
|
|
179
185
|
serialize: nil, deserialize: nil, &block)
|
|
180
186
|
validate_producer_id!(producer_id)
|
|
187
|
+
declare_generation_cap(producer_id, generation_cap)
|
|
181
188
|
disk = ensure_schema_version!
|
|
182
189
|
|
|
183
190
|
key = descriptor.cache_key_for(producer_id: producer_id, params: params)
|
|
@@ -217,8 +224,12 @@ module Rigor
|
|
|
217
224
|
#
|
|
218
225
|
# The block MUST return `[value, dependency_descriptor]`. Disk reads are not in-process-memoised —
|
|
219
226
|
# validation always re-checks the filesystem — but a single run only looks up once.
|
|
220
|
-
|
|
227
|
+
#
|
|
228
|
+
# `generation_cap:` carries the same producer-declared compaction budget as {#fetch_or_compute}.
|
|
229
|
+
def fetch_or_validate(producer_id:, key_descriptor:, generation_cap:, params: {},
|
|
230
|
+
serialize: nil, deserialize: nil)
|
|
221
231
|
validate_producer_id!(producer_id)
|
|
232
|
+
declare_generation_cap(producer_id, generation_cap)
|
|
222
233
|
disk = ensure_schema_version!
|
|
223
234
|
|
|
224
235
|
key = key_descriptor.cache_key_for(producer_id: producer_id, params: params)
|
|
@@ -243,6 +254,9 @@ module Rigor
|
|
|
243
254
|
# to serve a run's diagnostics WITHOUT loading the inference engine — it never runs a producer block, so
|
|
244
255
|
# there is nothing to write. Records a hit (for `--cache-stats` parity) but never a miss (a probe miss
|
|
245
256
|
# hands off to the full path, which records its own).
|
|
257
|
+
#
|
|
258
|
+
# Takes no `generation_cap:`: a pure read creates no generation, so a run that only ever peeks has
|
|
259
|
+
# nothing to compact. The write path for the same producer declares the cap.
|
|
246
260
|
def peek_validated(producer_id:, key_descriptor:, params: {}, deserialize: nil)
|
|
247
261
|
validate_producer_id!(producer_id)
|
|
248
262
|
return nil unless ensure_schema_version!
|
|
@@ -256,8 +270,15 @@ module Rigor
|
|
|
256
270
|
validated[0]
|
|
257
271
|
end
|
|
258
272
|
|
|
259
|
-
# ADR-6 § "Eviction" — compaction pass over the on-disk cache. No-op when the store is read-only.
|
|
260
|
-
#
|
|
273
|
+
# ADR-6 § "Eviction" — compaction pass over the on-disk cache. No-op when the store is read-only.
|
|
274
|
+
#
|
|
275
|
+
# The generation cap of pass 2 comes from what the producers themselves declared through this Store's
|
|
276
|
+
# fetch calls (`generation_cap:`), NOT from a maintained table of producer ids: a producer id this
|
|
277
|
+
# Store never saw is left alone. That is deliberately the safe direction — a producer that was not
|
|
278
|
+
# consulted this run also wrote no new generation, so the only entries this can skip are ones that were
|
|
279
|
+
# already sitting there.
|
|
280
|
+
#
|
|
281
|
+
# Stale temp file cleanup and the generation cap run regardless of `max_bytes:` — they reclaim
|
|
261
282
|
# provably-dead bytes (leaked temp files, unreachable content-keyed generations) rather than enforcing a
|
|
262
283
|
# size budget, so an explicitly unbounded store (`max_bytes: nil`) still benefits from them. The
|
|
263
284
|
# size-based LRU pass below stays gated on `max_bytes:` being configured: it walks all remaining
|
|
@@ -279,7 +300,7 @@ module Rigor
|
|
|
279
300
|
entries.each do |entry|
|
|
280
301
|
break if total <= @max_bytes
|
|
281
302
|
|
|
282
|
-
total -= entry[:bytes] if
|
|
303
|
+
total -= entry[:bytes] if unlink_entry_and_shard?(entry[:path])
|
|
283
304
|
end
|
|
284
305
|
nil
|
|
285
306
|
rescue StandardError
|
|
@@ -320,6 +341,37 @@ module Rigor
|
|
|
320
341
|
"producer_id must match #{VALID_PRODUCER_ID.inspect}, got #{producer_id.inspect}"
|
|
321
342
|
end
|
|
322
343
|
|
|
344
|
+
# Records the producer's declared compaction budget for {#evict!}. Every fetch call carries it, so a
|
|
345
|
+
# producer cannot reach disk without stating one — the failure mode the previous hardcoded id table
|
|
346
|
+
# had (a new whole-project producer silently uncapped) is not expressible.
|
|
347
|
+
#
|
|
348
|
+
# Two DIFFERENT caps for one producer id in one process is a producer bug, not a policy: whichever the
|
|
349
|
+
# compaction pass picked would be arbitrary. It raises, consistent with the serializer-contract
|
|
350
|
+
# violations {#try_write_entry} deliberately lets through.
|
|
351
|
+
def declare_generation_cap(producer_id, generation_cap)
|
|
352
|
+
validate_generation_cap!(producer_id, generation_cap)
|
|
353
|
+
@monitor.synchronize do
|
|
354
|
+
previous = @generation_caps[producer_id]
|
|
355
|
+
if !previous.nil? && previous != generation_cap
|
|
356
|
+
raise ArgumentError,
|
|
357
|
+
"producer #{producer_id.inspect} declared generation_cap #{generation_cap.inspect} after " \
|
|
358
|
+
"#{previous.inspect}; one producer id must declare one cap"
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
@generation_caps[producer_id] = generation_cap
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def validate_generation_cap!(producer_id, generation_cap)
|
|
366
|
+
return if generation_cap == UNBOUNDED_GENERATIONS
|
|
367
|
+
return if generation_cap.is_a?(Integer) && generation_cap.positive?
|
|
368
|
+
|
|
369
|
+
raise ArgumentError,
|
|
370
|
+
"producer #{producer_id.inspect} must declare generation_cap as a positive Integer (a " \
|
|
371
|
+
"whole-project producer: how many generations survive compaction) or " \
|
|
372
|
+
"#{UNBOUNDED_GENERATIONS.inspect} (many entries live at once), got #{generation_cap.inspect}"
|
|
373
|
+
end
|
|
374
|
+
|
|
323
375
|
def entry_path(producer_id, key)
|
|
324
376
|
File.join(@root, producer_id, key[0, 2], "#{key[2..]}.entry")
|
|
325
377
|
end
|
|
@@ -541,7 +593,7 @@ module Rigor
|
|
|
541
593
|
next unless File.file?(path)
|
|
542
594
|
next if File.mtime(path) > cutoff
|
|
543
595
|
|
|
544
|
-
|
|
596
|
+
unlink_entry_and_shard?(path)
|
|
545
597
|
rescue StandardError
|
|
546
598
|
next
|
|
547
599
|
end
|
|
@@ -550,16 +602,16 @@ module Rigor
|
|
|
550
602
|
end
|
|
551
603
|
|
|
552
604
|
def evict_excess_generations(entries)
|
|
605
|
+
caps = @monitor.synchronize { @generation_caps.dup }
|
|
553
606
|
removed = {}
|
|
554
607
|
entries.group_by { |entry| entry[:producer] }.each do |producer, producer_entries|
|
|
555
|
-
cap =
|
|
608
|
+
cap = caps[producer]
|
|
609
|
+
cap = nil if cap == UNBOUNDED_GENERATIONS
|
|
556
610
|
next if cap.nil? || producer_entries.size <= cap
|
|
557
611
|
|
|
558
|
-
producer_entries.sort_by { |entry| [entry[:mtime], entry[:path]] }
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
removed[entry[:path]] = true if unlink_entry(entry[:path])
|
|
562
|
-
end
|
|
612
|
+
excess = producer_entries.sort_by { |entry| [entry[:mtime], entry[:path]] }
|
|
613
|
+
.first(producer_entries.size - cap)
|
|
614
|
+
excess.each { |entry| removed[entry[:path]] = true if unlink_entry_and_shard?(entry[:path]) }
|
|
563
615
|
end
|
|
564
616
|
return entries if removed.empty?
|
|
565
617
|
|
|
@@ -573,6 +625,29 @@ module Rigor
|
|
|
573
625
|
false
|
|
574
626
|
end
|
|
575
627
|
|
|
628
|
+
# {#unlink_entry} plus a best-effort {#rmdir_if_empty} of the shard directory the unlinked file left
|
|
629
|
+
# behind. Shared by every eviction/sweep pass so an emptied shard is cleaned up wherever an entry (or
|
|
630
|
+
# stale temp file) is removed, per the issue #216 fossil fix.
|
|
631
|
+
def unlink_entry_and_shard?(path)
|
|
632
|
+
return false unless unlink_entry(path)
|
|
633
|
+
|
|
634
|
+
rmdir_if_empty(File.dirname(path))
|
|
635
|
+
true
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
# Removes `dir` — a shard directory (`entry_path`'s `key[0, 2]` component) — when the unlink that
|
|
639
|
+
# just preceded this call was the one to empty it. Best-effort and rescued the same way
|
|
640
|
+
# {#unlink_entry} is: a concurrent writer's `FileUtils.mkdir_p` recreating the shard between the
|
|
641
|
+
# unlink and this call (`Errno::ENOTEMPTY`) or another pass already having removed it
|
|
642
|
+
# (`Errno::ENOENT`) are both benign outcomes, never a reason to break the eviction/sweep pass. This
|
|
643
|
+
# is purely cosmetic (inode reclaim) — it never decides which ENTRIES are evicted, only tidies the
|
|
644
|
+
# directory left behind once they are gone.
|
|
645
|
+
def rmdir_if_empty(dir)
|
|
646
|
+
Dir.rmdir(dir)
|
|
647
|
+
rescue StandardError
|
|
648
|
+
nil
|
|
649
|
+
end
|
|
650
|
+
|
|
576
651
|
# Returns an array of `{ path:, producer:, mtime:, bytes: }` hashes for every `.entry` file under the
|
|
577
652
|
# cache root, skipping unreadable entries.
|
|
578
653
|
def collect_entry_stats
|
|
@@ -48,9 +48,6 @@ module Rigor
|
|
|
48
48
|
|
|
49
49
|
configuration = load_check_configuration(options)
|
|
50
50
|
configuration = apply_bleeding_edge_override(configuration, options)
|
|
51
|
-
conflict = parameter_inference_incremental_conflict(configuration, options)
|
|
52
|
-
return conflict unless conflict.nil?
|
|
53
|
-
|
|
54
51
|
config_warnings = warn_unresolved_config(configuration)
|
|
55
52
|
cache_root = configuration.cache_path
|
|
56
53
|
handle_clear_cache(cache_root) if options.fetch(:clear_cache)
|
|
@@ -62,14 +59,15 @@ module Rigor
|
|
|
62
59
|
return finalize_cache_hit(probed, configuration, options, config_warnings) unless probed.nil?
|
|
63
60
|
|
|
64
61
|
load_check_dependencies
|
|
65
|
-
special = dispatch_special_check_mode(configuration, options, cache_root)
|
|
62
|
+
special = dispatch_special_check_mode(configuration, options, cache_root, buffer)
|
|
66
63
|
return special unless special.nil?
|
|
67
64
|
|
|
68
|
-
|
|
65
|
+
invocation = invoke_check(
|
|
69
66
|
configuration: configuration, options: options,
|
|
70
67
|
buffer: buffer, cache_root: cache_root
|
|
71
68
|
)
|
|
72
|
-
|
|
69
|
+
runner = invocation.runner
|
|
70
|
+
raw_result = invocation.result
|
|
73
71
|
result = apply_baseline_filter(raw_result, configuration, options)
|
|
74
72
|
|
|
75
73
|
coverage = compute_coverage(runner, configuration, options)
|
|
@@ -142,30 +140,24 @@ module Rigor
|
|
|
142
140
|
exit_code
|
|
143
141
|
end
|
|
144
142
|
|
|
145
|
-
# ADR-67 WD6c — `parameter_inference:` and the ADR-46 incremental modes are mutually exclusive in slice 1.
|
|
146
|
-
# The parameter table introduces cross-file edges (a caller's argument types drive a callee's body
|
|
147
|
-
# diagnostics) that the per-file dependency recorder does not yet carry, so a cached file whose parameter
|
|
148
|
-
# seeds changed would serve a stale result. Refuse with a message naming the ADR-46 gap rather than
|
|
149
|
-
# silently producing unsound incremental diagnostics; the edge wiring is the named follow-up. Returns the
|
|
150
|
-
# usage exit code on a conflict, or nil to proceed. `--verify-incremental` (the acceptance gate that runs
|
|
151
|
-
# incremental analysis) is caught here too.
|
|
152
|
-
def parameter_inference_incremental_conflict(configuration, options)
|
|
153
|
-
return nil unless configuration.parameter_inference
|
|
154
|
-
return nil unless options.fetch(:incremental) || options.fetch(:verify_incremental)
|
|
155
|
-
|
|
156
|
-
flag = options.fetch(:incremental) ? "--incremental" : "--verify-incremental"
|
|
157
|
-
@err.puts("rigor: parameter_inference: cannot combine with #{flag} — the call-site parameter table " \
|
|
158
|
-
"introduces cross-file caller→callee edges the ADR-46 incremental dependency graph does not " \
|
|
159
|
-
"yet record, so a cached file could serve a stale diagnostic. Run a full check (drop " \
|
|
160
|
-
"#{flag}), or disable parameter_inference: for this run.")
|
|
161
|
-
CLI::EXIT_USAGE
|
|
162
|
-
end
|
|
163
|
-
|
|
164
143
|
# ADR-46 — the two incremental-analysis check modes both fully handle the run and return an exit code (so `run`
|
|
165
144
|
# short-circuits); returns nil for an ordinary check.
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
145
|
+
# A nil return means "not a special mode" — `run` continues with the ordinary check. Editor mode option B
|
|
146
|
+
# also returns nil when it declines (no reusable snapshot), so the run falls back to single-file scope.
|
|
147
|
+
def dispatch_special_check_mode(configuration, options, cache_root, buffer)
|
|
148
|
+
if options.fetch(:verify_incremental)
|
|
149
|
+
# The gate compares an incremental recheck against a full-run oracle, and a buffer makes the two
|
|
150
|
+
# disagree by construction (the oracle reads the file on disk). Refusing beats the silent wrong
|
|
151
|
+
# answer both incremental modes gave a buffer before #146.
|
|
152
|
+
if buffer
|
|
153
|
+
@err.puts("rigor: --verify-incremental cannot run against an editor buffer " \
|
|
154
|
+
"(--tmp-file / --instead-of); it compares against a full analysis of the files on disk.")
|
|
155
|
+
return CLI::EXIT_USAGE
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
return run_verify_incremental(configuration, options, cache_root)
|
|
159
|
+
end
|
|
160
|
+
return run_incremental_check(configuration, options, cache_root, buffer) if options.fetch(:incremental)
|
|
169
161
|
|
|
170
162
|
nil
|
|
171
163
|
end
|
|
@@ -211,7 +203,7 @@ module Rigor
|
|
|
211
203
|
# run (plus their dependents), serving the rest from the snapshot; on a miss runs a full baseline. Persists the
|
|
212
204
|
# updated snapshot for the next invocation. Diagnostics are identical to a full run (the `--verify-incremental`
|
|
213
205
|
# gate enforces this); the win is skipping per-file inference for unchanged files.
|
|
214
|
-
def run_incremental_check(configuration, options, cache_root)
|
|
206
|
+
def run_incremental_check(configuration, options, cache_root, buffer = nil)
|
|
215
207
|
require_relative "check_runner_factory"
|
|
216
208
|
paths = @argv.empty? ? nil : @argv
|
|
217
209
|
fingerprint = Cache::IncrementalSnapshot.fingerprint(
|
|
@@ -225,9 +217,12 @@ module Rigor
|
|
|
225
217
|
# ADR-46 — thread the same worker-count precedence the standard `check` path uses
|
|
226
218
|
# (CLI `--workers` > `RIGOR_RACTOR_WORKERS` > `parallel.workers:` > 0) so the recheck's closure
|
|
227
219
|
# re-analysis parallelises; the fork pool marshals dependency records back so the graph is sound.
|
|
228
|
-
workers: CheckRunnerFactory.resolve_workers(options, configuration)
|
|
220
|
+
workers: CheckRunnerFactory.resolve_workers(options, configuration),
|
|
221
|
+
buffer: buffer
|
|
229
222
|
)
|
|
230
223
|
|
|
224
|
+
return run_editor_mode_option_b(session, snapshot, fingerprint, configuration, options) if buffer
|
|
225
|
+
|
|
231
226
|
diagnostics, warm = session.run_incremental(snapshot: snapshot, fingerprint: fingerprint)
|
|
232
227
|
# The banner's file count comes from the session's analyzed set (cold analyses all; a warm recheck's
|
|
233
228
|
# `@analyzed` advances to the current file set), so a dedicated probe Runner + `Dir.glob` is no longer
|
|
@@ -243,6 +238,33 @@ module Rigor
|
|
|
243
238
|
result.success? ? 0 : 1
|
|
244
239
|
end
|
|
245
240
|
|
|
241
|
+
# Editor mode option B (#146) — `--incremental` plus an editor buffer. The whole project is in scope with
|
|
242
|
+
# the buffer substituted for one file: the buffer's logical path and its dependents re-analyse, every
|
|
243
|
+
# other file is served from the snapshot. Before this, the two flags together silently ignored the
|
|
244
|
+
# buffer and analysed the file on disk, which is a wrong answer rather than a missing feature.
|
|
245
|
+
#
|
|
246
|
+
# The session never saves — the buffer's bytes exist only in the editor — so there is no warm state to
|
|
247
|
+
# build up. That is why a snapshot it cannot reuse means falling back to option A (single-file scope,
|
|
248
|
+
# the pre-#146 behaviour) instead of running a baseline that would repeat on the next keystroke. The
|
|
249
|
+
# note tells the user how to get option B: warm the snapshot with a plain `rigor check --incremental`.
|
|
250
|
+
def run_editor_mode_option_b(session, snapshot, fingerprint, configuration, options)
|
|
251
|
+
result = session.run_buffer_recheck(snapshot: snapshot, fingerprint: fingerprint)
|
|
252
|
+
if result.nil?
|
|
253
|
+
@err.puts("rigor: --incremental has no reusable snapshot for this project; analysing the buffer " \
|
|
254
|
+
"alone (run `rigor check --incremental` once to enable whole-project editor mode).")
|
|
255
|
+
return nil
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
@err.puts("rigor: --incremental editor mode — re-analysed #{result.affected.size} file(s), " \
|
|
259
|
+
"#{result.reused.size} served from cache")
|
|
260
|
+
emit_incremental_fact_surface_notes(session)
|
|
261
|
+
filtered = apply_baseline_filter(
|
|
262
|
+
Analysis::Result.new(diagnostics: result.diagnostics, stats: nil), configuration, options
|
|
263
|
+
)
|
|
264
|
+
write_result(filtered, options.fetch(:format))
|
|
265
|
+
filtered.success? ? 0 : 1
|
|
266
|
+
end
|
|
267
|
+
|
|
246
268
|
# ADR-88 WD1 — a one-line stderr note when the plugin fact surface (an ADR-9 fact, an ADR-60 producer
|
|
247
269
|
# value, or an `incremental_state_fingerprint` hook) forced a full run: either it CHANGED since the
|
|
248
270
|
# snapshot (a Sorbet sig edit, a schema change) or a contributing plugin declares NO fingerprint surface
|
|
@@ -383,11 +405,16 @@ module Rigor
|
|
|
383
405
|
@err.puts("rigor: #{silenced_count} diagnostic(s) silenced by baseline #{baseline_path}")
|
|
384
406
|
end
|
|
385
407
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
408
|
+
# The primary check run, routed through the shared {CheckInvocation} entry point so `rigor doctor` and
|
|
409
|
+
# `rigor skill describe --deep` reach a result through this exact path rather than re-deriving it (#148).
|
|
410
|
+
# Required lazily for the reason the old inline `CheckRunnerFactory.build` was: it pulls the inference engine,
|
|
411
|
+
# and ADR-87 WD4's cache-hit fast path must reach its verdict without it.
|
|
412
|
+
def invoke_check(configuration:, options:, buffer:, cache_root:)
|
|
413
|
+
require_relative "check_invocation"
|
|
414
|
+
CheckInvocation.run(
|
|
389
415
|
configuration: configuration, options: options,
|
|
390
|
-
buffer: buffer, cache_root: cache_root
|
|
416
|
+
buffer: buffer, cache_root: cache_root,
|
|
417
|
+
paths: @argv.empty? ? nil : @argv
|
|
391
418
|
)
|
|
392
419
|
end
|
|
393
420
|
|