rigortype 0.4.4 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +5 -5
- data/docs/handbook/11-sig-gen.md +90 -17
- data/docs/manual/02-cli-reference.md +39 -9
- data/docs/manual/03-configuration.md +10 -8
- data/docs/manual/04-diagnostics.md +16 -5
- data/docs/manual/05-inspecting-types.md +2 -2
- data/docs/manual/06-baseline.md +1 -1
- data/docs/manual/07-plugins.md +2 -1
- data/docs/manual/08-skills.md +7 -0
- data/docs/manual/10-mcp-server.md +8 -1
- data/docs/manual/18-removing-dead-code.md +18 -7
- data/docs/manual/plugins/rigor-rspec.md +101 -1
- data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +111 -7
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +190 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +3 -1
- data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +13 -4
- data/lib/rigor/analysis/check_rules.rb +228 -15
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental.rb +49 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
- data/lib/rigor/analysis/incremental_session.rb +166 -24
- data/lib/rigor/analysis/path_expansion.rb +3 -1
- data/lib/rigor/analysis/reachability/graph.rb +90 -13
- data/lib/rigor/analysis/reachability/scan.rb +279 -39
- data/lib/rigor/analysis/rule_catalog.rb +17 -4
- data/lib/rigor/analysis/runner/pool_coordinator.rb +69 -16
- data/lib/rigor/analysis/runner.rb +69 -65
- data/lib/rigor/analysis/severity_stamp.rb +11 -0
- data/lib/rigor/analysis/worker_session.rb +12 -1
- data/lib/rigor/cache/descriptor.rb +40 -7
- data/lib/rigor/cache/engine_source.rb +3 -2
- data/lib/rigor/cache/incremental_snapshot.rb +26 -8
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +4 -3
- data/lib/rigor/cli/command.rb +3 -1
- data/lib/rigor/cli/docs_command.rb +3 -2
- data/lib/rigor/cli/doctor_command.rb +1 -1
- data/lib/rigor/cli/explain_command.rb +3 -2
- data/lib/rigor/cli/plugin_command.rb +2 -1
- data/lib/rigor/cli/plugins_command.rb +2 -1
- data/lib/rigor/cli/sig_gen_command.rb +32 -11
- data/lib/rigor/cli/skill_command.rb +2 -1
- data/lib/rigor/cli/skill_describe.rb +3 -2
- data/lib/rigor/cli/unused_command.rb +10 -7
- data/lib/rigor/cli.rb +3 -2
- data/lib/rigor/config_audit.rb +60 -15
- data/lib/rigor/configuration/severity_profile.rb +20 -2
- data/lib/rigor/configuration.rb +29 -3
- data/lib/rigor/effects/signature_sources.rb +3 -1
- data/lib/rigor/environment/bundle_sig_discovery.rb +3 -2
- data/lib/rigor/environment/gated_signature_guard.rb +2 -1
- data/lib/rigor/environment/gated_signature_plan.rb +2 -1
- data/lib/rigor/environment/installed_gem_set.rb +2 -1
- data/lib/rigor/environment/missing_gem_constant_index.rb +4 -3
- data/lib/rigor/environment/rbs_loader.rb +21 -1
- data/lib/rigor/inference/block_call_timing.rb +7 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/captured_locals.rb +44 -13
- data/lib/rigor/inference/clause_widening.rb +90 -0
- data/lib/rigor/inference/definer_resolution.rb +87 -20
- data/lib/rigor/inference/expression_typer.rb +35 -5
- data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
- data/lib/rigor/inference/macro_block_self_type.rb +20 -6
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +170 -35
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
- data/lib/rigor/inference/method_dispatcher.rb +67 -3
- data/lib/rigor/inference/module_function_state.rb +137 -42
- data/lib/rigor/inference/narrowing.rb +54 -10
- data/lib/rigor/inference/parameter_inference_collector.rb +4 -26
- data/lib/rigor/inference/project_patched_methods.rb +168 -4
- data/lib/rigor/inference/project_patched_mixins.rb +162 -0
- data/lib/rigor/inference/project_patched_scanner.rb +51 -12
- data/lib/rigor/inference/project_patched_visibility.rb +214 -0
- data/lib/rigor/inference/project_seed_tables.rb +120 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +113 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +41 -12
- data/lib/rigor/inference/reflective_ivar_write.rb +6 -6
- data/lib/rigor/inference/return_barrier.rb +16 -0
- data/lib/rigor/inference/scope_indexer.rb +675 -105
- data/lib/rigor/inference/self_call_ivar_writes.rb +11 -6
- data/lib/rigor/inference/singleton_external_ahead.rb +64 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +7 -0
- data/lib/rigor/inference/statement_evaluator.rb +446 -139
- data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
- data/lib/rigor/lens/contract_reader.rb +45 -2
- data/lib/rigor/lens/document.rb +2 -1
- data/lib/rigor/lens/file_reader.rb +12 -2
- data/lib/rigor/lens/members.rb +16 -2
- data/lib/rigor/lens/run.rb +34 -7
- data/lib/rigor/mcp/server.rb +2 -2
- data/lib/rigor/plugin/base.rb +3 -2
- data/lib/rigor/plugin/io_boundary.rb +42 -6
- data/lib/rigor/plugin/isolation.rb +4 -3
- data/lib/rigor/protection/discovery_seed.rb +9 -36
- data/lib/rigor/protection/mutation_cache.rb +8 -2
- data/lib/rigor/rooted_glob.rb +27 -0
- data/lib/rigor/scope/discovery_index.rb +5 -0
- data/lib/rigor/scope/resolution_chain/relevance.rb +14 -0
- data/lib/rigor/scope/resolution_chain.rb +40 -5
- data/lib/rigor/scope.rb +45 -2
- data/lib/rigor/sig_gen/alias_index.rb +2 -1
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/generator.rb +167 -22
- data/lib/rigor/sig_gen/layout_index.rb +5 -2
- data/lib/rigor/sig_gen/observation_collector.rb +137 -11
- data/lib/rigor/sig_gen/renderer.rb +13 -155
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
- data/lib/rigor/sig_gen/write_reporter.rb +203 -0
- data/lib/rigor/sig_gen/write_result.rb +3 -1
- data/lib/rigor/sig_gen/writer.rb +5 -3
- data/lib/rigor/signature_path_audit.rb +4 -2
- data/lib/rigor/type/constant.rb +10 -1
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +6 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +4 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +1 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +1 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +1 -1
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -1
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +38 -9
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +1 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +7 -2
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -5
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +18 -17
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +4 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +2 -2
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +2 -2
- data/sig/rigor/plugin/io_boundary.rbs +5 -2
- data/sig/rigor/rooted_glob.rbs +7 -0
- data/sig/rigor/scope.rbs +4 -3
- data/skills/rigor-doctor/references/01-checks.md +23 -11
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
- metadata +13 -1
|
@@ -111,6 +111,13 @@ module Rigor
|
|
|
111
111
|
# so the incremental session's dependency graph is refreshed whether analysis ran sequential or pooled.
|
|
112
112
|
attr_reader :collected_dependencies
|
|
113
113
|
|
|
114
|
+
# Issue #1553 — the per-file plugin reads the fork pool's workers recorded this run (empty unless
|
|
115
|
+
# `record_dependencies:` and a pool run occurred), folded into `Runner#analysis_reads`. Each worker's are
|
|
116
|
+
# marshalled back beside its dependency records.
|
|
117
|
+
def collected_analysis_reads
|
|
118
|
+
@collected_analysis_reads ||= {}
|
|
119
|
+
end
|
|
120
|
+
|
|
114
121
|
# ADR-103 WD12 — the per-file {Effects::FileCollection}s the fork pool captured this run (empty
|
|
115
122
|
# unless the configuration carries an `effects:` block and a pool run occurred). The runner merges
|
|
116
123
|
# these with its own sequential records before the post-pool fixpoint.
|
|
@@ -197,13 +204,17 @@ module Rigor
|
|
|
197
204
|
if pool_mode?
|
|
198
205
|
dispatch_pool(files, source_files: source_files)
|
|
199
206
|
else
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
)
|
|
207
|
+
environment ||= resolve_sequential_environment(source_files: source_files)
|
|
208
|
+
analyze_files_sequentially(files, environment)
|
|
203
209
|
end
|
|
204
210
|
# Issues #796 / #794 — a narrowed run's own demand set is a SUBSET of the full run's, so the two
|
|
205
|
-
# rows below are folded in after it, whichever backend produced the per-file stream.
|
|
206
|
-
|
|
211
|
+
# rows below are folded in after it, whichever backend produced the per-file stream. Issue #1929 —
|
|
212
|
+
# the sequential run's environment re-derives a stale replayed failure; a pool run has none on
|
|
213
|
+
# this side and resolves one only when a stale failure needs it.
|
|
214
|
+
redemand_env = lambda do
|
|
215
|
+
environment ||= resolve_sequential_environment(source_files: source_files)
|
|
216
|
+
end
|
|
217
|
+
replayed_restored_run_level_rows?(closure: files, redemand_env: redemand_env)
|
|
207
218
|
result
|
|
208
219
|
end
|
|
209
220
|
|
|
@@ -261,7 +272,9 @@ module Rigor
|
|
|
261
272
|
# `env` rather than `resolve.call`: read the demand the snapshots above just made when they built
|
|
262
273
|
# an environment, and force nothing when they did not.
|
|
263
274
|
record_definition_build_failures(env&.rbs_loader&.definition_build_failures)
|
|
264
|
-
|
|
275
|
+
# An empty closure analyses no file, so only a deleted file's buffer can make a tuple stale here.
|
|
276
|
+
replayed = replayed_restored_run_level_rows?(closure: [], redemand_env: resolve)
|
|
277
|
+
record_hkt_scan_failure(hkt_scan_outcome(resolve.call)) unless replayed
|
|
265
278
|
prepare_rows
|
|
266
279
|
end
|
|
267
280
|
|
|
@@ -319,18 +332,46 @@ module Rigor
|
|
|
319
332
|
# of its own — and so no environment.
|
|
320
333
|
#
|
|
321
334
|
# @param closure — the files this run analyses; see {#stale_replayed_failure?}.
|
|
322
|
-
|
|
335
|
+
# @param redemand_env — resolves the environment {#redemand_stale_failures} re-derives a stale
|
|
336
|
+
# failure in; nil (the empty closure, which has no stale failure) re-derives nothing.
|
|
337
|
+
def replayed_restored_run_level_rows?(closure: nil, redemand_env: nil)
|
|
323
338
|
rows = @restored_run_level_rows
|
|
324
339
|
return false if rows.nil?
|
|
325
340
|
|
|
326
|
-
replayable = Array(rows.definition_build_failures).
|
|
341
|
+
stale, replayable = Array(rows.definition_build_failures).partition do |failure|
|
|
327
342
|
stale_replayed_failure?(failure, closure)
|
|
328
343
|
end
|
|
329
344
|
record_definition_build_failures(replayable)
|
|
345
|
+
redemand_stale_failures(stale, redemand_env)
|
|
330
346
|
record_hkt_scan_failure(rows.hkt_scan_failure)
|
|
331
347
|
true
|
|
332
348
|
end
|
|
333
349
|
|
|
350
|
+
# Issue #1929 — a stale replayed failure is RE-DERIVED, not dropped. Dropping it was right for the
|
|
351
|
+
# class whose collision the edit removed, and wrong for every class the edit left failing: the file
|
|
352
|
+
# that demanded such a class usually sits outside the recheck's closure (the edited file only
|
|
353
|
+
# contributes a conflicting buffer, such as a missing superclass every subclass inherits), so
|
|
354
|
+
# nothing in the recheck asks for it again, and the row lost classes a cold run reports. The class
|
|
355
|
+
# is asked for again in this run's environment ({Environment::RbsLoader#redemand_definition}), so
|
|
356
|
+
# it is reported exactly when it still fails, with the tuple the current signatures give it.
|
|
357
|
+
#
|
|
358
|
+
# A class this run's own analysis already reported is skipped; its tuple is current. The residue:
|
|
359
|
+
# a class whose only demand was in an edited file that no longer makes it, and which still fails,
|
|
360
|
+
# stays reported until a full run. The failure itself is real, unlike the dropped-collision case
|
|
361
|
+
# this replaced.
|
|
362
|
+
def redemand_stale_failures(stale, redemand_env)
|
|
363
|
+
return if stale.empty? || redemand_env.nil?
|
|
364
|
+
|
|
365
|
+
reported = @snapshots.definition_build_failures.to_set(&:first)
|
|
366
|
+
pending = stale.map(&:first).reject { |name| reported.include?(name) }
|
|
367
|
+
return if pending.empty?
|
|
368
|
+
|
|
369
|
+
loader = redemand_env.call&.rbs_loader
|
|
370
|
+
return if loader.nil?
|
|
371
|
+
|
|
372
|
+
record_definition_build_failures(pending.filter_map { |name| loader.redemand_definition(name) })
|
|
373
|
+
end
|
|
374
|
+
|
|
334
375
|
# A replayed definition-build failure the snapshot's own fingerprint cannot vouch for. The
|
|
335
376
|
# fingerprint digests the three signature sources it can read WITHOUT an environment — `sig:`,
|
|
336
377
|
# `Gemfile.lock` / `rbs_collection.lock.yaml`, and the configuration — but NOT the virtual RBS a
|
|
@@ -340,11 +381,11 @@ module Rigor
|
|
|
340
381
|
# closure, its analysis demands nothing that would re-derive the row, and the run reports a
|
|
341
382
|
# collision between files that no longer collide.
|
|
342
383
|
#
|
|
343
|
-
#
|
|
344
|
-
#
|
|
345
|
-
#
|
|
346
|
-
#
|
|
347
|
-
#
|
|
384
|
+
# Such a failure is not replayed as it stands; {#redemand_stale_failures} re-derives it instead
|
|
385
|
+
# (#1929), so the row asserts no collision the current signatures do not have. A failure whose
|
|
386
|
+
# buffers name no file this run touches still replays. A buffer whose file no longer exists is stale
|
|
387
|
+
# too: deleting an inline source a tuple names would otherwise replay the deleted declaration's
|
|
388
|
+
# failure, where a full run reports what the remaining signatures give.
|
|
348
389
|
def stale_replayed_failure?(failure, closure)
|
|
349
390
|
return false if closure.nil?
|
|
350
391
|
|
|
@@ -354,7 +395,10 @@ module Rigor
|
|
|
354
395
|
closure_paths = closure.to_set { |path| File.absolute_path(path.to_s) }
|
|
355
396
|
Array(buffers).any? do |name|
|
|
356
397
|
path = virtual_buffer_path(name)
|
|
357
|
-
|
|
398
|
+
next false if path.nil?
|
|
399
|
+
|
|
400
|
+
absolute = File.absolute_path(path)
|
|
401
|
+
closure_paths.include?(absolute) || !File.exist?(absolute)
|
|
358
402
|
end
|
|
359
403
|
end
|
|
360
404
|
|
|
@@ -870,7 +914,7 @@ module Rigor
|
|
|
870
914
|
merge_run_disclosures(session.drain_run_disclosures)
|
|
871
915
|
# The degraded slice was re-analysed on the parent session, so its dependency records live there,
|
|
872
916
|
# not in a child payload — fold them in alongside the successful children's.
|
|
873
|
-
|
|
917
|
+
collect_dependencies(session.drain_dependencies, session.drain_analysis_reads) if @record_dependencies
|
|
874
918
|
@collected_effects.merge!(session.drain_effects) if @record_effects
|
|
875
919
|
end
|
|
876
920
|
|
|
@@ -907,6 +951,7 @@ module Rigor
|
|
|
907
951
|
results = slice.to_h { |path| [path, session.analyze(path)] }
|
|
908
952
|
payload = { results: results, reporters: session.drain_reporters,
|
|
909
953
|
dependencies: session.drain_dependencies,
|
|
954
|
+
analysis_reads: session.drain_analysis_reads,
|
|
910
955
|
disclosures: session.drain_run_disclosures,
|
|
911
956
|
refusals: session.drain_trust_refusals }
|
|
912
957
|
# ADR-103 WD13 — the effects slot exists only on a collecting run, so a normal `--workers N` run
|
|
@@ -945,7 +990,9 @@ module Rigor
|
|
|
945
990
|
merge_run_disclosures(payload.fetch(:disclosures, []))
|
|
946
991
|
refusals = payload.fetch(:refusals, [])
|
|
947
992
|
refusals_by_slice ? refusals_by_slice[child[:index]] = refusals : merge_trust_refusals(refusals)
|
|
948
|
-
|
|
993
|
+
if @record_dependencies
|
|
994
|
+
collect_dependencies(payload.fetch(:dependencies, {}), payload.fetch(:analysis_reads, {}))
|
|
995
|
+
end
|
|
949
996
|
@collected_effects.merge!(payload.fetch(:effects, {})) if @record_effects
|
|
950
997
|
else
|
|
951
998
|
degraded.concat(child[:slice])
|
|
@@ -954,6 +1001,12 @@ module Rigor
|
|
|
954
1001
|
degraded
|
|
955
1002
|
end
|
|
956
1003
|
|
|
1004
|
+
# ADR-46 / issue #1553 — fold one worker's dependency records and per-file plugin reads.
|
|
1005
|
+
def collect_dependencies(dependencies, analysis_reads)
|
|
1006
|
+
@collected_dependencies.merge!(dependencies)
|
|
1007
|
+
collected_analysis_reads.merge!(analysis_reads)
|
|
1008
|
+
end
|
|
1009
|
+
|
|
957
1010
|
# @return the child's `{results:, reporters:}` payload, or nil when the child exited
|
|
958
1011
|
# abnormally or wrote no readable payload. `Marshal.load` is safe here: the blob was written by
|
|
959
1012
|
# our own forked child to a temp file we created.
|
|
@@ -26,6 +26,7 @@ require_relative "../effects/identity"
|
|
|
26
26
|
require_relative "../effects/propagator"
|
|
27
27
|
require_relative "../inference/parameter_inference_collector"
|
|
28
28
|
require_relative "../inference/pre_eval_constants"
|
|
29
|
+
require_relative "../inference/project_seed_tables"
|
|
29
30
|
require_relative "../inference/scope_indexer"
|
|
30
31
|
require_relative "../inference/synthetic_method_scanner"
|
|
31
32
|
require_relative "../inference/project_patched_scanner"
|
|
@@ -98,6 +99,16 @@ module Rigor
|
|
|
98
99
|
pooled.empty? ? @file_dependencies : @file_dependencies.merge(pooled)
|
|
99
100
|
end
|
|
100
101
|
|
|
102
|
+
# Issue #1553 — the plugin reads ({Plugin::IoBoundary.attributing}) each file's analysis made this run,
|
|
103
|
+
# `{path => [row]}`, reconciled from the sequential and pooled halves exactly as {#file_dependencies} is.
|
|
104
|
+
# Empty unless `record_dependencies: true`; a file whose analysis read nothing has no key. The incremental
|
|
105
|
+
# session keeps them beside each file's cached rows so a later recheck re-analyses the file once one of
|
|
106
|
+
# them goes stale.
|
|
107
|
+
def analysis_reads
|
|
108
|
+
pooled = @pool_coordinator.collected_analysis_reads
|
|
109
|
+
pooled.empty? ? @analysis_reads : @analysis_reads.merge(pooled)
|
|
110
|
+
end
|
|
111
|
+
|
|
101
112
|
# ADR-103 — the run's propagated effect graph, or {Effects::EffectTable.empty} when collection did
|
|
102
113
|
# not run. Reconciled from the sequential and pooled halves exactly as {#file_dependencies} is, then
|
|
103
114
|
# closed by the post-pool fixpoint in {#assemble_run_diagnostics}.
|
|
@@ -464,6 +475,9 @@ module Rigor
|
|
|
464
475
|
@project_struct_member_layouts = {}.freeze
|
|
465
476
|
# Issue #684 — set per run by {#project_discovery_expansion}; nil on every path that does not widen.
|
|
466
477
|
@project_discovery_expansion = nil
|
|
478
|
+
# Issue #1583 — whether this run widened its discovery to the configured paths, even when the widening
|
|
479
|
+
# added no file; {#discovery_glob_entries} lists the configured roots for every such run.
|
|
480
|
+
@discovery_widened = false
|
|
467
481
|
# ADR-67 WD6a — the call-site parameter-inference table, populated by the opt-in pre-pass in
|
|
468
482
|
# `assemble_run_diagnostics` when `parameter_inference:` is enabled, then seeded onto every per-file
|
|
469
483
|
# scope (sequential + fork-worker) through `project_scope_seed_tables`. Empty by default, so the gate-off
|
|
@@ -518,6 +532,7 @@ module Rigor
|
|
|
518
532
|
|
|
519
533
|
expansion = expand_paths(paths)
|
|
520
534
|
@project_discovery_expansion = project_discovery_expansion(paths, expansion)
|
|
535
|
+
@discovery_widened = widen_discovery_to_project?(paths)
|
|
521
536
|
@snapshots.reset_for_run
|
|
522
537
|
# Per-run reset of the deferred-discovery memo (see `#ensure_project_discovery`).
|
|
523
538
|
@project_discovery_done = false
|
|
@@ -599,6 +614,19 @@ module Rigor
|
|
|
599
614
|
expand_paths(paths).fetch(:files)
|
|
600
615
|
end
|
|
601
616
|
|
|
617
|
+
# Issue #1583 — the project files a run over `paths` would DISCOVER without analysing them (#684's
|
|
618
|
+
# widening), by globbing only. Empty whenever the run would not widen, which includes every run over
|
|
619
|
+
# the configured `paths:`. {IncrementalSession} digests them, because an edit to one moves the
|
|
620
|
+
# diagnostics of the files it does analyse.
|
|
621
|
+
def discovery_only_file_set(paths = @configuration.paths)
|
|
622
|
+
expansion = expand_paths(paths)
|
|
623
|
+
widened = project_discovery_expansion(paths, expansion)
|
|
624
|
+
return [] if widened.nil?
|
|
625
|
+
|
|
626
|
+
analysed = expansion.fetch(:files).to_set
|
|
627
|
+
widened.fetch(:files).reject { |path| analysed.include?(path) }
|
|
628
|
+
end
|
|
629
|
+
|
|
602
630
|
# ADR-46 §2 — inverts {#file_dependencies} into the reverse edge the incremental step walks:
|
|
603
631
|
# `dependents[X] = { A : A read a declaration / body from X }`. On an edit to X, the body tier
|
|
604
632
|
# (slice 2) re-analyses `{X} ∪ dependents[X]` and serves every other file from the per-file cache.
|
|
@@ -791,6 +819,7 @@ module Rigor
|
|
|
791
819
|
@project_discovery_done = false
|
|
792
820
|
# This entry point supplies its own expansion; it never widens (see {#project_discovery_expansion}).
|
|
793
821
|
@project_discovery_expansion = nil
|
|
822
|
+
@discovery_widened = false
|
|
794
823
|
@run_generation = Object.new.freeze
|
|
795
824
|
run_project_pre_passes(expansion: expansion)
|
|
796
825
|
ensure_project_discovery(expansion)
|
|
@@ -1391,7 +1420,7 @@ module Rigor
|
|
|
1391
1420
|
# #979 — the signature ROOTS' listings, one glob row each. `rbs_descriptor.files` covers only the
|
|
1392
1421
|
# `.rbs` files that existed while the run read them, so without these a signature file written after
|
|
1393
1422
|
# the run left the slot validating fresh and the warm run answered without it.
|
|
1394
|
-
globs = rbs_descriptor.globs + template_unit_glob_entries
|
|
1423
|
+
globs = rbs_descriptor.globs + template_unit_glob_entries + discovery_glob_entries
|
|
1395
1424
|
boundary = plugin_boundary_rows
|
|
1396
1425
|
entries.concat(boundary.files)
|
|
1397
1426
|
globs.concat(boundary.globs).concat(producer_watch_glob_entries)
|
|
@@ -1406,7 +1435,7 @@ module Rigor
|
|
|
1406
1435
|
# auto-detected `sig`: the loader reads a directory and a file differently, and #979's listing rows cover a
|
|
1407
1436
|
# file appearing under a root that existed, not the root appearing. A kind row rather than an existence row,
|
|
1408
1437
|
# because a directory replaced by a file of the same name leaves an existence row fresh. The `--incremental`
|
|
1409
|
-
# slot records
|
|
1438
|
+
# slot records kind rows for the same paths ({#incremental_slot_rows}, {#baseline_dependency_rows}, #1977).
|
|
1410
1439
|
def root_presence_entries(expansion)
|
|
1411
1440
|
(expansion.fetch(:roots) + @configuration.pre_eval + signature_root_candidates)
|
|
1412
1441
|
.uniq.map { |path| Cache::Descriptor::FileEntry.kind(path: path) }
|
|
@@ -1452,11 +1481,11 @@ module Rigor
|
|
|
1452
1481
|
# globs, which a producer replays into its boundary whether it computed or was served (#1558). A save
|
|
1453
1482
|
# after the read leaves the row stale, so no guard is needed.
|
|
1454
1483
|
# - `derived` — rows taken when the run ENDS: the template files and their claimed globs (#392, re-analysed
|
|
1455
|
-
# every run), and one
|
|
1456
|
-
# such file or directory`, `pre-eval.file-not-found`) answer to those last rows
|
|
1457
|
-
#
|
|
1458
|
-
#
|
|
1459
|
-
#
|
|
1484
|
+
# every run), and one `:kind` row per analysis root and per `pre_eval:` entry. A run's path errors (`no
|
|
1485
|
+
# such file or directory`, `not a Ruby file`, `pre-eval.file-not-found`) answer to those last rows, and the
|
|
1486
|
+
# incremental path regenerates the errors every run, so a slot that served one past the edit that retracts
|
|
1487
|
+
# it would print what no run of that tree prints. A kind row rather than an existence row (#1977): an
|
|
1488
|
+
# existence row reads a directory replaced by a file of the same name as fresh, ADR-45 WD1b's bound.
|
|
1460
1489
|
# - `by_file` — `{path => Descriptor}`, the plugin reads each file's analysis made
|
|
1461
1490
|
# ({Plugin::IoBoundary.attributing}). A narrowed run analyses only its closure, so the session carries the
|
|
1462
1491
|
# other files' reads forward from the previous slot.
|
|
@@ -1480,7 +1509,7 @@ module Rigor
|
|
|
1480
1509
|
IncrementalSlotRows.new(
|
|
1481
1510
|
observed: plugin_boundary_rows,
|
|
1482
1511
|
derived: Cache::Descriptor.new(
|
|
1483
|
-
files: template_unit_file_entries +
|
|
1512
|
+
files: template_unit_file_entries + kind_entries(Array(roots) + @configuration.pre_eval),
|
|
1484
1513
|
globs: template_unit_glob_entries
|
|
1485
1514
|
),
|
|
1486
1515
|
by_file: @analysis_reads.transform_values do |reads|
|
|
@@ -1507,13 +1536,14 @@ module Rigor
|
|
|
1507
1536
|
# which the session therefore carries unchanged from the full run that recorded them to every later slot:
|
|
1508
1537
|
#
|
|
1509
1538
|
# - the signature tree the plain slot records (`RbsDescriptor.file_entries` / `.glob_entries` over the loader
|
|
1510
|
-
# roots this run's analysis resolved), and
|
|
1511
|
-
# auto-detected `sig` — a recheck rebuilds no environment unless its closure is non-empty
|
|
1512
|
-
# fingerprint digests the same roots
|
|
1513
|
-
#
|
|
1539
|
+
# roots this run's analysis resolved), and a `:kind` row per signature root, configured or the
|
|
1540
|
+
# auto-detected `sig` — a recheck rebuilds no environment unless its closure is non-empty. The snapshot
|
|
1541
|
+
# fingerprint digests the same roots, so a change to one makes the next full-path run a full run rather
|
|
1542
|
+
# than a recheck, but the engine-free slot probe serves before any fingerprint is consulted: a root that
|
|
1543
|
+
# turns from a file into a directory (or back) is declined there by this `:kind` row alone (#1977);
|
|
1514
1544
|
# - the discovered-not-analysed files (#684) and a listing row per discovery root — a narrowed recheck does
|
|
1515
1545
|
# not widen discovery at all;
|
|
1516
|
-
# - every `pre_eval:` file outside `files` (a stat row, or
|
|
1546
|
+
# - every `pre_eval:` file outside `files` (a stat row, or a kind row) — the session tracks only analysed
|
|
1517
1547
|
# files, and one that is analysed is guarded by its own row.
|
|
1518
1548
|
#
|
|
1519
1549
|
# Recomputing any of these on a recheck would vouch for the tree the recheck saw while its answer still
|
|
@@ -1537,7 +1567,7 @@ module Rigor
|
|
|
1537
1567
|
own, pinned = signature_rows
|
|
1538
1568
|
BaselineRows.new(
|
|
1539
1569
|
owned: Cache::Descriptor.new(
|
|
1540
|
-
files: own.files +
|
|
1570
|
+
files: own.files + kind_entries(signature_root_candidates) +
|
|
1541
1571
|
discovery_file_entries({ files: files }) + unanalysed_pre_eval_entries(files),
|
|
1542
1572
|
globs: (own.globs + discovery_glob_entries).uniq
|
|
1543
1573
|
),
|
|
@@ -1563,16 +1593,18 @@ module Rigor
|
|
|
1563
1593
|
|
|
1564
1594
|
# The project's own signature roots, whether or not they exist: the configured `signature_paths:`, or the
|
|
1565
1595
|
# `sig` directory `Environment.default_signature_paths` auto-detects when none are configured. The
|
|
1566
|
-
# signature rows' listing rows cover a file appearing under a root that existed; only
|
|
1567
|
-
# covers
|
|
1596
|
+
# signature rows' listing rows cover a file appearing under a root that existed; only a row for the root
|
|
1597
|
+
# itself covers it appearing.
|
|
1568
1598
|
def signature_root_candidates
|
|
1569
1599
|
@configuration.signature_paths || ["sig"]
|
|
1570
1600
|
end
|
|
1571
1601
|
|
|
1572
1602
|
# One names-mode listing row per directory the discovery widening globbed, so a file appearing there is seen
|
|
1573
|
-
# as well as an edit to one that existed.
|
|
1603
|
+
# as well as an edit to one that existed. Issue #1583 — keyed on the widening decision, not on what it
|
|
1604
|
+
# added: a run over `lib/b.rb` alone in `lib` widens to nothing, and a `lib/a.rb` created afterwards must
|
|
1605
|
+
# still move the answer of the run-result cache and the `--incremental` slot.
|
|
1574
1606
|
def discovery_glob_entries
|
|
1575
|
-
return []
|
|
1607
|
+
return [] unless @discovery_widened
|
|
1576
1608
|
|
|
1577
1609
|
@configuration.paths.select { |root| File.directory?(root) }.map do |root|
|
|
1578
1610
|
Cache::Descriptor::GlobEntry.compute(root: root, pattern: PathExpansion::RUBY_GLOB, mode: :names)
|
|
@@ -1586,22 +1618,18 @@ module Rigor
|
|
|
1586
1618
|
if File.file?(path)
|
|
1587
1619
|
Cache::Descriptor::FileEntry.stat(path: path, digest: Cache::FileDigest.hexdigest(path))
|
|
1588
1620
|
else
|
|
1589
|
-
|
|
1621
|
+
kind_entries([path]).first
|
|
1590
1622
|
end
|
|
1591
1623
|
end
|
|
1592
1624
|
end
|
|
1593
1625
|
|
|
1594
|
-
|
|
1626
|
+
# #1977 — one `:kind` row per path, the row {#root_presence_entries} records in the plain slot.
|
|
1627
|
+
def kind_entries(paths)
|
|
1595
1628
|
paths.uniq.map do |path|
|
|
1596
|
-
|
|
1597
|
-
if File.exist?(physical)
|
|
1598
|
-
Cache::Descriptor::FileEntry.present(path: physical)
|
|
1599
|
-
else
|
|
1600
|
-
Cache::Descriptor::FileEntry.absent(path: physical)
|
|
1601
|
-
end
|
|
1629
|
+
Cache::Descriptor::FileEntry.kind(path: @buffer ? @buffer.resolve(path) : path)
|
|
1602
1630
|
end
|
|
1603
1631
|
end
|
|
1604
|
-
private :plugin_boundary_rows, :producer_watch_glob_entries, :
|
|
1632
|
+
private :plugin_boundary_rows, :producer_watch_glob_entries, :kind_entries, :root_presence_entries,
|
|
1605
1633
|
:analysis_reads_attributed?, :signature_root_candidates, :discovery_glob_entries,
|
|
1606
1634
|
:unanalysed_pre_eval_entries, :signature_rows, :within?
|
|
1607
1635
|
|
|
@@ -1825,8 +1853,14 @@ module Rigor
|
|
|
1825
1853
|
# on what else was on the command line.
|
|
1826
1854
|
#
|
|
1827
1855
|
# Excluded, each because its expansion is already the one it means: the LSP `prebuilt:` path (which
|
|
1828
|
-
# seeds discovery from a snapshot), editor `buffer:` mode, `run_source`'s in-memory single file
|
|
1829
|
-
#
|
|
1856
|
+
# seeds discovery from a snapshot), editor `buffer:` mode, and `run_source`'s in-memory single file.
|
|
1857
|
+
#
|
|
1858
|
+
# Issue #1583 — ADR-46's `analyze_only` subset mode widens like a full run. It was excluded on the
|
|
1859
|
+
# reading that it "already passes the full expansion", which holds only when the incremental session
|
|
1860
|
+
# runs over the configured `paths:`. `rigor check --incremental lib/b.rb` passes the argument's
|
|
1861
|
+
# expansion, so its rechecks (and `--verify-incremental`'s partitions) discovered the argument alone
|
|
1862
|
+
# while its baseline discovered the project, and `lib/b.rb`'s report on a sibling's constant came and
|
|
1863
|
+
# went with the run kind.
|
|
1830
1864
|
#
|
|
1831
1865
|
# Issue #1556 — the widened set holds each FILE once. `.rigor.yml`'s `paths:` load as absolute paths and a
|
|
1832
1866
|
# path argument usually does not, so `rigor check lib` widened discovery over both spellings of every file:
|
|
@@ -1864,8 +1898,7 @@ module Rigor
|
|
|
1864
1898
|
end
|
|
1865
1899
|
|
|
1866
1900
|
def widen_discovery_to_project?(paths)
|
|
1867
|
-
@prebuilt.nil? && @buffer.nil? && @in_memory_sources.nil? && @
|
|
1868
|
-
paths != @configuration.paths
|
|
1901
|
+
@prebuilt.nil? && @buffer.nil? && @in_memory_sources.nil? && paths != @configuration.paths
|
|
1869
1902
|
end
|
|
1870
1903
|
|
|
1871
1904
|
# ADR-46 — the dependency-recording and subset-analysis modes read the discovery tables OUTSIDE the
|
|
@@ -2425,50 +2458,21 @@ module Rigor
|
|
|
2425
2458
|
tables[:published_constant_names] = names unless names.empty?
|
|
2426
2459
|
end
|
|
2427
2460
|
|
|
2428
|
-
# Issue #617 — the
|
|
2429
|
-
#
|
|
2430
|
-
# segment span two files: either may load first and set the constant the other then reads, so only a
|
|
2431
|
-
# memo no other file shares keeps the idiom's reading. Files are counted expanded, because discovery can
|
|
2432
|
-
# walk one file under two spellings (`lib/b.rb`, `./lib/b.rb`).
|
|
2461
|
+
# Issue #617 / #1290 — the binding and shadowing constant names, by last segment; derived by
|
|
2462
|
+
# {Inference::ProjectSeedTables}, which the seed paths that run no `check` share.
|
|
2433
2463
|
def constant_writer_index
|
|
2434
|
-
@constant_writer_index ||=
|
|
2435
|
-
memo_files = memo_files_by_segment
|
|
2436
|
-
@project_constant_writes.each_with_object({}) do |(name, by_path), index|
|
|
2437
|
-
segment = name.split("::").last
|
|
2438
|
-
next if memo_only?(by_path) && memo_files.fetch(segment).size < 2
|
|
2439
|
-
|
|
2440
|
-
(index[segment] ||= []) << name
|
|
2441
|
-
end.each_value(&:freeze).freeze
|
|
2442
|
-
end
|
|
2464
|
+
@constant_writer_index ||= Inference::ProjectSeedTables.constant_writers(@project_constant_writes)
|
|
2443
2465
|
end
|
|
2444
2466
|
|
|
2445
|
-
# Issue #1290 — the censused names some write other than a memo `||=` assigns, grouped by last segment
|
|
2446
|
-
# (`Scope#shadowing_constant_names`): the ones that exist whether or not an outer constant of the name
|
|
2447
|
-
# does, and so stop the lexical ladder above it. A memo-only name is left out however many files memoize
|
|
2448
|
-
# it, unlike in {#constant_writer_index}: `X ||= v` resolves `X` through the very lookup whose lower rung
|
|
2449
|
-
# the stop would replace, so wherever that rung answers, the memo finds it and never creates the name.
|
|
2450
2467
|
def constant_shadower_index
|
|
2451
|
-
@constant_shadower_index ||= @project_constant_writes
|
|
2452
|
-
(index[name.split("::").last] ||= []) << name unless memo_only?(by_path)
|
|
2453
|
-
end.each_value(&:freeze).freeze
|
|
2468
|
+
@constant_shadower_index ||= Inference::ProjectSeedTables.constant_shadowers(@project_constant_writes)
|
|
2454
2469
|
end
|
|
2455
2470
|
|
|
2456
|
-
def memo_files_by_segment
|
|
2457
|
-
@project_constant_writes.each_with_object({}) do |(name, by_path), files|
|
|
2458
|
-
set = (files[name.split("::").last] ||= Set.new)
|
|
2459
|
-
by_path.each { |path, descriptor| set << File.absolute_path(path) if memo_descriptor?(descriptor) }
|
|
2460
|
-
end
|
|
2461
|
-
end
|
|
2462
|
-
|
|
2463
|
-
def memo_only?(by_path) = by_path.each_value.all? { |descriptor| memo_descriptor?(descriptor) }
|
|
2464
|
-
def memo_descriptor?(descriptor) = descriptor == Inference::ScopeIndexer::CONSTANT_MEMO
|
|
2465
|
-
|
|
2466
2471
|
# Issue #644 — the LAST SEGMENTS of the published table, the run-wide half of
|
|
2467
2472
|
# `Scope#published_constant?`. Seeded on EVERY run (unlike `constant_sources`, which only a recording
|
|
2468
2473
|
# run needs) because {CheckRules::PublishedConstantGuard} asks the question on every `if` / `unless`.
|
|
2469
2474
|
def published_constant_name_set
|
|
2470
|
-
@published_constant_name_set ||=
|
|
2471
|
-
@project_constant_values.keys.to_set { |name| name.split("::").last }.freeze
|
|
2475
|
+
@published_constant_name_set ||= Inference::ProjectSeedTables.published_constant_names(@project_constant_values)
|
|
2472
2476
|
end
|
|
2473
2477
|
|
|
2474
2478
|
# Issue #644 — the seeded `in_source_constants` table, joining the two cross-file constant producers.
|
|
@@ -19,6 +19,9 @@ module Rigor
|
|
|
19
19
|
# `plugin.<id>`, never to a bare id or `all`. The run-level rows outside the check-rule catalogue
|
|
20
20
|
# (`rbs.coverage.*`, `dynamic.dependency-source.*`, the plugin loader, `plugin_trust.*`, `pool-degraded`) keep
|
|
21
21
|
# answering to `severity_overrides:` only, by any spelling.
|
|
22
|
+
# Issue #1985 — `severity_overrides:` follows the same rule: a plugin's row answers to its qualified id or
|
|
23
|
+
# `plugin.<id>`, never to its bare rule or a family wildcard; the plugin loader's rows answer to
|
|
24
|
+
# `plugin_loader.<rule>` first and to their bare `load-error` / `runtime-error` after it.
|
|
22
25
|
#
|
|
23
26
|
# Extracted (ADR-87 WD4) so the miss path ({Runner::DiagnosticAggregator}) and the boot-slimming hit path
|
|
24
27
|
# ({RunCacheProbe}, which never loads the aggregator) apply the IDENTICAL final filter — a warm HIT's
|
|
@@ -52,11 +55,19 @@ module Rigor
|
|
|
52
55
|
diagnostic.source_family == Diagnostic::DEFAULT_SOURCE_FAMILY && CHECK_RULES.include?(diagnostic.rule)
|
|
53
56
|
end
|
|
54
57
|
|
|
58
|
+
# `plugin.<id>` for a row a plugin emitted, nil for every other row.
|
|
59
|
+
def plugin_family(diagnostic)
|
|
60
|
+
family = diagnostic.source_family.to_s
|
|
61
|
+
family if family.start_with?(CheckRules::PLUGIN_FAMILY_PREFIX)
|
|
62
|
+
end
|
|
63
|
+
|
|
55
64
|
def stamp(diagnostic, configuration)
|
|
56
65
|
return diagnostic if diagnostic.rule.nil?
|
|
57
66
|
|
|
58
67
|
resolved = Configuration::SeverityProfile.resolve(
|
|
59
68
|
rule: diagnostic.rule,
|
|
69
|
+
qualified_rule: diagnostic.qualified_rule,
|
|
70
|
+
plugin_family: plugin_family(diagnostic),
|
|
60
71
|
authored_severity: diagnostic.severity,
|
|
61
72
|
profile: configuration.severity_profile,
|
|
62
73
|
overrides: configuration.severity_overrides,
|
|
@@ -170,8 +170,13 @@ module Rigor
|
|
|
170
170
|
return analyze_with_effects(path) unless @record_dependencies
|
|
171
171
|
|
|
172
172
|
diagnostics = nil
|
|
173
|
-
|
|
173
|
+
reads = []
|
|
174
|
+
record = Plugin::IoBoundary.attributing(reads) do
|
|
175
|
+
DependencyRecorder.record_for(path) { diagnostics = analyze_with_effects(path) }
|
|
176
|
+
end
|
|
174
177
|
@file_dependencies[path] = record
|
|
178
|
+
# Issue #1553 — the plugin reads this file's analysis made, as `Runner#analyze_file` keeps them.
|
|
179
|
+
(@analysis_reads ||= {})[path] = reads unless reads.empty?
|
|
175
180
|
diagnostics
|
|
176
181
|
end
|
|
177
182
|
|
|
@@ -309,6 +314,12 @@ module Rigor
|
|
|
309
314
|
@file_dependencies
|
|
310
315
|
end
|
|
311
316
|
|
|
317
|
+
# Issue #1553 — the plugin reads ({Plugin::IoBoundary.attributing}) each `#analyze` call's file made (empty
|
|
318
|
+
# unless `record_dependencies:` was set). The rows are value objects of frozen Strings, so they marshal back.
|
|
319
|
+
def drain_analysis_reads
|
|
320
|
+
@analysis_reads || {}
|
|
321
|
+
end
|
|
322
|
+
|
|
312
323
|
# ADR-103 WD12 — the per-file {Effects::FileCollection}s this session collected (empty unless the
|
|
313
324
|
# configuration carries an `effects:` block). Marshal-clean by construction — frozen Hashes of
|
|
314
325
|
# Strings, `LabelSet`s of frozen Arrays, and `Data` edges — so the fork pool ships them back to the
|
|
@@ -4,6 +4,7 @@ require "digest"
|
|
|
4
4
|
require "json"
|
|
5
5
|
require_relative "../value_semantics"
|
|
6
6
|
require_relative "file_digest"
|
|
7
|
+
require_relative "../rooted_glob"
|
|
7
8
|
|
|
8
9
|
module Rigor
|
|
9
10
|
module Cache
|
|
@@ -87,7 +88,13 @@ module Rigor
|
|
|
87
88
|
# appears, vanishes or changes kind invalidates the warm entry. A
|
|
88
89
|
# pre-19 entry carries none of them and would validate fresh across exactly those edits, so cached
|
|
89
90
|
# entries must read as misses once and rebuild carrying the rows.
|
|
90
|
-
|
|
91
|
+
# v20: #1977 — the `--incremental` run-result slot records a `:kind` row, not an existence row, per analysis
|
|
92
|
+
# root, `pre_eval:` entry and project signature root. A pre-20 slot's existence row validates fresh across a
|
|
93
|
+
# directory replaced by a file of the same name, so it must read as a miss once and rebuild.
|
|
94
|
+
# v21: #1984 — the visibility walk records each class's singleton slots (`[:singleton, name]`) in the seed
|
|
95
|
+
# bundles' `method_visibilities`. A pre-21 bundle carries none for an unchanged file, so cached bundles must read
|
|
96
|
+
# as misses once and rebuild carrying them.
|
|
97
|
+
SCHEMA_VERSION = 21
|
|
91
98
|
|
|
92
99
|
# Per-slot entry value objects. Constructors validate enums / required fields and freeze the resulting
|
|
93
100
|
# struct so no caller can mutate after the entry is in a Descriptor.
|
|
@@ -325,7 +332,12 @@ module Rigor
|
|
|
325
332
|
# tuple moves, so a `git checkout`, a `bundle install` or a CI cache restored over a fresh checkout
|
|
326
333
|
# keeps the file rows fresh while it would move every glob signature, turning every such run into a
|
|
327
334
|
# full re-analysis. `:names` also reads no stat at all, so it costs a `Dir.glob` and nothing else.
|
|
328
|
-
|
|
335
|
+
#
|
|
336
|
+
# `:kinds` (#1959) is `:names` plus each match's `File.lstat` file type: the set moves it, and so does a
|
|
337
|
+
# path that keeps its name and changes kind (a file `gen` replaced by a directory `gen/`, a symlinked
|
|
338
|
+
# directory replaced by a real one). It reads one `lstat` per match and no content, so a `touch` or an
|
|
339
|
+
# edit leaves it fresh.
|
|
340
|
+
VALID_MODES = %i[stat names kinds].freeze
|
|
329
341
|
|
|
330
342
|
attr_reader :root, :pattern, :value
|
|
331
343
|
|
|
@@ -357,13 +369,15 @@ module Rigor
|
|
|
357
369
|
# The aggregate signature the entry's `value` carries: SHA-256 over the sorted per-file rows. In the
|
|
358
370
|
# default (`:stat`) mode a row is the file's `(path, size, mtime_ns, ctime_ns, inode)` tuple — NO
|
|
359
371
|
# content read; in strict mode a row is `(path, content-sha256)`, restoring the pre-ADR-87 authority.
|
|
360
|
-
# In `:names` mode a row is the path alone, stat-free, so only the matching SET moves the signature
|
|
372
|
+
# In `:names` mode a row is the path alone, stat-free, so only the matching SET moves the signature; in
|
|
373
|
+
# `:kinds` mode it is the path and its {.lstat_kind}.
|
|
361
374
|
# Per-file stat failures (a file vanishing between the glob and the stat) drop the row — same race
|
|
362
375
|
# posture as {Descriptor#file_entry_fresh?}. `Dir.glob` returns sorted entries by default so the row
|
|
363
376
|
# order — and therefore the signature — is stable.
|
|
364
377
|
def self.signature_for(root:, pattern:, mode: :stat)
|
|
365
|
-
paths =
|
|
378
|
+
paths = RootedGlob.glob(root, pattern)
|
|
366
379
|
return Digest::SHA256.hexdigest(paths.map { |path| "#{path}#{ROW_SEPARATOR}" }.join) if mode == :names
|
|
380
|
+
return kinds_signature(paths) if mode == :kinds
|
|
367
381
|
|
|
368
382
|
strict = FileDigest.strict_validation?
|
|
369
383
|
rows = paths.filter_map do |path|
|
|
@@ -382,10 +396,29 @@ module Rigor
|
|
|
382
396
|
Digest::SHA256.hexdigest(rows.join)
|
|
383
397
|
end
|
|
384
398
|
|
|
399
|
+
# #1959 — what `path` itself is, a symlink not followed: `:file`, `:directory`, `:symlink` or `:other`, and
|
|
400
|
+
# `:absent` for a path that vanished between the glob and the `lstat`.
|
|
401
|
+
def self.lstat_kind(path)
|
|
402
|
+
case File.lstat(path).ftype
|
|
403
|
+
when "file" then :file
|
|
404
|
+
when "directory" then :directory
|
|
405
|
+
when "link" then :symlink
|
|
406
|
+
else :other
|
|
407
|
+
end
|
|
408
|
+
rescue SystemCallError
|
|
409
|
+
:absent
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def self.kinds_signature(paths)
|
|
413
|
+
rows = paths.map { |path| "#{path}#{FIELD_SEPARATOR}#{lstat_kind(path)}#{ROW_SEPARATOR}" }
|
|
414
|
+
Digest::SHA256.hexdigest(rows.join)
|
|
415
|
+
end
|
|
416
|
+
private_class_method :kinds_signature
|
|
417
|
+
|
|
385
418
|
# ADR-87 WD2 — fresh iff re-globbing + re-stat-ing reproduces the recorded signature. Zero file-content
|
|
386
|
-
# bytes are read in the default mode,
|
|
387
|
-
# (recompute), never a crash. A stored signature recorded under the opposite
|
|
388
|
-
# mismatches and recomputes.
|
|
419
|
+
# bytes are read in the default mode, no stat at all in `:names`, and one `lstat` per match in `:kinds`.
|
|
420
|
+
# Any failure reads as stale (recompute), never a crash. A stored signature recorded under the opposite
|
|
421
|
+
# validation mode simply mismatches and recomputes.
|
|
389
422
|
def self.fresh?(entry)
|
|
390
423
|
signature_for(root: entry.root, pattern: entry.pattern, mode: entry.mode) == entry.value
|
|
391
424
|
rescue StandardError
|
|
@@ -4,6 +4,7 @@ require "digest"
|
|
|
4
4
|
|
|
5
5
|
require_relative "../version"
|
|
6
6
|
require_relative "descriptor"
|
|
7
|
+
require_relative "../rooted_glob"
|
|
7
8
|
|
|
8
9
|
module Rigor
|
|
9
10
|
module Cache
|
|
@@ -195,8 +196,8 @@ module Rigor
|
|
|
195
196
|
# Sorted, de-duplicated absolute paths of every engine `.rb` file and {DATA_GLOBS} data file, across
|
|
196
197
|
# the directories that exist. `plugins/*/sig/**/*.rbs` overlaps nothing else but is de-duplicated anyway.
|
|
197
198
|
def source_files(root)
|
|
198
|
-
sources = SOURCE_DIRECTORIES.flat_map { |relative|
|
|
199
|
-
data = DATA_GLOBS.flat_map { |pattern|
|
|
199
|
+
sources = SOURCE_DIRECTORIES.flat_map { |relative| RootedGlob.glob(root, File.join(relative, "**", "*.rb")) }
|
|
200
|
+
data = DATA_GLOBS.flat_map { |pattern| RootedGlob.glob(root, pattern) }
|
|
200
201
|
(sources + data).uniq.sort
|
|
201
202
|
end
|
|
202
203
|
end
|