rigortype 0.4.0 → 0.4.1
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 +4 -3
- data/docs/handbook/08-understanding-errors.md +3 -2
- data/docs/install.md +87 -15
- data/docs/manual/02-cli-reference.md +2 -2
- data/docs/manual/03-configuration.md +5 -0
- data/docs/manual/08-skills.md +4 -3
- data/docs/manual/12-caching.md +43 -4
- data/docs/manual/14-rails-quickstart.md +19 -11
- data/lib/rigor/analysis/check_rules/source_arity.rb +163 -59
- data/lib/rigor/analysis/check_rules.rb +118 -87
- data/lib/rigor/analysis/dependency_recorder.rb +200 -18
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +266 -0
- data/lib/rigor/analysis/incremental_run_slot.rb +171 -0
- data/lib/rigor/analysis/incremental_session.rb +273 -49
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
- data/lib/rigor/analysis/reachability/scan_cache.rb +2 -1
- data/lib/rigor/analysis/run_cache_key.rb +27 -8
- data/lib/rigor/analysis/run_stats.rb +2 -2
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +1 -1
- data/lib/rigor/analysis/runner/pool_coordinator.rb +2 -2
- data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -13
- data/lib/rigor/analysis/runner.rb +337 -46
- data/lib/rigor/analysis/shadow_harness.rb +27 -0
- data/lib/rigor/analysis/source_rbs_gate.rb +157 -0
- data/lib/rigor/analysis/template_unit_paths.rb +6 -4
- data/lib/rigor/analysis/template_units.rb +15 -5
- data/lib/rigor/analysis/worker_session.rb +4 -3
- data/lib/rigor/cache/descriptor.rb +40 -1
- data/lib/rigor/cache/file_digest.rb +28 -0
- data/lib/rigor/cache/incremental_snapshot.rb +70 -8
- data/lib/rigor/cache/store.rb +63 -3
- data/lib/rigor/cache/value_digest.rb +325 -0
- data/lib/rigor/cli/check_command.rb +83 -13
- data/lib/rigor/cli/doctor_command.rb +1 -1
- data/lib/rigor/cli/effects_report.rb +2 -2
- data/lib/rigor/cli/unused_command.rb +3 -3
- data/lib/rigor/cli.rb +1 -1
- data/lib/rigor/config_audit.rb +3 -2
- data/lib/rigor/config_path.rb +29 -0
- data/lib/rigor/configuration/severity_profile.rb +5 -0
- data/lib/rigor/configuration.rb +6 -5
- data/lib/rigor/effects/plugin_facts.rb +6 -6
- data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
- data/lib/rigor/environment/lockfile_resolver.rb +10 -1
- data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
- data/lib/rigor/environment/rbs_loader.rb +1 -1
- data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
- data/lib/rigor/environment.rb +13 -70
- data/lib/rigor/inference/block_parameter_binder.rb +29 -13
- data/lib/rigor/inference/captured_locals.rb +7 -1
- data/lib/rigor/inference/declaration_walk/context.rb +195 -0
- data/lib/rigor/inference/declaration_walk/errors.rb +18 -0
- data/lib/rigor/inference/declaration_walk/shadow.rb +147 -0
- data/lib/rigor/inference/declaration_walk/traversal.rb +441 -0
- data/lib/rigor/inference/declaration_walk.rb +7 -0
- data/lib/rigor/inference/def_node_resolver.rb +3 -2
- data/lib/rigor/inference/definer_resolution.rb +266 -0
- data/lib/rigor/inference/expression_typer.rb +136 -45
- data/lib/rigor/inference/external_ancestor_resolution.rb +13 -16
- data/lib/rigor/inference/guard_rebinding.rb +53 -12
- data/lib/rigor/inference/macro_block_self_type.rb +14 -2
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +46 -26
- data/lib/rigor/inference/module_function_state.rb +213 -0
- data/lib/rigor/inference/multi_target_binder.rb +21 -16
- data/lib/rigor/inference/narrowing.rb +147 -17
- data/lib/rigor/inference/parameter_inference_collector.rb +10 -9
- data/lib/rigor/inference/scope_indexer/certainty.rb +176 -0
- data/lib/rigor/inference/scope_indexer/class_cvars_collector.rb +35 -0
- data/lib/rigor/inference/scope_indexer/def_nestings_collector.rb +47 -0
- data/lib/rigor/inference/scope_indexer/layered_def_nestings.rb +36 -0
- data/lib/rigor/inference/scope_indexer/member_layouts_collector.rb +61 -0
- data/lib/rigor/inference/scope_indexer/mixin_accumulator.rb +130 -0
- data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
- data/lib/rigor/inference/scope_indexer.rb +1443 -295
- data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
- data/lib/rigor/inference/statement_evaluator.rb +80 -14
- data/lib/rigor/plugin/base.rb +28 -8
- data/lib/rigor/plugin/io_boundary.rb +82 -4
- data/lib/rigor/plugin/isolation.rb +1 -1
- data/lib/rigor/plugin/trust_policy.rb +3 -3
- data/lib/rigor/protection/discovery_seed.rb +18 -9
- data/lib/rigor/protection/mutation_cache.rb +3 -3
- data/lib/rigor/reflection/constant_ancestors.rb +96 -47
- data/lib/rigor/reflection/constant_path.rb +22 -7
- data/lib/rigor/reflection.rb +2 -3
- data/lib/rigor/scope/discovery_index.rb +218 -2
- data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
- data/lib/rigor/scope/resolution_chain.rb +884 -0
- data/lib/rigor/scope.rb +295 -322
- data/lib/rigor/sig_gen/alias_index.rb +1 -1
- data/lib/rigor/sig_gen/effect_annotation.rb +2 -2
- data/lib/rigor/sig_gen/generator.rb +22 -16
- data/lib/rigor/type/combinator.rb +25 -1
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +10 -0
- data/sig/rigor/environment.rbs +2 -0
- data/sig/rigor/plugin/io_boundary.rbs +1 -0
- data/sig/rigor/scope.rbs +43 -1
- data/skills/rigor-project-init/SKILL.md +37 -11
- data/skills/rigor-project-init/references/02-configure.md +7 -2
- metadata +25 -1
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "diagnostic"
|
|
4
|
+
require_relative "path_expansion"
|
|
5
|
+
require_relative "result"
|
|
6
|
+
require_relative "run_cache_key"
|
|
7
|
+
require_relative "../cache/descriptor"
|
|
8
|
+
require_relative "../cache/file_digest"
|
|
9
|
+
require_relative "../cache/store"
|
|
10
|
+
|
|
11
|
+
module Rigor
|
|
12
|
+
module Analysis
|
|
13
|
+
# ADR-45 WD2 (#1507) — the run-result slot `rigor check --incremental` writes, and the engine-free probe that
|
|
14
|
+
# serves a null run from it. The writer is {IncrementalSession#run_incremental}; the reader is the CLI, before
|
|
15
|
+
# it loads the inference engine. Both go through this module, so the key, the producer id and the stored shape
|
|
16
|
+
# have one definition.
|
|
17
|
+
#
|
|
18
|
+
# It is ADR-45's record-and-validate slot with a second writer. The value is the diagnostics the session's run
|
|
19
|
+
# printed (before the baseline filter, which the CLI applies to every answer), and the dependency descriptor
|
|
20
|
+
# validates everything that answer was computed from:
|
|
21
|
+
#
|
|
22
|
+
# - one `:stat` row per analysed file, carrying the digest the SESSION holds for it — the bytes its cached rows
|
|
23
|
+
# were computed from — rather than a re-digest taken after the run;
|
|
24
|
+
# - the rows the run re-read itself ({Runner#incremental_slot_rows}' `observed` and `derived` rows);
|
|
25
|
+
# - the chain the {Entry} carries forward: the rows the last full run recorded for the inputs the incremental
|
|
26
|
+
# path does not re-derive on a recheck, and the plugin reads credited to each file's analysis
|
|
27
|
+
# ({IncrementalSession#write_run_slot}).
|
|
28
|
+
#
|
|
29
|
+
# A hit is at least as careful as the full incremental path — where that path would notice a change, a row
|
|
30
|
+
# notices it first — but it is not a cold run: a stale answer that path serves for a read reaching a file
|
|
31
|
+
# through a plugin's memo (#1553) the probe may serve too. ADR-45 WD2 states the guarantee.
|
|
32
|
+
#
|
|
33
|
+
# Kept apart from the plain run's `analysis.run-diagnostics` by its own producer id and by the roots entry its
|
|
34
|
+
# key adds, either enough alone, so neither probe can ever read the other's entry, and an incremental defect
|
|
35
|
+
# cannot leak into a default run.
|
|
36
|
+
#
|
|
37
|
+
# The key is the one {RunCacheProbe} reconstructs from configuration alone — the library list without a
|
|
38
|
+
# `rbs.virtual_rbs` slot, and no `template-units` slot — plus the analysis roots ({Target}). Neither left-out
|
|
39
|
+
# slot is needed. A virtual RBS buffer is a function of an analysed file's bytes (a row) and of the
|
|
40
|
+
# synthesising plugin's identity and configuration (the key's `configuration`, lockfile and engine slots). A
|
|
41
|
+
# template unit is a function of its template's bytes and of what its transform read (rows) and of the
|
|
42
|
+
# plugin's identity and the engine's (key slots), so a project whose plugins claim template globs is served
|
|
43
|
+
# too. The ADR-87 probe misses on one: the plain slot's key carries the `template-units` slot.
|
|
44
|
+
#
|
|
45
|
+
# `explain:` is always false: the session's runners never set it (#1533), and the CLI declines the probe under
|
|
46
|
+
# `--explain`.
|
|
47
|
+
module IncrementalRunSlot
|
|
48
|
+
module_function
|
|
49
|
+
|
|
50
|
+
PRODUCER_ID = "analysis.incremental-run-diagnostics"
|
|
51
|
+
|
|
52
|
+
# One live generation per analysed-path set, as for the plain slot. A writer that moves the path set discards
|
|
53
|
+
# the entry it carried from (see {IncrementalSession}), so the churn a file addition makes is bounded without
|
|
54
|
+
# a compaction pass — the incremental path never runs one.
|
|
55
|
+
GENERATION_CAP = RunCacheKey::GENERATION_CAP
|
|
56
|
+
|
|
57
|
+
# The stored value. `baseline` and `pinned` (each a {Cache::Descriptor}) and `reads` (`{path =>
|
|
58
|
+
# Cache::Descriptor}`) are the chain the next writer carries forward, kept apart from the rest of the
|
|
59
|
+
# dependency descriptor so it knows which rows are which; their entries are the same objects the descriptor
|
|
60
|
+
# holds, so `Marshal` writes them once. `baseline` and `pinned` hold the rows the last full run recorded for
|
|
61
|
+
# inputs the incremental path does not re-derive on a recheck (the signature tree, the
|
|
62
|
+
# discovered-not-analysed files, the `pre_eval:` files outside the analysed set), `pinned` the signature
|
|
63
|
+
# files the key identifies already ({Runner::BaselineRows}); `reads` the plugin reads credited to each
|
|
64
|
+
# analysed file. `snapshot` is the identity of
|
|
65
|
+
# the incremental snapshot file the writing run left behind, which the next writer compares against the one
|
|
66
|
+
# it restored before trusting the chain. `roots` are the analysis roots as the run was given them
|
|
67
|
+
# ({.as_written}): the key holds them as a set of paths, and the probe serves the entry only to a run that
|
|
68
|
+
# names them the same way, in the same order.
|
|
69
|
+
Entry = Data.define(:diagnostics, :roots, :baseline, :pinned, :reads, :snapshot)
|
|
70
|
+
|
|
71
|
+
# What a slot is keyed by: the analysed-path set and the analysis roots the run was given. The roots are
|
|
72
|
+
# not implied by the files — `rigor check --incremental lib extra` with `extra` missing analyses the same
|
|
73
|
+
# files as `rigor check --incremental lib`, and only the first reports `extra` as missing.
|
|
74
|
+
Target = Data.define(:files, :roots)
|
|
75
|
+
|
|
76
|
+
# What a probe hit hands the CLI: the answer, and the analysed-file count its banner reports.
|
|
77
|
+
Hit = Data.define(:result, :file_count)
|
|
78
|
+
|
|
79
|
+
def key(configuration:, target:)
|
|
80
|
+
base = RunCacheKey.descriptor(
|
|
81
|
+
configuration: configuration, files: target.files, explain: false,
|
|
82
|
+
rbs_config_entries: RunCacheKey.libraries_config_entries(configuration)
|
|
83
|
+
)
|
|
84
|
+
return nil if base.nil?
|
|
85
|
+
|
|
86
|
+
roots = RunCacheKey.config_entry("incremental.roots", normalize_roots(target.roots).sort.join("\n"))
|
|
87
|
+
Cache::Descriptor.new(gems: base.gems, configs: base.configs + [roots])
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# The roots as absolute paths, which the KEY holds sorted: `lib`, `./lib` and `lib/` are one root there. A
|
|
91
|
+
# run that reorders its roots still finds the previous slot's chain (the snapshot fingerprint sorts them
|
|
92
|
+
# too), and one that respells them replaces the slot rather than adding one beside it.
|
|
93
|
+
def normalize_roots(roots)
|
|
94
|
+
Array(roots).map { |root| File.absolute_path(root.to_s) }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# The roots as the run was given them, which {Entry#roots} holds and {.serve} compares. A missing root is
|
|
98
|
+
# reported as written (`./extra: no such file or directory`), and `a b` lists `a`'s files before `b`'s, so
|
|
99
|
+
# neither a respelling nor a reordering may be served another's answer.
|
|
100
|
+
def as_written(roots)
|
|
101
|
+
Array(roots).map(&:to_s)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# The engine-free probe. Returns a {Hit} when the slot for this run's analysed-path set validates, or nil to
|
|
105
|
+
# decline: a miss, a stale row, an unreadable entry, a project with effect collection on (see below), or any
|
|
106
|
+
# failure at all — declining hands the run to the full path, which is always safe.
|
|
107
|
+
#
|
|
108
|
+
# Effect collection is declined here and never written by the session. Its configuration (`effects:`) is
|
|
109
|
+
# deliberately absent from `Configuration#to_h` and so from the key, and two of its rows are judged each run
|
|
110
|
+
# from state no engine-free path can rebuild (the plain probe's `envelope_lane_live?`, #428). With it off,
|
|
111
|
+
# the only effect row a run can print is `effect.annotations-unchecked`, which is a function of the
|
|
112
|
+
# signature tree and the analysed files' annotations — both validated rows — so it is served as stored.
|
|
113
|
+
def serve(configuration:, cache_root:, paths:)
|
|
114
|
+
return nil if configuration.effects_enabled?
|
|
115
|
+
|
|
116
|
+
files = PathExpansion.ruby_files(paths, configuration.exclude_patterns)
|
|
117
|
+
slot_key = key(configuration: configuration, target: Target.new(files: files, roots: paths))
|
|
118
|
+
return nil if slot_key.nil?
|
|
119
|
+
|
|
120
|
+
entry = validated_entry(configuration, cache_root, slot_key)
|
|
121
|
+
return nil unless entry.is_a?(Entry) && entry.diagnostics.is_a?(Array)
|
|
122
|
+
return nil unless entry.roots == as_written(paths)
|
|
123
|
+
|
|
124
|
+
Hit.new(result: Result.new(diagnostics: entry.diagnostics, stats: nil), file_count: files.size)
|
|
125
|
+
rescue StandardError
|
|
126
|
+
nil
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# The previous slot's {Entry}, read WITHOUT validating it: it is stale by construction, since the run that
|
|
130
|
+
# is asking changed something. nil when there is no such slot, or it is not an {Entry} — the writer then
|
|
131
|
+
# skips its own write rather than guess what the files it served from cache depended on.
|
|
132
|
+
def previous_entry(store:, configuration:, target:)
|
|
133
|
+
slot_key = key(configuration: configuration, target: target)
|
|
134
|
+
return nil if slot_key.nil?
|
|
135
|
+
|
|
136
|
+
entry = store.peek_unvalidated(producer_id: PRODUCER_ID, key_descriptor: slot_key)
|
|
137
|
+
return nil unless entry.is_a?(Entry) && entry.reads.is_a?(Hash)
|
|
138
|
+
return nil unless entry.baseline.is_a?(Cache::Descriptor) && entry.pinned.is_a?(Cache::Descriptor)
|
|
139
|
+
|
|
140
|
+
entry
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Writes `entry` as the slot for `target`, validated by `dependencies` (the whole descriptor, of which the
|
|
144
|
+
# entry's chain is a part). Returns whether it wrote.
|
|
145
|
+
def write(store:, configuration:, target:, entry:, dependencies:)
|
|
146
|
+
slot_key = key(configuration: configuration, target: target)
|
|
147
|
+
return false if slot_key.nil?
|
|
148
|
+
|
|
149
|
+
store.store_validated(
|
|
150
|
+
producer_id: PRODUCER_ID, key_descriptor: slot_key, generation_cap: GENERATION_CAP,
|
|
151
|
+
value: entry, dependencies: dependencies
|
|
152
|
+
)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def discard(store:, configuration:, target:)
|
|
156
|
+
slot_key = key(configuration: configuration, target: target)
|
|
157
|
+
slot_key && store.discard(producer_id: PRODUCER_ID, key_descriptor: slot_key)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Validation runs in its own per-run digest scope, with the configuration's `cache.validation` mode, exactly
|
|
161
|
+
# as the plain probe's does.
|
|
162
|
+
def validated_entry(configuration, cache_root, slot_key)
|
|
163
|
+
store = Cache::Store.new(root: cache_root, max_bytes: configuration.cache_max_bytes)
|
|
164
|
+
Cache::FileDigest.with_run(strict: configuration.cache_validation_strict?) do
|
|
165
|
+
store.peek_validated(producer_id: PRODUCER_ID, key_descriptor: slot_key)
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
private_class_method :validated_entry
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
require "digest"
|
|
4
4
|
require_relative "incremental"
|
|
5
|
+
require_relative "incremental_run_slot"
|
|
6
|
+
require_relative "incremental_run_slot/write_guard"
|
|
5
7
|
require_relative "plugin_fact_fingerprint"
|
|
8
|
+
require_relative "source_rbs_gate"
|
|
6
9
|
require_relative "../cache/file_digest"
|
|
7
10
|
require_relative "../cache/incremental_snapshot"
|
|
8
11
|
require_relative "../effects/file_collection"
|
|
@@ -22,10 +25,11 @@ module Rigor
|
|
|
22
25
|
# only `ΔF ∪ dependents[ΔF]`, and serves every other analyzed file from the cache — the body tier.
|
|
23
26
|
#
|
|
24
27
|
# The invariant the verify harness (and the spec) assert: `#recheck`'s merged diagnostics are
|
|
25
|
-
# byte-identical
|
|
26
|
-
# `--verify-incremental` acceptance gate, here without disk
|
|
27
|
-
# in-process). It models the body tier only: an edit that adds /
|
|
28
|
-
# the analyzed set it maintains and falls to a fresh {#baseline} (the
|
|
28
|
+
# byte-identical to a full `--no-cache` re-analysis of the edited tree, in the same order (#1524; the gate
|
|
29
|
+
# itself compares sorted sets). This is the `--verify-incremental` acceptance gate, here without disk
|
|
30
|
+
# persistence or CLI wiring (the cache is in-process). It models the body tier only: an edit that adds /
|
|
31
|
+
# removes / moves a *file* is outside the analyzed set it maintains and falls to a fresh {#baseline} (the
|
|
32
|
+
# structural tier is a later slice).
|
|
29
33
|
# The class-length budget is relaxed: this is one cohesive orchestrator of the incremental state
|
|
30
34
|
# (per-file diagnostics cache, the file-level / symbol-level / negative dependency graphs, and the ADR-85
|
|
31
35
|
# seed bundles), clearer read together than split across micro-classes that would all share the same ivars.
|
|
@@ -159,6 +163,7 @@ module Rigor
|
|
|
159
163
|
# Full baseline analysis with recording. Returns the run's diagnostics; populates the in-process cache
|
|
160
164
|
# + dependency state.
|
|
161
165
|
def baseline
|
|
166
|
+
@slot_carried_from = nil # ADR-45 WD2 — see {#load_snapshot}
|
|
162
167
|
runner = build_runner(record_dependencies: true)
|
|
163
168
|
diagnostics = run_runner(runner).diagnostics
|
|
164
169
|
@last_runner = runner # ADR-88 WD1 — the post-hoc fact-surface fingerprint reads this prepared registry.
|
|
@@ -167,7 +172,9 @@ module Rigor
|
|
|
167
172
|
# must never read as a file that VANISHED from the project on the next recheck (which is what
|
|
168
173
|
# `previous - current` would say, `current_files` being a `.rb` expansion).
|
|
169
174
|
@analyzed = runner.analyzed_files - runner.template_unit_paths
|
|
170
|
-
|
|
175
|
+
# ADR-85 WD2 — the freshly built bundle set for the next run, each stamped with its file's source-RBS
|
|
176
|
+
# digest (issue #1536).
|
|
177
|
+
@seed_bundles = source_rbs_gate.stamp(runner.seed_bundles, whole_project: true)
|
|
171
178
|
absorb_dependency_graph(runner)
|
|
172
179
|
@return_summaries = runner.return_summaries # ADR-89 WD2 — the full-run behavioural surface.
|
|
173
180
|
# ADR-67 WD6c lift — the seed table the runner's own pre-pass computed ({} when the gate is off).
|
|
@@ -178,6 +185,7 @@ module Rigor
|
|
|
178
185
|
@effects_identity = current_effects_identity
|
|
179
186
|
@cache = per_file(runner.per_file_diagnostics)
|
|
180
187
|
@digests = @analyzed.to_h { |path| [path, pack_digest(path)] }
|
|
188
|
+
forget_unbound_digests
|
|
181
189
|
@run_level_rows = runner.run_level_rows
|
|
182
190
|
diagnostics
|
|
183
191
|
end
|
|
@@ -199,15 +207,16 @@ module Rigor
|
|
|
199
207
|
param_files, param_pairs = param_seed_invalidation(fresh_params)
|
|
200
208
|
affected = affected_closure(changed, added, removed, param_files, param_pairs)
|
|
201
209
|
analyze_set = affected & current
|
|
210
|
+
reused = (current & previous) - affected.to_a
|
|
202
211
|
# The freshly collected table is handed to the runner so the run seeds from the SAME table the diff
|
|
203
|
-
# was decided on (and the collector runs once per recheck, not twice).
|
|
212
|
+
# was decided on (and the collector runs once per recheck, not twice). The reused files' cached rows
|
|
213
|
+
# go in too, so the runner lists them in the full run's file order (#1524).
|
|
204
214
|
runner = build_runner(analyze_only: analyze_set, record_dependencies: true,
|
|
205
215
|
param_inferred_types: fresh_params,
|
|
206
|
-
restored_run_level_rows: @run_level_rows
|
|
207
|
-
|
|
216
|
+
restored_run_level_rows: @run_level_rows,
|
|
217
|
+
served_per_file_diagnostics: @cache.slice(*reused))
|
|
218
|
+
merged = run_runner(runner).diagnostics
|
|
208
219
|
@last_runner = runner # ADR-88 WD1 — the post-hoc fact-surface fingerprint reads this prepared registry.
|
|
209
|
-
reused = (current & previous) - affected.to_a
|
|
210
|
-
merged = fresh + reused.flat_map { |path| @cache[path] || [] }
|
|
211
220
|
absorb(runner, current, analyze_set, removed)
|
|
212
221
|
@param_table = fresh_params
|
|
213
222
|
Recheck.new(diagnostics: merged, changed: changed.to_set, added: added.to_set,
|
|
@@ -235,6 +244,7 @@ module Rigor
|
|
|
235
244
|
# stale. A full baseline is the sound answer for `--incremental`, but in editor mode it is also the
|
|
236
245
|
# latency this mode exists to avoid, so decline and let the caller drop to single-file scope.
|
|
237
246
|
break nil unless @plugin_fact_reusable.reusable_against?(restored.plugin_fact_digest)
|
|
247
|
+
break nil if source_rbs_unverified?(result)
|
|
238
248
|
|
|
239
249
|
result
|
|
240
250
|
end
|
|
@@ -253,7 +263,20 @@ module Rigor
|
|
|
253
263
|
# return exactly the way a body edit does, so it reuses the same audited dependents machinery. The
|
|
254
264
|
# pairs join AFTER the ADR-89 WD2 behavioural-stability pruning: that gate re-evaluates returns under
|
|
255
265
|
# the snapshot's OLD seeds, which is the wrong oracle for a pair whose seeds are the thing that moved.
|
|
266
|
+
#
|
|
267
|
+
# Issue #1536 (ADR-89 WD1 amendment) — every gate in {#gated_closure} reads the RUBY side of a file. What a
|
|
268
|
+
# source-RBS synthesizer derives from its comments reaches the environment as a `virtual:` buffer, and a
|
|
269
|
+
# reader of that RBS records no edge to the file it came from (an `attr_reader :name #: String` consumer
|
|
270
|
+
# records none at all), so no dependent set can bound who read it. An edit that moved it re-analyses the
|
|
271
|
+
# whole project — what a `sig/` edit already costs through the snapshot fingerprint.
|
|
256
272
|
def affected_closure(changed, added, removed, param_files = Set.new, param_pairs = Set.new)
|
|
273
|
+
return whole_project_closure(changed, added) if source_rbs_gate.moved?(@seed_bundles, changed, added, removed)
|
|
274
|
+
|
|
275
|
+
gated_closure(changed, added, removed, param_files, param_pairs)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# {#affected_closure} for an edit whose synthesized RBS is byte-identical in every file it touched.
|
|
279
|
+
def gated_closure(changed, added, removed, param_files, param_pairs)
|
|
257
280
|
scan = changed + added
|
|
258
281
|
# Parse the changed / added set ONCE for the per-symbol fingerprints, the class declarations, AND the
|
|
259
282
|
# ADR-89 WD1 declaration signatures. They were separate `discovered_def_index_for_paths` passes over
|
|
@@ -396,13 +419,11 @@ module Rigor
|
|
|
396
419
|
# dependents skippable) when its current {ScopeIndexer.declaration_signature} matches the one stored in
|
|
397
420
|
# the snapshot's seed bundle — i.e. its edit changed no method signature, visibility, ancestry, member
|
|
398
421
|
# layout, def line, or method existence, so every cross-file DECLARATION fact its dependents consume is
|
|
399
|
-
# unchanged.
|
|
400
|
-
#
|
|
401
|
-
#
|
|
402
|
-
# CODE, captured by ADR-88's plugin-fact fingerprint (WD3)
|
|
422
|
+
# unchanged. The signature reads code, not comments; the other half of stability — that no source-RBS
|
|
423
|
+
# synthesizer's output for the file moved — is settled before this runs ({SourceRbsGate#moved?}), so every
|
|
424
|
+
# file reaching here has a byte-identical synthesized contribution. Sorbet sigs / dry-types includes are
|
|
425
|
+
# CODE, captured by ADR-88's plugin-fact fingerprint (WD3).
|
|
403
426
|
def declaration_unstable(changed, declaration_signatures)
|
|
404
|
-
return changed if comment_ingesting_plugin_loaded?
|
|
405
|
-
|
|
406
427
|
changed.reject { |path| declaration_stable?(path, declaration_signatures[path]) }
|
|
407
428
|
end
|
|
408
429
|
|
|
@@ -415,14 +436,11 @@ module Rigor
|
|
|
415
436
|
bundle[:declaration_signature] == current_declaration_signature
|
|
416
437
|
end
|
|
417
438
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
@configuration.plugins.any? do |entry|
|
|
422
|
-
name = entry.is_a?(Hash) ? (entry["gem"] || entry["id"]) : entry
|
|
423
|
-
COMMENT_INGESTING_PLUGIN_IDS.include?(name.to_s)
|
|
424
|
-
end
|
|
439
|
+
# Every file this recheck can analyse: the removed ones drop out at `affected & current`.
|
|
440
|
+
def whole_project_closure(changed, added)
|
|
441
|
+
(@analyzed.to_set | changed | added).freeze
|
|
425
442
|
end
|
|
443
|
+
private :gated_closure, :whole_project_closure
|
|
426
444
|
|
|
427
445
|
# The current project file set (cheap directory expansion, no analysis), used to detect files added /
|
|
428
446
|
# removed since the last run. Also where an editor buffer's binding is re-spelled onto the set, since
|
|
@@ -441,13 +459,13 @@ module Rigor
|
|
|
441
459
|
# mutating session state. Returns the merged diagnostics.
|
|
442
460
|
def reanalyze_subset(subset)
|
|
443
461
|
affected = subset.to_set
|
|
462
|
+
reused = @analyzed - affected.to_a
|
|
444
463
|
# ADR-67 WD6c lift — seed the subset run from the baseline's own table so the verification engine
|
|
445
464
|
# exercises the exact seeds the served cache entries were computed under (and skips a re-collect).
|
|
446
465
|
runner = build_runner(analyze_only: affected, param_inferred_types: @param_table,
|
|
447
|
-
restored_run_level_rows: @run_level_rows
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
fresh + reused.flat_map { |path| @cache[path] || [] }
|
|
466
|
+
restored_run_level_rows: @run_level_rows,
|
|
467
|
+
served_per_file_diagnostics: @cache.slice(*reused))
|
|
468
|
+
run_runner(runner).diagnostics
|
|
451
469
|
end
|
|
452
470
|
|
|
453
471
|
# Cross-process incremental run (the `--incremental` flag's engine). With a disk `snapshot` whose
|
|
@@ -468,7 +486,7 @@ module Rigor
|
|
|
468
486
|
# shares one digest memo across change detection and the baseline/absorb re-pack. The inner
|
|
469
487
|
# `Runner#run` nests its own `with_run` for the analysis descriptors; nesting is safe (each restores).
|
|
470
488
|
Cache::FileDigest.with_run(strict: @configuration.cache_validation_strict?) do
|
|
471
|
-
restored =
|
|
489
|
+
restored = load_snapshot(snapshot, fingerprint)
|
|
472
490
|
# ADR-88 WD1 — the plugin fact-surface fingerprint gates snapshot reuse the same way the global
|
|
473
491
|
# fingerprint gates the load: a plugin sig/catalog edit outside `signature_paths:` (a Sorbet `.rbi`)
|
|
474
492
|
# changes the types unchanged call sites resolve, without moving any analyzed file, so the global
|
|
@@ -489,7 +507,7 @@ module Rigor
|
|
|
489
507
|
result = recheck
|
|
490
508
|
adopt_plugin_fact_fingerprint
|
|
491
509
|
fact_reusable = @plugin_fact_reusable.reusable_against?(restored.plugin_fact_digest)
|
|
492
|
-
if fact_reusable && effects_reusable
|
|
510
|
+
if fact_reusable && effects_reusable && !source_rbs_unverified?(result)
|
|
493
511
|
diagnostics = result.diagnostics
|
|
494
512
|
warm = true
|
|
495
513
|
# ADR-87 WD3 — a warm recheck that changed nothing leaves the session state byte-equivalent to the
|
|
@@ -497,9 +515,10 @@ module Rigor
|
|
|
497
515
|
# A cold baseline always persists — there was no valid snapshot to reuse.
|
|
498
516
|
skip_save = result.no_change?
|
|
499
517
|
else
|
|
500
|
-
# The fact surface moved (a plugin sig/catalog edit), a plugin is opaque,
|
|
501
|
-
#
|
|
502
|
-
#
|
|
518
|
+
# The fact surface moved (a plugin sig/catalog edit), a plugin is opaque, the effects identity
|
|
519
|
+
# moved, or a synthesizer the source-RBS gate could not see was loaded (#1536): the cached-served
|
|
520
|
+
# files the recheck merged may be stale (or, for effects, a partial collection cannot be closed),
|
|
521
|
+
# so re-analyze the whole tree. The current fact-surface
|
|
503
522
|
# digest (from the recheck runner) is unchanged by the re-analysis, so it is kept for the save.
|
|
504
523
|
# Only a genuine fact-surface reason sets the reporting flag the CLI banner reads.
|
|
505
524
|
@fact_surface_invalidated = true unless fact_reusable
|
|
@@ -513,7 +532,7 @@ module Rigor
|
|
|
513
532
|
warm = false
|
|
514
533
|
skip_save = false
|
|
515
534
|
end
|
|
516
|
-
snapshot
|
|
535
|
+
persist_run(snapshot, fingerprint, diagnostics, skip_save: skip_save) if persist && fingerprint
|
|
517
536
|
[diagnostics, warm]
|
|
518
537
|
end
|
|
519
538
|
end
|
|
@@ -538,11 +557,41 @@ module Rigor
|
|
|
538
557
|
|
|
539
558
|
# ADR-88 WD1 — capture this invocation's fact-surface fingerprint (from the last analysis runner) onto the
|
|
540
559
|
# reporting ivars + the `@plugin_fact_reusable` decision object.
|
|
560
|
+
#
|
|
561
|
+
# Issue #1536 — the same prepared registry settles whether the synthesizers the session digested with
|
|
562
|
+
# are the ones the run's environment was built from ({SourceRbsGate#verify}). A plugin that builds its
|
|
563
|
+
# synthesizer in `#prepare` leaves the gate untrusted, and every stored digest is stamped unknown before
|
|
564
|
+
# the snapshot is saved.
|
|
541
565
|
def adopt_plugin_fact_fingerprint
|
|
542
|
-
|
|
566
|
+
registry = prepared_plugin_registry
|
|
567
|
+
fact = compute_plugin_fact_fingerprint(registry)
|
|
543
568
|
@plugin_fact_digest = fact.digest
|
|
544
569
|
@opaque_plugin_ids = fact.opaque_plugin_ids
|
|
545
570
|
@plugin_fact_reusable = fact
|
|
571
|
+
@seed_bundles = source_rbs_gate.verify(registry, @seed_bundles)
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
# Issue #1536 — a recheck of an edit whose closure left some file out may have under-read a synthesizer
|
|
575
|
+
# the gate could not see. {#run_incremental} re-analyses the project and {#run_buffer_recheck} declines,
|
|
576
|
+
# the way each treats a moved plugin fact surface. A run that changed nothing is exempt: under an
|
|
577
|
+
# untrusted gate every earlier edit already re-analysed the whole project, so the snapshot it serves is
|
|
578
|
+
# whole, and a null run stays one.
|
|
579
|
+
def source_rbs_unverified?(result)
|
|
580
|
+
source_rbs_gate.untrusted? && !result.no_change? && !result.reused.empty?
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
# Issue #1536 — a file the gate could not stamp because it was saved after the closure was decided (its
|
|
584
|
+
# readers' cached answers predate that save) must read as changed on the next run, where its unknown
|
|
585
|
+
# stamp sends the edit to the whole project. Recording its post-save content digest would hide it.
|
|
586
|
+
def forget_unbound_digests
|
|
587
|
+
source_rbs_gate.unbound.each { |path| @digests.delete(path) }
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
# Issue #1536 — decides whether an edit moved synthesized RBS, and stamps each seed bundle's digest. One
|
|
591
|
+
# per session: its untrusted state must outlive every run the session makes.
|
|
592
|
+
def source_rbs_gate
|
|
593
|
+
@source_rbs_gate ||= SourceRbsGate.new(configuration: @configuration, cache_store: @cache_store,
|
|
594
|
+
plugin_requirer: @plugin_requirer)
|
|
546
595
|
end
|
|
547
596
|
|
|
548
597
|
# ADR-88 WD1 — the plugin fact-surface fingerprint for this invocation. For a SEQUENTIAL run it is read
|
|
@@ -551,17 +600,195 @@ module Rigor
|
|
|
551
600
|
# published facts — there it falls back to the always-sequential probe. Both paths compute the identical
|
|
552
601
|
# digest for a given fact surface, so the reuse decision is worker-count-independent (the parity spec
|
|
553
602
|
# asserts this).
|
|
554
|
-
def compute_plugin_fact_fingerprint
|
|
603
|
+
def compute_plugin_fact_fingerprint(registry = prepared_plugin_registry)
|
|
604
|
+
PluginFactFingerprint.from_registry(registry)
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
# The registry the run's environment was built from, after `#prepare`: the analysis runner's own on a
|
|
608
|
+
# sequential run, the always-sequential probe's on a pooled one (nil when the probe fails).
|
|
609
|
+
def prepared_plugin_registry
|
|
555
610
|
registry = @last_runner&.plugin_registry
|
|
556
|
-
if @workers.zero? && registry && !registry.empty?
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
611
|
+
return registry if @workers.zero? && registry && !registry.empty?
|
|
612
|
+
|
|
613
|
+
PluginFactFingerprint.prepared_registry(
|
|
614
|
+
configuration: @configuration, cache_store: @cache_store, plugin_requirer: @plugin_requirer
|
|
615
|
+
)
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
# Loads the snapshot, noting (ADR-45 WD2) which write of the file it read: the identity is kept only when
|
|
619
|
+
# the file was the same before and after the read, since a rewrite in between leaves no telling which one
|
|
620
|
+
# the payload came from.
|
|
621
|
+
def load_snapshot(snapshot, fingerprint)
|
|
622
|
+
@slot_guard = start_slot_guard(fingerprint)
|
|
623
|
+
before = snapshot_identity(snapshot)
|
|
624
|
+
restored = fingerprint && snapshot.load(fingerprint: fingerprint)
|
|
625
|
+
@restored_snapshot_identity = restored && before == snapshot_identity(snapshot) ? before : nil
|
|
626
|
+
# The path set the previous run-result slot is keyed by, for a recheck to carry its chain from; a
|
|
627
|
+
# {#baseline} clears it, since a full run starts a chain of its own.
|
|
628
|
+
@slot_carried_from = restored&.analyzed
|
|
629
|
+
restored
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
# ADR-87 WD3 — a warm recheck that changed nothing skips the snapshot rewrite: the file it restored is still
|
|
633
|
+
# the one on disk, which is what the run-result slot records as its snapshot.
|
|
634
|
+
def persist_run(snapshot, fingerprint, diagnostics, skip_save:)
|
|
635
|
+
written = if skip_save
|
|
636
|
+
@restored_snapshot_identity
|
|
637
|
+
elsif snapshot.save(fingerprint: fingerprint, payload: to_payload)
|
|
638
|
+
snapshot_identity(snapshot)
|
|
639
|
+
end
|
|
640
|
+
write_run_slot(diagnostics, written_identity: written)
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
# ADR-45 WD2 (#1507) — records this run's answer as the run-result slot a later `rigor check --incremental`
|
|
644
|
+
# serves without loading the engine ({IncrementalRunSlot}). The slot's dependency descriptor must validate
|
|
645
|
+
# what the answer was computed from, and after a narrowed recheck most of the answer was computed by earlier
|
|
646
|
+
# runs, so it is assembled from three sources:
|
|
647
|
+
#
|
|
648
|
+
# - the analysed files, at the digest this session holds for each: the bytes its cached or fresh rows were
|
|
649
|
+
# computed from, never a re-digest taken after the fact;
|
|
650
|
+
# - what this run read and re-derives every run ({Runner#incremental_slot_rows}' `observed` and `derived`
|
|
651
|
+
# rows);
|
|
652
|
+
# - the CHAIN: the rows the last full run recorded for inputs a recheck does not re-derive
|
|
653
|
+
# ({Runner#baseline_dependency_rows}), and the plugin reads credited to each file's analysis. A full run
|
|
654
|
+
# starts a chain; a recheck takes the previous slot's, replaces the reads of every file it re-analysed with
|
|
655
|
+
# this run's, and drops the removed files'.
|
|
656
|
+
#
|
|
657
|
+
# A recheck writes only when the previous slot exists and was written against the very snapshot file it
|
|
658
|
+
# restored: otherwise some run in between — a pool run, `--no-cache`, a write that failed — analysed files
|
|
659
|
+
# without recording their reads, and the chain cannot vouch for them. It then writes nothing, and the next
|
|
660
|
+
# full run starts a fresh chain. Declining to write is always safe; it only leaves the next null run on the
|
|
661
|
+
# full path.
|
|
662
|
+
#
|
|
663
|
+
# Every row not recorded as a plugin read it must still describe the tree the run read
|
|
664
|
+
# ({IncrementalRunSlot::WriteGuard}): the analysed files' (whose session digest a full run takes after its
|
|
665
|
+
# analysis), the `derived` rows, and the chain's baseline, taken now or carried ({#guarded_rows}).
|
|
666
|
+
#
|
|
667
|
+
# @param written_identity — the snapshot file as this run leaves it, recorded for the next writer's check.
|
|
668
|
+
def write_run_slot(diagnostics, written_identity:)
|
|
669
|
+
return unless run_slot_writable?
|
|
670
|
+
|
|
671
|
+
roots = @paths || @configuration.paths
|
|
672
|
+
rows = @last_runner.incremental_slot_rows(roots: roots)
|
|
673
|
+
chain = rows && (@slot_carried_from ? carry_chain(rows, roots) : start_chain(rows))
|
|
674
|
+
analysed = chain && analysed_file_rows
|
|
675
|
+
return if analysed.nil?
|
|
676
|
+
|
|
677
|
+
baseline, pinned, reads = chain
|
|
678
|
+
return unless @slot_guard&.admits?(guarded_rows(analysed, rows.derived, baseline), pinned: pinned)
|
|
679
|
+
|
|
680
|
+
# A save the run read after the mark's tick must not hide behind an unmoved stat tuple.
|
|
681
|
+
reads = reads.transform_values { |descriptor| @slot_guard.mark_racy(descriptor) }
|
|
682
|
+
|
|
683
|
+
entry = IncrementalRunSlot::Entry.new(
|
|
684
|
+
diagnostics: diagnostics, roots: IncrementalRunSlot.as_written(roots),
|
|
685
|
+
baseline: baseline, pinned: pinned, reads: reads, snapshot: written_identity
|
|
686
|
+
)
|
|
687
|
+
dependencies = @slot_guard.mark_racy(run_slot_dependencies(analysed, rows, [baseline, pinned], reads.values))
|
|
688
|
+
wrote = IncrementalRunSlot.write(
|
|
689
|
+
store: @cache_store, configuration: @configuration, entry: entry, dependencies: dependencies,
|
|
690
|
+
target: IncrementalRunSlot::Target.new(files: @analyzed, roots: roots)
|
|
691
|
+
)
|
|
692
|
+
discard_previous_slot(roots) if wrote
|
|
693
|
+
rescue StandardError
|
|
694
|
+
nil
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
# The previous slot is keyed by the previous path set; once this run's stands, nothing asks for it again.
|
|
698
|
+
def discard_previous_slot(roots)
|
|
699
|
+
return if @slot_carried_from.nil? || @slot_carried_from.sort == @analyzed.sort
|
|
700
|
+
|
|
701
|
+
IncrementalRunSlot.discard(
|
|
702
|
+
store: @cache_store, configuration: @configuration,
|
|
703
|
+
target: IncrementalRunSlot::Target.new(files: @slot_carried_from, roots: roots)
|
|
704
|
+
)
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
# ADR-45 WD2 — the mark {IncrementalRunSlot::WriteGuard#admits?} checks the slot's rows against, taken before
|
|
708
|
+
# the run reads anything. nil (no slot this run) for a store no slot is written to, or no fingerprint.
|
|
709
|
+
def start_slot_guard(fingerprint)
|
|
710
|
+
return nil if fingerprint.nil? || @cache_store.nil? || @cache_store.read_only? || !@buffer.nil?
|
|
711
|
+
# Known before the run: none of these can be written afterwards, so the stamp and the recomputed
|
|
712
|
+
# fingerprint would be spent for nothing.
|
|
713
|
+
return nil unless @workers.zero? && !@configuration.effects_enabled?
|
|
714
|
+
|
|
715
|
+
IncrementalRunSlot::WriteGuard.start(
|
|
716
|
+
configuration: @configuration, roots: @paths || @configuration.paths, cache_root: @cache_store.root,
|
|
717
|
+
fingerprint: fingerprint
|
|
718
|
+
)
|
|
719
|
+
end
|
|
720
|
+
|
|
721
|
+
# The runs whose answer the slot may hold: a writable store, no editor buffer (whose bytes exist only in
|
|
722
|
+
# the editor), a sequential run (a pool worker's plugin reads never reach this process), effect collection
|
|
723
|
+
# off ({IncrementalRunSlot.serve} declines it), and no opaque plugin — the next full-path run would then be
|
|
724
|
+
# cold, not the warm run the probe stands in for.
|
|
725
|
+
def run_slot_writable?
|
|
726
|
+
!@cache_store.nil? && !@cache_store.read_only? && @buffer.nil? && @workers.zero? &&
|
|
727
|
+
!@configuration.effects_enabled? && @opaque_plugin_ids.empty? && !@last_runner.nil?
|
|
728
|
+
end
|
|
729
|
+
|
|
730
|
+
def start_chain(rows)
|
|
731
|
+
baseline = @last_runner.baseline_dependency_rows(files: @analyzed)
|
|
732
|
+
baseline && [baseline.owned, baseline.pinned, rows.by_file.slice(*@analyzed)]
|
|
733
|
+
end
|
|
734
|
+
|
|
735
|
+
def carry_chain(rows, roots)
|
|
736
|
+
return nil if @restored_snapshot_identity.nil?
|
|
737
|
+
|
|
738
|
+
previous = IncrementalRunSlot.previous_entry(
|
|
739
|
+
store: @cache_store, configuration: @configuration,
|
|
740
|
+
target: IncrementalRunSlot::Target.new(files: @slot_carried_from, roots: roots)
|
|
741
|
+
)
|
|
742
|
+
return nil if previous.nil? || previous.snapshot != @restored_snapshot_identity
|
|
743
|
+
|
|
744
|
+
served = previous.reads.except(*@last_runner.analyzed_files)
|
|
745
|
+
[previous.baseline, previous.pinned, served.merge(rows.by_file).slice(*@analyzed)]
|
|
746
|
+
end
|
|
747
|
+
|
|
748
|
+
# One `:stat` row per analysed file, from the entry this session recorded for it (`#pack_digest`), re-packed
|
|
749
|
+
# against the file's current stat when the bytes still hash to the recorded digest but the tuple moved (a
|
|
750
|
+
# `touch`, a checkout): the snapshot keeps the old tuple, and a row carrying it would cost every later probe
|
|
751
|
+
# a re-hash of the file. nil when any file has no usable entry (it could not be read): no row could say what
|
|
752
|
+
# its answer was computed from.
|
|
753
|
+
def analysed_file_rows
|
|
754
|
+
@analyzed.map do |path|
|
|
755
|
+
packed = Cache::FileDigest.refresh_stat(path, @digests[path])
|
|
756
|
+
return nil if packed.nil?
|
|
757
|
+
|
|
758
|
+
Cache::Descriptor::FileEntry.new(path: path, comparator: :stat, value: packed)
|
|
562
759
|
end
|
|
563
760
|
end
|
|
564
761
|
|
|
762
|
+
# @param chained — the chain's baseline descriptors, owned and pinned
|
|
763
|
+
def run_slot_dependencies(analysed, rows, chained, reads)
|
|
764
|
+
carried = chained + reads
|
|
765
|
+
Cache::Descriptor.new(
|
|
766
|
+
files: analysed + rows.derived.files + rows.observed.files + carried.flat_map(&:files),
|
|
767
|
+
globs: (rows.derived.globs + rows.observed.globs + carried.flat_map(&:globs)).uniq
|
|
768
|
+
)
|
|
769
|
+
end
|
|
770
|
+
|
|
771
|
+
# What the write guard asks about besides the pinned baseline: every row the key does not pin that was not
|
|
772
|
+
# recorded as a plugin read it. A recheck's carried baseline is among them. The run that took it vouched for
|
|
773
|
+
# the tree it saw, but this run reads those inputs again (a recheck with a non-empty closure rebuilds its
|
|
774
|
+
# environment from the signature tree), and a save it read that is reverted before the run ends leaves every
|
|
775
|
+
# carried row fresh while the answer came from the saved bytes.
|
|
776
|
+
def guarded_rows(analysed, derived, baseline)
|
|
777
|
+
Cache::Descriptor.new(files: analysed + derived.files + baseline.files, globs: derived.globs + baseline.globs)
|
|
778
|
+
end
|
|
779
|
+
|
|
780
|
+
# The snapshot file's `(size, mtime_ns, ctime_ns, inode)`: which write of it a run restored, or left behind.
|
|
781
|
+
# A rewrite renames a new file into place, so any write moves it. nil when there is no file to stat.
|
|
782
|
+
def snapshot_identity(snapshot)
|
|
783
|
+
path = snapshot.respond_to?(:path) ? snapshot.path : nil
|
|
784
|
+
return nil if path.nil?
|
|
785
|
+
|
|
786
|
+
stat = File.stat(path)
|
|
787
|
+
[stat.size, Cache::FileDigest.ns_of(stat.mtime), Cache::FileDigest.ns_of(stat.ctime), stat.ino]
|
|
788
|
+
rescue SystemCallError
|
|
789
|
+
nil
|
|
790
|
+
end
|
|
791
|
+
|
|
565
792
|
# Adopt a persisted snapshot's per-file state as this session's baseline (the warm-start path).
|
|
566
793
|
def restore(payload)
|
|
567
794
|
@analyzed = payload.analyzed
|
|
@@ -723,13 +950,16 @@ module Rigor
|
|
|
723
950
|
removed.each { |path| forget(path) }
|
|
724
951
|
@analyzed = current
|
|
725
952
|
# ADR-85 WD2 — the recheck's discovery folded the restored bundles and refreshed them (changed files
|
|
726
|
-
# re-walked, removed files dropped, added files built), so adopt the runner's current set wholesale
|
|
727
|
-
|
|
953
|
+
# re-walked, removed files dropped, added files built), so adopt the runner's current set wholesale,
|
|
954
|
+
# stamping the re-walked ones with their source-RBS digest (issue #1536).
|
|
955
|
+
whole_project = current.all? { |path| analyze_set.include?(path) }
|
|
956
|
+
@seed_bundles = source_rbs_gate.stamp(runner.seed_bundles, whole_project: whole_project)
|
|
728
957
|
fresh_by_file = per_file(runner.per_file_diagnostics)
|
|
729
958
|
analyze_set.each do |path|
|
|
730
959
|
@cache[path] = fresh_by_file[path] || []
|
|
731
960
|
@digests[path] = pack_digest(path)
|
|
732
961
|
end
|
|
962
|
+
forget_unbound_digests
|
|
733
963
|
absorb_dependency_graph(runner)
|
|
734
964
|
# Issues #796 / #794 — the recheck's own snapshots already carry the replayed rows folded together
|
|
735
965
|
# with anything its closure demanded for itself, so this is the same union the next run replays.
|
|
@@ -1017,12 +1247,6 @@ module Rigor
|
|
|
1017
1247
|
TOP_LEVEL_KEY = Inference::ScopeIndexer::TOP_LEVEL_DEF_KEY
|
|
1018
1248
|
private_constant :TOP_LEVEL_KEY
|
|
1019
1249
|
|
|
1020
|
-
# B1 — plugin require-names that ingest COMMENT content as semantic input (inline-RBS reads `# @rbs` /
|
|
1021
|
-
# `#:` annotations). B1's code fingerprint ignores comments, so a project configuring one of these opts
|
|
1022
|
-
# OUT of the bundle-equality skip (a comment edit could change a cross-file type it contributes).
|
|
1023
|
-
COMMENT_INGESTING_PLUGIN_IDS = %w[rigor-rbs-inline].freeze
|
|
1024
|
-
private_constant :COMMENT_INGESTING_PLUGIN_IDS
|
|
1025
|
-
|
|
1026
1250
|
def negative_key_for(symbol)
|
|
1027
1251
|
class_name, method = symbol.split("#", 2)
|
|
1028
1252
|
class_name == TOP_LEVEL_KEY ? "toplevel:#{method}" : "method:#{symbol}"
|