rigortype 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +4 -3
- data/data/builtins/ruby_core/io.yml +1 -0
- data/data/builtins/ruby_core/proc.yml +4 -0
- data/data/builtins/ruby_core/random.yml +4 -0
- data/data/builtins/ruby_core/struct.yml +3 -0
- data/data/core_overlay/env.rbs +34 -0
- data/data/core_overlay/gc.rbs +50 -0
- data/data/core_overlay/hash.rbs +45 -0
- data/data/core_overlay/integer.rbs +31 -0
- data/data/core_overlay/io_buffer.rbs +27 -0
- data/data/core_overlay/kernel.rbs +40 -0
- data/data/core_overlay/match_data.rbs +28 -0
- data/data/core_overlay/module.rbs +51 -0
- data/data/core_overlay/object_space.rbs +29 -0
- data/data/core_overlay/proc.rbs +45 -0
- data/data/core_overlay/range.rbs +40 -0
- data/data/core_overlay/source_range.rbs +78 -0
- data/data/core_overlay/string.rbs +86 -0
- data/data/vendored_gem_sigs/prime/integer-extension.rbs +41 -0
- data/data/vendored_gem_sigs/prime/prime.rbs +372 -0
- data/docs/handbook/01-getting-started.md +3 -3
- data/docs/handbook/06-classes.md +3 -1
- data/docs/handbook/08-understanding-errors.md +3 -2
- data/docs/handbook/11-sig-gen.md +6 -0
- data/docs/handbook/appendix-liskov.md +5 -0
- data/docs/install.md +87 -15
- data/docs/manual/02-cli-reference.md +2 -2
- data/docs/manual/03-configuration.md +6 -1
- data/docs/manual/04-diagnostics.md +2 -1
- data/docs/manual/06-baseline.md +2 -0
- data/docs/manual/07-plugins.md +1 -1
- 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/docs/manual/18-removing-dead-code.md +32 -2
- data/docs/manual/plugins/README.md +6 -1
- data/docs/manual/plugins/rigor-activerecord.md +21 -0
- data/docs/manual/plugins/rigor-alba.md +60 -0
- data/docs/manual/plugins/rigor-minitest.md +22 -4
- data/docs/manual/plugins/rigor-typelizer.md +71 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +27 -3
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +21 -92
- data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +275 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +4 -0
- data/lib/rigor/analysis/check_rules/source_arity.rb +184 -61
- data/lib/rigor/analysis/check_rules.rb +407 -144
- data/lib/rigor/analysis/dependency_recorder.rb +215 -18
- data/lib/rigor/analysis/dependency_source_inference/builder.rb +8 -1
- data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +21 -0
- data/lib/rigor/analysis/dependency_source_inference/index.rb +36 -11
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +196 -27
- data/lib/rigor/analysis/erb_template_detector.rb +2 -1
- data/lib/rigor/analysis/incremental.rb +29 -9
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +291 -0
- data/lib/rigor/analysis/incremental_run_slot.rb +173 -0
- data/lib/rigor/analysis/incremental_session.rb +287 -50
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +24 -13
- data/lib/rigor/analysis/reachability/graph.rb +123 -23
- data/lib/rigor/analysis/reachability/scan.rb +290 -36
- data/lib/rigor/analysis/reachability/scan_cache.rb +2 -1
- data/lib/rigor/analysis/rule_catalog.rb +43 -0
- data/lib/rigor/analysis/run_cache_key.rb +43 -9
- data/lib/rigor/analysis/run_stats.rb +2 -2
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +88 -6
- data/lib/rigor/analysis/runner/pool_coordinator.rb +52 -13
- data/lib/rigor/analysis/runner/project_pre_passes.rb +21 -13
- data/lib/rigor/analysis/runner/run_snapshots.rb +6 -1
- data/lib/rigor/analysis/runner.rb +371 -49
- 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 +11 -7
- data/lib/rigor/cache/descriptor.rb +40 -1
- data/lib/rigor/cache/file_digest.rb +28 -0
- data/lib/rigor/cache/incremental_snapshot.rb +92 -8
- data/lib/rigor/cache/store.rb +63 -3
- data/lib/rigor/cache/value_digest.rb +325 -0
- data/lib/rigor/cli/annotate_command.rb +5 -0
- data/lib/rigor/cli/baseline_command.rb +7 -18
- data/lib/rigor/cli/check_command.rb +85 -14
- data/lib/rigor/cli/doctor_command.rb +1 -1
- data/lib/rigor/cli/effects_report.rb +2 -2
- data/lib/rigor/cli/trace_command.rb +5 -0
- data/lib/rigor/cli/type_of_command.rb +5 -3
- data/lib/rigor/cli/unused_command.rb +16 -10
- data/lib/rigor/cli.rb +11 -3
- data/lib/rigor/config_audit.rb +3 -2
- data/lib/rigor/config_path.rb +29 -0
- data/lib/rigor/configuration/severity_profile.rb +11 -0
- data/lib/rigor/configuration.rb +52 -7
- data/lib/rigor/effects/plugin_facts.rb +6 -6
- data/lib/rigor/environment/bundle_sig_discovery.rb +5 -3
- data/lib/rigor/environment/gated_signature_guard.rb +43 -0
- data/lib/rigor/environment/gated_signature_plan.rb +339 -0
- data/lib/rigor/environment/lockfile_resolver.rb +10 -1
- data/lib/rigor/environment/rbs_collection_discovery.rb +10 -1
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +284 -15
- data/lib/rigor/environment/required_features.rb +206 -0
- data/lib/rigor/environment/source_rbs_synthesis.rb +147 -0
- data/lib/rigor/environment.rb +34 -73
- data/lib/rigor/inference/acceptance.rb +28 -3
- data/lib/rigor/inference/block_parameter_binder.rb +29 -13
- data/lib/rigor/inference/builtins/cruby_definers.rb +61 -0
- data/lib/rigor/inference/builtins/method_catalog.rb +3 -0
- 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 +26 -3
- data/lib/rigor/inference/definer_resolution.rb +266 -0
- data/lib/rigor/inference/expression_typer.rb +351 -61
- data/lib/rigor/inference/external_ancestor_resolution.rb +13 -16
- data/lib/rigor/inference/global_write_census.rb +7 -4
- data/lib/rigor/inference/guard_rebinding.rb +54 -13
- data/lib/rigor/inference/in_effect_refinements/proc_literals.rb +83 -0
- data/lib/rigor/inference/in_effect_refinements/refine_defs.rb +54 -0
- data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +282 -0
- data/lib/rigor/inference/in_effect_refinements/walk.rb +276 -0
- data/lib/rigor/inference/in_effect_refinements.rb +256 -0
- data/lib/rigor/inference/key_presence_guard/scan.rb +281 -0
- data/lib/rigor/inference/key_presence_guard.rb +303 -0
- data/lib/rigor/inference/macro_block_self_type.rb +73 -14
- data/lib/rigor/inference/method_dispatcher/alias_strict_nominals.rb +36 -0
- data/lib/rigor/inference/method_dispatcher/arithmetic_sequence_elements.rb +73 -0
- data/lib/rigor/inference/method_dispatcher/imprecise_argument.rb +68 -0
- data/lib/rigor/inference/method_dispatcher/integer_step_block_params.rb +81 -0
- data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +6 -0
- data/lib/rigor/inference/method_dispatcher/keyword_arguments.rb +267 -0
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +2 -1
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +122 -105
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +305 -54
- data/lib/rigor/inference/method_dispatcher/splat_arity.rb +71 -0
- data/lib/rigor/inference/method_dispatcher.rb +44 -11
- data/lib/rigor/inference/module_function_state.rb +214 -0
- data/lib/rigor/inference/multi_target_binder.rb +21 -16
- data/lib/rigor/inference/narrowing.rb +166 -22
- data/lib/rigor/inference/object_mixins.rb +180 -0
- data/lib/rigor/inference/parameter_inference_collector.rb +10 -9
- data/lib/rigor/inference/project_patched_methods.rb +10 -5
- data/lib/rigor/inference/project_patched_scanner.rb +71 -3
- data/lib/rigor/inference/refine_census.rb +239 -0
- data/lib/rigor/inference/refine_self.rb +317 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +403 -0
- data/lib/rigor/inference/refined_dispatch.rb +272 -0
- 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 +143 -0
- data/lib/rigor/inference/scope_indexer/superclasses_collector.rb +64 -0
- data/lib/rigor/inference/scope_indexer.rb +2129 -354
- data/lib/rigor/inference/singleton_hook_decline.rb +253 -0
- data/lib/rigor/inference/statement_evaluator.rb +149 -32
- data/lib/rigor/inference/string_mutation.rb +9 -3
- data/lib/rigor/inference/synthetic_method_scanner.rb +2 -1
- data/lib/rigor/inference/version_guard.rb +19 -10
- data/lib/rigor/language_server/project_context.rb +31 -0
- data/lib/rigor/language_server/server.rb +18 -0
- data/lib/rigor/plugin/base.rb +28 -8
- data/lib/rigor/plugin/inflector.rb +1 -1
- data/lib/rigor/plugin/io_boundary.rb +124 -7
- data/lib/rigor/plugin/isolation.rb +1 -1
- data/lib/rigor/plugin/macro/block_as_method.rb +51 -8
- data/lib/rigor/plugin/trust_policy.rb +3 -3
- data/lib/rigor/project_environment.rb +29 -6
- data/lib/rigor/protection/discovery_seed.rb +18 -9
- data/lib/rigor/protection/mutation_cache.rb +4 -4
- data/lib/rigor/protection/mutator.rb +1 -1
- data/lib/rigor/reflection/constant_ancestors.rb +211 -46
- data/lib/rigor/reflection/constant_path.rb +33 -7
- data/lib/rigor/reflection.rb +41 -10
- data/lib/rigor/scope/discovery_index.rb +258 -3
- data/lib/rigor/scope/resolution_chain/relevance.rb +165 -0
- data/lib/rigor/scope/resolution_chain.rb +1032 -0
- data/lib/rigor/scope.rb +480 -328
- data/lib/rigor/sig_gen/alias_index.rb +1 -1
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/effect_annotation.rb +2 -2
- data/lib/rigor/sig_gen/generator.rb +77 -21
- data/lib/rigor/sig_gen/method_candidate.rb +2 -2
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +12 -0
- data/lib/rigor/source/alias_names.rb +52 -0
- data/lib/rigor/source/literals.rb +3 -2
- data/lib/rigor/source/symbol_name.rb +20 -0
- data/lib/rigor/type/combinator.rb +59 -3
- data/lib/rigor/type/float_range.rb +4 -1
- data/lib/rigor/type/nominal.rb +6 -1
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-ac-library-rb/lib/rigor/plugin/ac_library_rb.rb +28 -0
- data/plugins/rigor-ac-library-rb/lib/rigor-ac-library-rb.rb +3 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/convolution.rbs +7 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/data_structures.rbs +160 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/graph.rbs +94 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/math.rbs +42 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/modint.rbs +71 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/object.rbs +37 -0
- data/plugins/rigor-ac-library-rb/sig/ac_library_rb/segtree.rbs +83 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +2 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/association_roots.rb +100 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +94 -10
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +43 -2
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +3 -1
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_collector.rb +149 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +74 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_index.rb +104 -0
- data/plugins/rigor-alba/lib/rigor/plugin/alba.rb +85 -0
- data/plugins/rigor-alba/lib/rigor-alba.rb +3 -0
- data/plugins/rigor-alba/sig/alba.rbs +4 -0
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/analyzer.rb +2 -2
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +29 -12
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +13 -2
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest/assertion_analyzer.rb +130 -21
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +6 -4
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/analyzer.rb +4 -4
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/analyzer.rb +1 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +1 -1
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +31 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_collector.rb +102 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +87 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_index.rb +76 -0
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +73 -0
- data/plugins/rigor-typelizer/lib/rigor-typelizer.rb +3 -0
- data/sig/rigor/analysis/dependency_source_inference/index.rbs +1 -1
- data/sig/rigor/environment.rbs +2 -0
- data/sig/rigor/plugin/io_boundary.rbs +4 -0
- data/sig/rigor/scope.rbs +69 -4
- data/sig/rigor/source.rbs +4 -0
- data/sig/rigor.rbs +3 -1
- data/skills/rigor-project-init/SKILL.md +37 -11
- data/skills/rigor-project-init/references/02-configure.md +7 -2
- metadata +90 -1
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Analysis
|
|
5
|
+
# The `RIGOR_SHADOW_RULE_WALK` switch, read in one place by both halves of the shadow harness: the rule
|
|
6
|
+
# walk's collectors (ADR-53 Track B, {CheckRules}) and the discovery tables (ADR-116 WD5,
|
|
7
|
+
# {Inference::DeclarationWalk::Shadow}, which also says how a run must be made), and by the cache keys that
|
|
8
|
+
# must not let a warm run answer for either. A leaf file, so the ADR-87 boot-slim probe loads it without
|
|
9
|
+
# any engine code.
|
|
10
|
+
module ShadowHarness
|
|
11
|
+
ENV_KEY = "RIGOR_SHADOW_RULE_WALK"
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
# Set to anything, `0` and the empty string included.
|
|
16
|
+
def enabled?
|
|
17
|
+
!ENV[ENV_KEY].nil?
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# What the run-result cache and the incremental snapshot key on: nil with the harness off, which adds
|
|
21
|
+
# no key slot, so a run without the variable computes the key it computed before the harness existed.
|
|
22
|
+
def cache_identity
|
|
23
|
+
enabled? ? ENV_KEY : nil
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Analysis
|
|
7
|
+
# Issue #1536 (ADR-89 WD1 amendment) — what the incremental session knows about the RBS that source-RBS
|
|
8
|
+
# synthesizers derive from each file's comments. The declaration gates read a file's Ruby side only; a
|
|
9
|
+
# synthesizer's output reaches the environment as a `virtual:` buffer, and a reader of that RBS records no
|
|
10
|
+
# edge back to the file, so the session asks this gate one question before deciding a closure: did the
|
|
11
|
+
# edit move any synthesized output? A yes re-analyses the whole project.
|
|
12
|
+
#
|
|
13
|
+
# Each seed bundle carries its file's {Environment::SourceRbsSynthesis.digest} under {DIGEST_KEY}. The
|
|
14
|
+
# gate writes it ({#stamp}), not the scope indexer that builds the rest of the bundle: the digest needs a
|
|
15
|
+
# plugin registry, which discovery never sees. A stamp must describe the synthesized RBS the cached
|
|
16
|
+
# diagnostics of the file's readers were computed under; where that cannot be vouched for, it is nil, which
|
|
17
|
+
# reads as moved.
|
|
18
|
+
#
|
|
19
|
+
# The synthesizers are read from a registry the gate loads for itself, WITHOUT `#prepare`, because the
|
|
20
|
+
# closure is decided before the recheck runner (which loads and prepares its own) exists. That is exact
|
|
21
|
+
# only for a synthesizer reachable without `#prepare`, so {#verify} compares the two sets after every run:
|
|
22
|
+
# a plugin that builds its synthesizer in `#prepare` leaves the gate UNTRUSTED for the rest of the
|
|
23
|
+
# session — every stored digest reads as unknown, and every edit re-analyses the whole project.
|
|
24
|
+
class SourceRbsGate
|
|
25
|
+
DIGEST_KEY = :source_rbs_digest
|
|
26
|
+
|
|
27
|
+
# The paths {#stamp} last stored as unknown because the bytes its bundle was built from are not the
|
|
28
|
+
# bytes the closure was decided on. The session forgets their recorded content digests, so the next run
|
|
29
|
+
# detects them as changed and, reading the unknown stamp, re-analyses the project.
|
|
30
|
+
attr_reader :unbound
|
|
31
|
+
|
|
32
|
+
def initialize(configuration:, cache_store:, plugin_requirer:)
|
|
33
|
+
@configuration = configuration
|
|
34
|
+
@cache_store = cache_store
|
|
35
|
+
@plugin_requirer = plugin_requirer
|
|
36
|
+
@synthesizers = nil
|
|
37
|
+
@readings = nil
|
|
38
|
+
@unbound = Set.new.freeze
|
|
39
|
+
@untrusted = false
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# True once a run's prepared registry declared a different synthesizer set from the one the gate
|
|
43
|
+
# digests with. Sticky for the session: the configuration, and so the plugin set, is fixed for its life.
|
|
44
|
+
def untrusted?
|
|
45
|
+
@untrusted
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Whether this edit moved what any synthesizer contributes: a changed file whose digest differs from
|
|
49
|
+
# the one on its snapshot bundle (or either is unknown), an added file that contributes anything, or a
|
|
50
|
+
# removed file that did. Decided from the synthesizers' output, never from which plugins `plugins:`
|
|
51
|
+
# names. The readings taken here are kept for {#stamp}.
|
|
52
|
+
def moved?(bundles, changed, added, removed)
|
|
53
|
+
return true if @untrusted && [changed, added, removed].any?(&:any?)
|
|
54
|
+
|
|
55
|
+
@readings = (changed + added).to_h { |path| [path, reading(path)] }
|
|
56
|
+
none = Environment::SourceRbsSynthesis::NO_CONTRIBUTION
|
|
57
|
+
changed.any? { |path| !unmoved?(bundles, path) } ||
|
|
58
|
+
added.any? { |path| @readings.fetch(path).last != none } ||
|
|
59
|
+
removed.any? { |path| stored(bundles, path) != none }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Stamps every bundle that needs a digest: one the runner built this run (a baseline's all, a recheck's
|
|
63
|
+
# re-walked ones), and one carrying nil that this run can now vouch for. A bundle the runner reused keeps
|
|
64
|
+
# the digest it carries, which stays exact because a bundle is reused only for byte-identical content.
|
|
65
|
+
#
|
|
66
|
+
# A digest is vouched for only by a reading of the bytes the bundle was built from: the reading
|
|
67
|
+
# {#moved?} took when the closure was decided, or, when `whole_project` (every file was just
|
|
68
|
+
# re-analysed), one taken now. Anything else is stamped nil and listed in {#unbound}; a file saved after
|
|
69
|
+
# the closure was decided is never re-read, because its readers' cached answers predate that save. An
|
|
70
|
+
# untrusted gate stamps every bundle unknown.
|
|
71
|
+
def stamp(bundles, whole_project: false)
|
|
72
|
+
readings = @readings || {}
|
|
73
|
+
@readings = nil
|
|
74
|
+
@unbound = Set.new
|
|
75
|
+
return distrust(bundles) if @untrusted
|
|
76
|
+
|
|
77
|
+
targets = bundles.select { |path, bundle| stamp?(bundle, whole_project || readings.key?(path)) }
|
|
78
|
+
return bundles if targets.empty?
|
|
79
|
+
|
|
80
|
+
stamped = bundles.dup
|
|
81
|
+
targets.each do |path, bundle|
|
|
82
|
+
stamped[path] = bundle.merge(DIGEST_KEY => bound_digest(path, bundle, readings[path], whole_project))
|
|
83
|
+
end
|
|
84
|
+
stamped
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Compares, by plugin id and order, the synthesizers the gate digests with against those of
|
|
88
|
+
# `prepared_registry` — the registry the run's environment was built from, after `#prepare` (nil when
|
|
89
|
+
# it could not be built, which also distrusts). Runs after every run, a run that digested nothing
|
|
90
|
+
# included: a stamp written by an earlier process from the other set would otherwise be trusted. On a
|
|
91
|
+
# mismatch the gate turns untrusted and every bundle is stamped unknown. Returns the bundles to keep.
|
|
92
|
+
def verify(prepared_registry, bundles)
|
|
93
|
+
return bundles if synthesizer_ids(prepared_registry&.source_rbs_synthesizers) == synthesizer_ids(synthesizers)
|
|
94
|
+
|
|
95
|
+
@untrusted = true
|
|
96
|
+
distrust(bundles)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def unmoved?(bundles, path)
|
|
102
|
+
before = stored(bundles, path)
|
|
103
|
+
!before.nil? && before == @readings.fetch(path).last
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def stored(bundles, path)
|
|
107
|
+
bundles[path]&.fetch(DIGEST_KEY, nil)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def distrust(bundles)
|
|
111
|
+
bundles.transform_values { |bundle| bundle.merge(DIGEST_KEY => nil) }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# A bundle without a digest always needs one; one carrying nil is re-stamped only when this run can
|
|
115
|
+
# vouch for it, so an unknown stamp left by a distrust or an unbound read clears on the file's next edit
|
|
116
|
+
# or the next whole-project run.
|
|
117
|
+
def stamp?(bundle, vouched)
|
|
118
|
+
!bundle.key?(DIGEST_KEY) || (bundle[DIGEST_KEY].nil? && vouched)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# The digest for `bundle` when a reading vouches for the bytes it was built from, else nil (and the path
|
|
122
|
+
# joins {#unbound}).
|
|
123
|
+
def bound_digest(path, bundle, reading, whole_project)
|
|
124
|
+
reading ||= reading(path) if whole_project
|
|
125
|
+
return reading.last if reading && reading.first == bundle[:digest]
|
|
126
|
+
|
|
127
|
+
@unbound << path
|
|
128
|
+
nil
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# `[content SHA-256, digest]`, the SHA nil unless the file read the same before and after the digest was
|
|
132
|
+
# taken, so the pair can be matched against a bundle's content digest.
|
|
133
|
+
def reading(path)
|
|
134
|
+
before = content_sha(path)
|
|
135
|
+
digest = Environment::SourceRbsSynthesis.digest(synthesizers, path, @cache_store)
|
|
136
|
+
[before && before == content_sha(path) ? before : nil, digest]
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def content_sha(path)
|
|
140
|
+
Digest::SHA256.file(path).hexdigest
|
|
141
|
+
rescue SystemCallError
|
|
142
|
+
nil
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def synthesizers
|
|
146
|
+
@synthesizers ||= Runner::ProjectPrePasses.new(
|
|
147
|
+
configuration: @configuration, cache_store: @cache_store, buffer: nil,
|
|
148
|
+
plugin_requirer: @plugin_requirer, pool_mode: -> { false }
|
|
149
|
+
).prepared_registry(prepare: false).source_rbs_synthesizers
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def synthesizer_ids(pairs)
|
|
153
|
+
pairs&.map { |plugin, _| plugin.manifest.id.to_s }
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -21,13 +21,15 @@ module Rigor
|
|
|
21
21
|
# how an LSP publish for an open `.rbx` reported `call.unresolved-toplevel` for every helper while the
|
|
22
22
|
# unit sat in the index under its relative name). A path outside the root is left alone.
|
|
23
23
|
def relative(path, root)
|
|
24
|
-
# `
|
|
24
|
+
# `absolute_path` against the root first, so `./app/views/x.rbx`, `app/views/x.rbx` and
|
|
25
25
|
# `$ROOT/lib/../app/views/x.rbx` all reduce to the one absolute spelling before anything is
|
|
26
26
|
# compared. Without it an `--instead-of=./app/views/x.rbx` matched no unit and was parsed as plain
|
|
27
27
|
# Ruby — the spellings a shell and an editor produce are not the spelling `Dir.glob` returns.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
# `absolute_path`, not `expand_path`: a leading `~` is a directory name here (`~drafts/a.rb`), and
|
|
29
|
+
# `expand_path` reads it as a user's home and raises `ArgumentError` for a user that does not exist.
|
|
30
|
+
root_dir = File.absolute_path(root.to_s)
|
|
31
|
+
text = File.absolute_path(path.to_s, root_dir)
|
|
32
|
+
# BOTH spellings of the root. `absolute_path` does not resolve symlinks, and the root is not always
|
|
31
33
|
# already resolved: `Dir.pwd` is (which is why this was latent), but a caller may pass any path.
|
|
32
34
|
prefixes(root_dir).each do |prefix|
|
|
33
35
|
return text.delete_prefix(prefix) if text.start_with?(prefix)
|
|
@@ -190,11 +190,11 @@ module Rigor
|
|
|
190
190
|
end
|
|
191
191
|
|
|
192
192
|
def [](path)
|
|
193
|
-
|
|
193
|
+
entry_for(path)
|
|
194
194
|
end
|
|
195
195
|
|
|
196
196
|
def key?(path)
|
|
197
|
-
|
|
197
|
+
!entry_for(path).nil?
|
|
198
198
|
end
|
|
199
199
|
|
|
200
200
|
# `{ path => ruby_source }` — what {Runner#parse_source} and {WorkerSession#parse_source} read
|
|
@@ -287,7 +287,7 @@ module Rigor
|
|
|
287
287
|
# it compiles a partial's locals into the method's parameters, and it is what makes `locals:` mean
|
|
288
288
|
# anything.
|
|
289
289
|
def parse_scopes(path)
|
|
290
|
-
entry =
|
|
290
|
+
entry = entry_for(path)
|
|
291
291
|
return nil if entry.nil? || entry.locals.empty?
|
|
292
292
|
|
|
293
293
|
[entry.locals.keys.map(&:to_sym)]
|
|
@@ -301,7 +301,7 @@ module Rigor
|
|
|
301
301
|
end
|
|
302
302
|
|
|
303
303
|
def unit_key_for(path)
|
|
304
|
-
|
|
304
|
+
entry_for(path)&.unit_key
|
|
305
305
|
end
|
|
306
306
|
|
|
307
307
|
# Binds the declared `self`, locals and ivar seeds onto the per-file scope, so the unit's body types
|
|
@@ -315,7 +315,7 @@ module Rigor
|
|
|
315
315
|
# the honest reading of "a receiver is declared and the analyzer cannot see it" (ADR-5), and it is
|
|
316
316
|
# silent.
|
|
317
317
|
def seed(scope, path)
|
|
318
|
-
entry =
|
|
318
|
+
entry = entry_for(path)
|
|
319
319
|
return scope if entry.nil?
|
|
320
320
|
|
|
321
321
|
scope = bind_self(scope, entry)
|
|
@@ -369,6 +369,16 @@ module Rigor
|
|
|
369
369
|
|
|
370
370
|
private
|
|
371
371
|
|
|
372
|
+
# The unit `path` names, or nil. A run with no units answers before reducing the path: the reduction
|
|
373
|
+
# realpaths the root on every call, and every analysed file is looked up at least twice, which made 922
|
|
374
|
+
# `File.realpath` calls on a template-free `rigor check` of the v0.3.9 `lib`
|
|
375
|
+
# ([#1507](https://github.com/rigortype/rigor/issues/1507)).
|
|
376
|
+
def entry_for(path)
|
|
377
|
+
return nil if @entries.empty?
|
|
378
|
+
|
|
379
|
+
@entries[normalize(path)]
|
|
380
|
+
end
|
|
381
|
+
|
|
372
382
|
def normalize(path)
|
|
373
383
|
TemplateUnitPaths.relative(path, @root)
|
|
374
384
|
end
|
|
@@ -6,6 +6,7 @@ require_relative "../environment"
|
|
|
6
6
|
require_relative "../project_environment"
|
|
7
7
|
require_relative "../scope"
|
|
8
8
|
require_relative "../cache/store"
|
|
9
|
+
require_relative "shadow_harness"
|
|
9
10
|
require_relative "../plugin"
|
|
10
11
|
require_relative "../rbs_extended/reporter"
|
|
11
12
|
require_relative "../reflection"
|
|
@@ -85,7 +86,7 @@ module Rigor
|
|
|
85
86
|
plugin_blueprints: [], explain: false, buffer: nil,
|
|
86
87
|
synthetic_method_index: nil, project_patched_methods: nil,
|
|
87
88
|
project_scope_seed: {}, source_files: [], record_dependencies: false,
|
|
88
|
-
template_units: nil, locked_gems: nil)
|
|
89
|
+
template_units: nil, locked_gems: nil, required_features: nil)
|
|
89
90
|
@configuration = configuration
|
|
90
91
|
@cache_store = cache_store
|
|
91
92
|
@explain = explain
|
|
@@ -118,7 +119,9 @@ module Rigor
|
|
|
118
119
|
# RubyGems module state a non-main Ractor may not touch, so a Ractor worker that resolved it here got
|
|
119
120
|
# `Ractor::IsolationError` and an empty map — no gem `sig/` directories, no ADR-72 overlays. nil (the
|
|
120
121
|
# fork backend and every other caller) resolves inside `Environment.for_project` exactly as before.
|
|
121
|
-
|
|
122
|
+
# Issue #1700 — `required_features` is the coordinator's scan ({Runner::PoolCoordinator#required_features_for});
|
|
123
|
+
# nil scans `source_files` inside `Environment.for_project`.
|
|
124
|
+
@environment_inputs = { locked_gems: locked_gems, required_features: required_features }.freeze
|
|
122
125
|
|
|
123
126
|
# NOTE: `Inference::MethodDispatcher::FileFolding.fold_platform_specific_paths` is process-global
|
|
124
127
|
# state. Writing it from a non-main Ractor would raise `Ractor::IsolationError`, so the session does
|
|
@@ -152,7 +155,7 @@ module Rigor
|
|
|
152
155
|
synthetic_method_index: @synthetic_method_index,
|
|
153
156
|
project_patched_methods: @project_patched_methods,
|
|
154
157
|
source_files: @source_files,
|
|
155
|
-
|
|
158
|
+
**@environment_inputs
|
|
156
159
|
)
|
|
157
160
|
@prepare_diagnostics = run_plugin_prepare.freeze
|
|
158
161
|
end
|
|
@@ -235,7 +238,8 @@ module Rigor
|
|
|
235
238
|
scope_index: index,
|
|
236
239
|
comments: parse_result.comments,
|
|
237
240
|
disabled_rules: @configuration.disabled_rules,
|
|
238
|
-
node_collectors: node_collectors
|
|
241
|
+
node_collectors: node_collectors,
|
|
242
|
+
stated_runtime_ruby: @configuration.stated_runtime_ruby
|
|
239
243
|
)
|
|
240
244
|
diagnostics += plugin_emitted_diagnostics(path, root, scope, node_results)
|
|
241
245
|
diagnostics + explain_diagnostics(path, parse_result.value, scope)
|
|
@@ -364,12 +368,12 @@ module Rigor
|
|
|
364
368
|
def build_trust_policy
|
|
365
369
|
trusted_gems = @configuration.plugins.map { |entry| trusted_gem_name(entry) }.uniq
|
|
366
370
|
roots = [Dir.pwd]
|
|
367
|
-
Array(@configuration.signature_paths).each { |sp| roots << File.
|
|
371
|
+
Array(@configuration.signature_paths).each { |sp| roots << File.absolute_path(sp) }
|
|
368
372
|
trusted_gems.each do |gem_name|
|
|
369
373
|
path = trusted_gem_root(gem_name)
|
|
370
374
|
roots << path if path
|
|
371
375
|
end
|
|
372
|
-
@configuration.plugins_io_allowed_paths.each { |p| roots << File.
|
|
376
|
+
@configuration.plugins_io_allowed_paths.each { |p| roots << File.absolute_path(p) }
|
|
373
377
|
|
|
374
378
|
Plugin::TrustPolicy.new(
|
|
375
379
|
trusted_gems: trusted_gems,
|
|
@@ -440,7 +444,7 @@ module Rigor
|
|
|
440
444
|
results = walk.diagnostics_for_file(
|
|
441
445
|
path: path, scope: scope, root: root, collector_driver: driver
|
|
442
446
|
)
|
|
443
|
-
if
|
|
447
|
+
if ShadowHarness.enabled?
|
|
444
448
|
CheckRules.shadow_verify_converged_collectors(path, root, scope_index, node_collectors)
|
|
445
449
|
end
|
|
446
450
|
results.each_with_object({}.compare_by_identity) do |result, by_plugin|
|
|
@@ -58,7 +58,31 @@ module Rigor
|
|
|
58
58
|
# searches ahead of a class's own `def`s. A pre-10 bundle would silently contribute no prepends for an
|
|
59
59
|
# unchanged file, so that file's consumers would keep answering with the pre-fix MRO — cached bundles
|
|
60
60
|
# must read as misses once and rebuild carrying the slot.
|
|
61
|
-
|
|
61
|
+
# v11: #1558 — a plugin producer served from its own cache now replays its entry's dependency rows into
|
|
62
|
+
# the plugin's IoBoundary, so the run-result entry and the entry of any producer computed after it carry
|
|
63
|
+
# the served value's inputs. A pre-11 entry written while a producer hit lacks those rows and would
|
|
64
|
+
# validate fresh across exactly the edit they exist to catch. A checkout's source digest already moved
|
|
65
|
+
# every such key, and a release clears the root through `Store::PAYLOAD_ABI_VERSION`; the bump closes
|
|
66
|
+
# the remaining window, a same-`VERSION` build installed as `gems/rigortype-<VERSION>`, which
|
|
67
|
+
# {EngineSource} treats as version-pinned (#285) and so keys on the version alone.
|
|
68
|
+
# v12: the def-index seed bundles gain the `:unpositioned_mixins` table, the mixin edges whose order is
|
|
69
|
+
# not a fact, which an ancestor-order reader declines on. A pre-12 bundle would silently contribute no
|
|
70
|
+
# such edge for an unchanged file, so cached bundles must read as misses once and rebuild carrying it.
|
|
71
|
+
# v13/v14: #1587 — the prepend table keeps every statement (a repeated `prepend` is no longer collapsed at
|
|
72
|
+
# the freeze) and a module written to both the include and the prepend table, or a repeated `extend`, is
|
|
73
|
+
# named unpositioned. A pre-14 bundle would hold the collapsed prepend list and miss the taint for an
|
|
74
|
+
# unchanged file, so cached bundles must read as misses once and rebuild.
|
|
75
|
+
# v15: #1592 — an `extend` (or a `class << self` include) written inside a block the walk used to skip is
|
|
76
|
+
# now recorded and named unpositioned. A pre-15 bundle would hold neither for an unchanged file.
|
|
77
|
+
# v16: ADR-119 WD1 — the def-index seed bundles gain the `:siblings` Hash (the `possible_*` / `contested_*`
|
|
78
|
+
# halves of the discovery pairs). A pre-16 bundle would contribute no sibling for an unchanged file, so cached
|
|
79
|
+
# bundles must read as misses once and rebuild carrying it.
|
|
80
|
+
# v17: ADR-119 WD3 — the def-contribution producers fill the siblings (a conditional `def`'s name possible, its
|
|
81
|
+
# def-node slot and envelope contested). A pre-17 bundle carries the empty siblings of the walk before them, so
|
|
82
|
+
# an unchanged file would fold as "no possible fact" where a cold walk records one.
|
|
83
|
+
# v18: ADR-119 WD3 — the visibility walk fills `contested_discovered_method_visibilities`; a pre-18 bundle
|
|
84
|
+
# carries it empty, so an unchanged file's conditional visibility would fold as certain.
|
|
85
|
+
SCHEMA_VERSION = 18
|
|
62
86
|
|
|
63
87
|
# Per-slot entry value objects. Constructors validate enums / required fields and freeze the resulting
|
|
64
88
|
# struct so no caller can mutate after the entry is in a Descriptor.
|
|
@@ -105,6 +129,21 @@ module Rigor
|
|
|
105
129
|
new(path: path, comparator: :stat, value: packed)
|
|
106
130
|
end
|
|
107
131
|
|
|
132
|
+
# A `:stat` row whose recording instant is no later than `limit_ns`: any file whose recorded mtime is at or
|
|
133
|
+
# after `limit_ns` is then racy, and validation re-hashes it instead of trusting its stat tuple. Any other
|
|
134
|
+
# row, and one whose instant is already earlier, is returned as is.
|
|
135
|
+
def with_recording_instant_at_most(limit_ns)
|
|
136
|
+
return self unless comparator == :stat
|
|
137
|
+
|
|
138
|
+
parts = value.split
|
|
139
|
+
return self unless parts.size == 6 && Integer(parts[5], 10) > limit_ns
|
|
140
|
+
|
|
141
|
+
parts[5] = limit_ns.to_s
|
|
142
|
+
self.class.new(path: path, comparator: comparator, value: parts.join(" "))
|
|
143
|
+
rescue ArgumentError
|
|
144
|
+
self
|
|
145
|
+
end
|
|
146
|
+
|
|
108
147
|
# ADR-45 WD1 (#577) — the absence row: fresh while `path` does not exist, stale the moment anything
|
|
109
148
|
# (a file, a directory, a symlink) comes into existence there. {Plugin::IoBoundary#read_file}
|
|
110
149
|
# records one when a read fails because the path is missing, so a value computed on "X is absent"
|
|
@@ -112,6 +112,34 @@ module Rigor
|
|
|
112
112
|
hexdigest(path) == digest
|
|
113
113
|
end
|
|
114
114
|
|
|
115
|
+
# ADR-45 WD2 (#1507) — `packed` re-packed against `path`'s current stat, for a writer about to record it
|
|
116
|
+
# again: when the tuple moved (a `touch`, a checkout) or the entry is racy, but the file still hashes to the
|
|
117
|
+
# recorded digest, the new pack carries the current tuple and this run's recording instant, so the next
|
|
118
|
+
# validation is one `File::Stat` again rather than a re-hash every time.
|
|
119
|
+
#
|
|
120
|
+
# The hash is taken fresh, never from the per-run memo: a memoised digest describes the bytes as they were
|
|
121
|
+
# when change detection read them, and pairing it with a later stat would vouch for bytes written since. And
|
|
122
|
+
# the re-pack is kept only when the tuple it packed, taken after the hash, is the one taken before it: a
|
|
123
|
+
# write landing while the file is hashed moves the change time, and leaves `packed` as it was.
|
|
124
|
+
#
|
|
125
|
+
# `packed` comes back unchanged when its tuple still matches and it is not racy, and whenever the bytes no
|
|
126
|
+
# longer match the digest: a row that must stay stale is never refreshed into a fresh one. nil when `packed`
|
|
127
|
+
# is not a stat pack. A stat failure (the file is gone) returns `packed` unchanged, which then validates as
|
|
128
|
+
# stale.
|
|
129
|
+
def self.refresh_stat(path, packed)
|
|
130
|
+
parsed = parse_stat(packed)
|
|
131
|
+
return nil if parsed.nil?
|
|
132
|
+
|
|
133
|
+
before = File.stat(path)
|
|
134
|
+
return packed if !racy?(parsed) && tuple_matches?(before, parsed)
|
|
135
|
+
return packed unless Digest::SHA256.file(path).hexdigest == parsed[0]
|
|
136
|
+
|
|
137
|
+
repacked = pack_stat(path, parsed[0])
|
|
138
|
+
repacked && tuple_matches?(before, parse_stat(repacked)) ? repacked : packed
|
|
139
|
+
rescue SystemCallError
|
|
140
|
+
packed
|
|
141
|
+
end
|
|
142
|
+
|
|
115
143
|
# The content-digest field of a packed `:stat` entry ({.pack_stat}); nil when the entry is absent or not
|
|
116
144
|
# well-formed. Exposed for the caller that must compare the recorded digest against bytes it hashes
|
|
117
145
|
# itself rather than against the file on disk — the incremental session's editor-mode freshness check,
|
|
@@ -5,6 +5,11 @@ require "digest"
|
|
|
5
5
|
require "zlib"
|
|
6
6
|
|
|
7
7
|
require_relative "engine_source"
|
|
8
|
+
require_relative "store"
|
|
9
|
+
require_relative "../analysis/shadow_harness"
|
|
10
|
+
require_relative "../analysis/run_cache_key"
|
|
11
|
+
require_relative "../environment"
|
|
12
|
+
require_relative "../analysis/path_expansion"
|
|
8
13
|
|
|
9
14
|
module Rigor
|
|
10
15
|
module Cache
|
|
@@ -138,7 +143,33 @@ module Rigor
|
|
|
138
143
|
# the `write` / `to_str` / `to_int` / hatch definitions and top-level mixins it holds, which the `global.*`
|
|
139
144
|
# write rules decline on project-wide. A pre-29 bundle would fold as "none of those", and a warm run would
|
|
140
145
|
# report a write a cold run exempts.
|
|
141
|
-
|
|
146
|
+
# 30: issue #1536 gives each seed bundle a `source_rbs_digest` — the digest of what every loaded source-RBS
|
|
147
|
+
# synthesizer contributes for the file — which ADR-89 WD1 now compares beside `declaration_signature`
|
|
148
|
+
# in place of switching the gate off whenever `rigor-rbs-inline` is configured. A pre-30 bundle
|
|
149
|
+
# carries none, which the session reads as "unknown" and so as moved: sound, but every first edit to
|
|
150
|
+
# each file would re-analyse the whole project, so the gate rejects the blob and the next run is cold.
|
|
151
|
+
# 31: issue #1574 digests `plugin_fact_digest` and a seed bundle's `source_rbs_digest` with
|
|
152
|
+
# {ValueDigest} instead of over `Marshal.dump` bytes, so the same value digests alike whether a producer
|
|
153
|
+
# computed it or its cache entry served it. A pre-31 digest is in the old form and would never match, so
|
|
154
|
+
# the next run would go cold anyway; the bump makes that a clean miss instead of a "plugin fact surface
|
|
155
|
+
# changed" note that nothing changed.
|
|
156
|
+
# 32: each seed bundle gains an `unpositioned_mixins` table — the file's mixin edges whose order is not a
|
|
157
|
+
# fact (`Scope::DiscoveryIndex#unpositioned_mixins`), which an ancestor-order reader declines on. A pre-32
|
|
158
|
+
# bundle would fold as "every edge positioned", and a warm run would answer an order a cold run declines.
|
|
159
|
+
# 33: the mixin producers change what a seed bundle holds (#1587 — the prepend table keeps every statement,
|
|
160
|
+
# and a both-kind module or a repeated `extend` is named unpositioned), so a pre-33 bundle folds as the
|
|
161
|
+
# collapsed, positioned table and a warm run would answer an order a cold run declines.
|
|
162
|
+
# 34: #1592 — an `extend` or a `class << self` include inside a block the extends walk used to skip is now
|
|
163
|
+
# recorded and named unpositioned, so a pre-34 bundle folds as if the edge did not exist.
|
|
164
|
+
# 35: ADR-119 WD1 — each seed bundle gains a `siblings` Hash, the `possible_*` / `contested_*` siblings of the
|
|
165
|
+
# `Scope::DiscoveryIndex` members that may admit a possible fact. Every sibling is empty until a producer
|
|
166
|
+
# fills one, so a pre-35 bundle folds as "no possible fact", which is what it would hold today; the bump keeps
|
|
167
|
+
# that true once one does.
|
|
168
|
+
# 36: ADR-119 WD3 — the def-contribution producers fill those siblings, so a pre-36 bundle folds an unchanged
|
|
169
|
+
# file's conditional `def` as certain and a warm run would answer a definer a cold run declines.
|
|
170
|
+
# 37: ADR-119 WD3 — the visibility walk fills `contested_discovered_method_visibilities`, which a pre-37 bundle
|
|
171
|
+
# carries empty.
|
|
172
|
+
SCHEMA = 37
|
|
142
173
|
|
|
143
174
|
# The persisted per-file state.
|
|
144
175
|
# `cache` maps an analyzed file to its diagnostics.
|
|
@@ -210,8 +241,13 @@ module Rigor
|
|
|
210
241
|
# rebuild — the engine version + schema, the engine's own SOURCE when the version does not pin it, the
|
|
211
242
|
# resolved configuration, the analysis **roots** (the path arguments, e.g. `["lib"]`, NOT the expanded
|
|
212
243
|
# file list — so a snapshot is keyed to an invocation's roots but adding / removing a file under them is
|
|
213
|
-
# handled incrementally by the session, not a full rebuild), the resolved gem set (
|
|
214
|
-
#
|
|
244
|
+
# handled incrementally by the session, not a full rebuild), the resolved gem set (the lockfiles
|
|
245
|
+
# {Analysis::RunCacheKey.resolved_lockfile_paths} resolves — `bundler.lockfile:` / `rbs_collection.lockfile:`,
|
|
246
|
+
# else `./Gemfile.lock` / `./rbs_collection.lock.yaml` — the same files the run-result key digests), the
|
|
247
|
+
# project's own RBS (the `sig/` roots the environment actually loads: the explicit
|
|
248
|
+
# `signature_paths`, or the auto-detected `<root>/sig` when `signature_paths` is nil), and the contents of
|
|
249
|
+
# every `pre_eval:` file (its PATH is in the configuration, but an edit to a file outside the analysed
|
|
250
|
+
# paths changes no analysed file, so it needs its own part). Built WITHOUT
|
|
215
251
|
# constructing the RBS environment so the warm path can gate the load cheaply, before the costly env
|
|
216
252
|
# build. The `--verify-incremental` gate is the safety net for any under-capture (it would surface as an
|
|
217
253
|
# incremental-vs-full mismatch). Returns nil on any error → the caller falls back to a non-persisted run.
|
|
@@ -225,16 +261,32 @@ module Rigor
|
|
|
225
261
|
# part, so a released gem's fingerprint is byte-identical to the pre-#289 one and its warm snapshots
|
|
226
262
|
# survive the upgrade untouched.
|
|
227
263
|
def self.fingerprint(configuration:, roots:)
|
|
264
|
+
bundler_lock, collection_lock = Analysis::RunCacheKey.resolved_lockfile_paths(configuration)
|
|
228
265
|
parts = [
|
|
229
266
|
"engine:#{Rigor::VERSION}:#{SCHEMA}",
|
|
230
267
|
"config:#{Digest::SHA256.hexdigest(Marshal.dump(configuration.to_h))}",
|
|
231
268
|
"roots:#{Array(roots).map(&:to_s).sort.join("\n")}",
|
|
232
|
-
"gems:#{digest_file_if_present(
|
|
233
|
-
"rbs_collection:#{digest_file_if_present(
|
|
234
|
-
"sig:#{digest_signature_paths(configuration
|
|
269
|
+
"gems:#{digest_file_if_present(bundler_lock)}",
|
|
270
|
+
"rbs_collection:#{digest_file_if_present(collection_lock)}",
|
|
271
|
+
"sig:#{digest_signature_paths(project_sig_roots(configuration))}",
|
|
272
|
+
"pre_eval:#{digest_pre_eval(configuration.pre_eval)}"
|
|
235
273
|
]
|
|
274
|
+
parts.concat(dependency_source_parts(configuration))
|
|
236
275
|
identity = EngineSource.process_identity
|
|
237
276
|
parts << "engine-source:#{identity}" if identity
|
|
277
|
+
# Issue #1700 — the gated vendored signatures the run's source activates. A file that gains or drops a
|
|
278
|
+
# `require "prime"` changes the environment every other file was analysed against, which no per-file
|
|
279
|
+
# dependency edge records, so the whole snapshot goes. Scanned over the configured paths plus the roots'
|
|
280
|
+
# files, the set the run's environment scans; no part when nothing is required, so every other
|
|
281
|
+
# project's fingerprint is as it was.
|
|
282
|
+
features = Environment::RequiredFeatures.for_configuration(
|
|
283
|
+
configuration, Analysis::PathExpansion.ruby_files(roots, configuration.exclude_patterns)
|
|
284
|
+
)
|
|
285
|
+
parts << "required-features:#{features.join(',')}" unless features.empty?
|
|
286
|
+
# ADR-116 WD5 — the snapshot's per-file rows and seed bundles are only as checked as the run that
|
|
287
|
+
# wrote them ({Inference::DeclarationWalk::Shadow}); nil with the harness off adds no part.
|
|
288
|
+
shadow = Analysis::ShadowHarness.cache_identity
|
|
289
|
+
parts << "shadow-harness:#{shadow}" if shadow
|
|
238
290
|
Digest::SHA256.hexdigest(parts.join("\x00"))
|
|
239
291
|
rescue StandardError
|
|
240
292
|
# {EngineSource::Unavailable} lands here too, and nil is the answer it requires rather than one it
|
|
@@ -245,11 +297,41 @@ module Rigor
|
|
|
245
297
|
nil
|
|
246
298
|
end
|
|
247
299
|
|
|
300
|
+
# ADR-10 slice 3, issue #1814 — the walked gems' versions: their method catalogue, their refinement table and the
|
|
301
|
+
# constants they bind reach every file's checks, and the lockfile digest above does not cover a project with none.
|
|
302
|
+
# No part for a project that lists no gem, so its fingerprint is what it was.
|
|
303
|
+
def self.dependency_source_parts(configuration)
|
|
304
|
+
rows = Analysis::RunCacheKey.dependency_source_entries(configuration).map do |row|
|
|
305
|
+
[row.gem_name, row.gem_version, row.mode]
|
|
306
|
+
end
|
|
307
|
+
rows.empty? ? [] : ["dependency-source:#{rows.sort.inspect}"]
|
|
308
|
+
end
|
|
309
|
+
private_class_method :dependency_source_parts
|
|
310
|
+
|
|
248
311
|
def self.digest_file_if_present(path)
|
|
249
|
-
|
|
312
|
+
path = path&.to_s
|
|
313
|
+
path && File.file?(path) ? Digest::SHA256.file(path).hexdigest : "absent"
|
|
250
314
|
end
|
|
251
315
|
private_class_method :digest_file_if_present
|
|
252
316
|
|
|
317
|
+
# Content-digest every `pre_eval:` file in configuration order, so an edit to one invalidates the snapshot
|
|
318
|
+
# even when it lies outside the analysed paths. A missing file digests as "absent", so creating it counts too.
|
|
319
|
+
def self.digest_pre_eval(paths)
|
|
320
|
+
digest = Digest::SHA256.new
|
|
321
|
+
Array(paths).each do |path|
|
|
322
|
+
digest << path.to_s << "\0" << digest_file_if_present(path.to_s) << "\0"
|
|
323
|
+
end
|
|
324
|
+
digest.hexdigest
|
|
325
|
+
end
|
|
326
|
+
private_class_method :digest_pre_eval
|
|
327
|
+
|
|
328
|
+
# The `sig/` roots the environment loads for this configuration: the configured `signature_paths`, or the
|
|
329
|
+
# auto-detected `<root>/sig` when those are nil ({Environment.project_signature_roots}).
|
|
330
|
+
def self.project_sig_roots(configuration)
|
|
331
|
+
Environment.project_signature_roots(configuration.signature_paths).map(&:to_s)
|
|
332
|
+
end
|
|
333
|
+
private_class_method :project_sig_roots
|
|
334
|
+
|
|
253
335
|
# Content-digest every `.rbs` under the configured signature paths (sorted for determinism) so a project
|
|
254
336
|
# RBS edit invalidates the snapshot. Sig trees are small; content (not mtime) keeps it stable across
|
|
255
337
|
# checkouts.
|
|
@@ -351,7 +433,9 @@ module Rigor
|
|
|
351
433
|
effects_identity: payload.effects_identity,
|
|
352
434
|
run_level_rows: payload.run_level_rows
|
|
353
435
|
)
|
|
354
|
-
blob
|
|
436
|
+
# #1507 — every `--incremental` run that analysed anything rewrites this whole blob, so its deflate is
|
|
437
|
+
# on the edit path; see {Store::DEFLATE_LEVEL}.
|
|
438
|
+
blob = Zlib::Deflate.deflate(raw, Store::DEFLATE_LEVEL)
|
|
355
439
|
tmp = "#{@path}.#{Process.pid}.tmp"
|
|
356
440
|
File.binwrite(tmp, blob)
|
|
357
441
|
File.rename(tmp, @path)
|