rigortype 0.2.9 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/data/builtins/ruby_core/array.yml +416 -392
- data/data/builtins/ruby_core/file.yml +42 -42
- data/data/builtins/ruby_core/hash.yml +302 -302
- data/data/builtins/ruby_core/io.yml +191 -191
- data/data/builtins/ruby_core/numeric.yml +321 -366
- data/data/builtins/ruby_core/proc.yml +124 -124
- data/data/builtins/ruby_core/range.yml +21 -21
- data/data/builtins/ruby_core/rational.yml +39 -39
- data/data/builtins/ruby_core/re.yml +65 -65
- data/data/builtins/ruby_core/set.yml +106 -106
- data/data/builtins/ruby_core/struct.yml +14 -14
- data/data/core_overlay/string_scanner.rbs +6 -5
- data/docs/handbook/01-getting-started.md +22 -34
- data/docs/handbook/03-narrowing.md +2 -1
- data/docs/handbook/04-tuples-and-shapes.md +7 -3
- data/docs/handbook/06-classes.md +19 -10
- data/docs/handbook/07-rbs-and-extended.md +76 -101
- data/docs/handbook/08-understanding-errors.md +114 -246
- data/docs/handbook/09-plugins.md +54 -144
- data/docs/handbook/11-sig-gen.md +11 -1
- data/docs/handbook/README.md +5 -3
- data/docs/handbook/appendix-liskov.md +4 -2
- data/docs/handbook/appendix-mypy.md +4 -3
- data/docs/handbook/appendix-phpstan.md +14 -7
- data/docs/handbook/appendix-steep.md +4 -2
- data/docs/handbook/appendix-type-theory.md +3 -1
- data/docs/install.md +14 -6
- data/docs/manual/01-installation.md +98 -4
- data/docs/manual/02-cli-reference.md +81 -12
- data/docs/manual/03-configuration.md +34 -2
- data/docs/manual/04-diagnostics.md +59 -4
- data/docs/manual/06-baseline.md +35 -1
- data/docs/manual/07-plugins.md +4 -4
- data/docs/manual/08-skills.md +6 -1
- data/docs/manual/09-editor-integration.md +7 -6
- data/docs/manual/11-ci.md +51 -0
- data/docs/manual/12-caching.md +65 -0
- data/docs/manual/plugins/rigor-actioncable.md +32 -0
- data/docs/manual/plugins/rigor-devise.md +4 -2
- data/lib/rigor/analysis/check_rules/always_truthy_condition_collector.rb +9 -1
- data/lib/rigor/analysis/check_rules/dead_assignment_collector.rb +5 -3
- data/lib/rigor/analysis/check_rules/duplicate_hash_key_collector.rb +128 -0
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +60 -0
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +3 -2
- data/lib/rigor/analysis/check_rules/return_in_ensure_collector.rb +117 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +125 -0
- data/lib/rigor/analysis/check_rules/rule_walk.rb +2 -1
- data/lib/rigor/analysis/check_rules/self_closedness_scanner.rb +2 -1
- data/lib/rigor/analysis/check_rules/shadowed_rescue_collector.rb +283 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +10 -1
- data/lib/rigor/analysis/check_rules/void_value_use_collector.rb +112 -0
- data/lib/rigor/analysis/check_rules.rb +581 -98
- data/lib/rigor/analysis/dependency_recorder.rb +93 -9
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental_session.rb +456 -51
- data/lib/rigor/analysis/path_expansion.rb +42 -0
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +188 -0
- data/lib/rigor/analysis/rule_catalog.rb +226 -1
- data/lib/rigor/analysis/run_cache_key.rb +78 -0
- data/lib/rigor/analysis/run_cache_probe.rb +72 -0
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +181 -28
- data/lib/rigor/analysis/runner/pool_coordinator.rb +77 -15
- data/lib/rigor/analysis/runner/project_pre_passes.rb +95 -49
- data/lib/rigor/analysis/runner/run_snapshots.rb +9 -2
- data/lib/rigor/analysis/runner.rb +344 -68
- data/lib/rigor/analysis/severity_stamp.rb +43 -0
- data/lib/rigor/analysis/worker_session.rb +30 -2
- data/lib/rigor/bleeding_edge.rb +37 -8
- data/lib/rigor/builtins/regex_refinement.rb +36 -1
- data/lib/rigor/cache/descriptor.rb +88 -28
- data/lib/rigor/cache/file_digest.rb +158 -0
- data/lib/rigor/cache/incremental_snapshot.rb +41 -5
- data/lib/rigor/cache/rbs_cache_producer.rb +11 -1
- data/lib/rigor/cache/rbs_descriptor.rb +56 -10
- data/lib/rigor/cache/rbs_environment_marshal_patch.rb +38 -0
- data/lib/rigor/cache/store.rb +132 -27
- data/lib/rigor/ci_detector.rb +90 -0
- data/lib/rigor/cli/annotate_command.rb +15 -9
- data/lib/rigor/cli/baseline_command.rb +4 -1
- data/lib/rigor/cli/check_command.rb +223 -32
- data/lib/rigor/cli/check_invocation.rb +84 -0
- data/lib/rigor/cli/coverage_command.rb +6 -1
- data/lib/rigor/cli/docs_command.rb +0 -30
- data/lib/rigor/cli/doctor_command.rb +169 -31
- data/lib/rigor/cli/lsp_command.rb +5 -0
- data/lib/rigor/cli/mcp_command.rb +5 -0
- data/lib/rigor/cli/plugins_command.rb +36 -6
- data/lib/rigor/cli/plugins_renderer.rb +44 -12
- data/lib/rigor/cli/probe_environment.rb +85 -0
- data/lib/rigor/cli/sig_gen_command.rb +36 -9
- data/lib/rigor/cli/skill_command.rb +22 -33
- data/lib/rigor/cli/skill_deep_probe.rb +172 -0
- data/lib/rigor/cli/skill_describe.rb +75 -9
- data/lib/rigor/cli/trace_command.rb +5 -6
- data/lib/rigor/cli/type_of_command.rb +9 -8
- data/lib/rigor/cli/type_scan_command.rb +8 -8
- data/lib/rigor/cli.rb +15 -4
- data/lib/rigor/config_audit.rb +48 -2
- data/lib/rigor/configuration/severity_profile.rb +31 -0
- data/lib/rigor/configuration.rb +156 -8
- data/lib/rigor/environment/default_libraries.rb +36 -0
- data/lib/rigor/environment/rbs_loader.rb +164 -5
- data/lib/rigor/environment.rb +29 -28
- data/lib/rigor/inference/budget_trace.rb +77 -2
- data/lib/rigor/inference/closure_escape_analyzer.rb +14 -1
- data/lib/rigor/inference/def_handle.rb +23 -0
- data/lib/rigor/inference/def_node_resolver.rb +90 -0
- data/lib/rigor/inference/def_return_typer.rb +2 -1
- data/lib/rigor/inference/expression_typer.rb +361 -113
- data/lib/rigor/inference/method_dispatcher/data_folding.rb +30 -5
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +200 -0
- data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +22 -16
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +33 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +17 -1
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +42 -9
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +5 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +28 -35
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +40 -8
- data/lib/rigor/inference/method_dispatcher.rb +57 -1
- data/lib/rigor/inference/mutation_widening.rb +42 -8
- data/lib/rigor/inference/narrowing.rb +127 -17
- data/lib/rigor/inference/parameter_inference_collector.rb +15 -0
- data/lib/rigor/inference/project_patched_scanner.rb +2 -1
- data/lib/rigor/inference/rbs_type_translator.rb +8 -1
- data/lib/rigor/inference/scope_indexer.rb +451 -36
- data/lib/rigor/inference/statement_evaluator.rb +64 -15
- data/lib/rigor/inference/struct_fold_safety.rb +48 -11
- data/lib/rigor/inference/synthetic_method_scanner.rb +15 -14
- data/lib/rigor/inference/void_origin.rb +25 -0
- data/lib/rigor/inference/void_tail_summary.rb +220 -0
- data/lib/rigor/language_server/buffer_resolution.rb +6 -3
- data/lib/rigor/language_server/buffer_table.rb +46 -6
- data/lib/rigor/language_server/completion_provider.rb +2 -1
- data/lib/rigor/language_server/diagnostic_publisher.rb +4 -0
- data/lib/rigor/language_server/document_symbol_provider.rb +2 -1
- data/lib/rigor/language_server/folding_range_provider.rb +2 -1
- data/lib/rigor/language_server/incremental_sync.rb +159 -0
- data/lib/rigor/language_server/selection_range_provider.rb +2 -1
- data/lib/rigor/language_server/server.rb +19 -9
- data/lib/rigor/language_server/signature_help_provider.rb +2 -1
- data/lib/rigor/language_server.rb +1 -0
- data/lib/rigor/plugin/base.rb +38 -24
- data/lib/rigor/plugin/inflector.rb +12 -3
- data/lib/rigor/plugin/io_boundary.rb +5 -1
- data/lib/rigor/plugin/isolation.rb +81 -11
- data/lib/rigor/plugin/load_error.rb +10 -1
- data/lib/rigor/plugin/loader.rb +96 -14
- data/lib/rigor/plugin/node_rule_walk.rb +5 -3
- data/lib/rigor/plugin/registry.rb +32 -23
- data/lib/rigor/plugin.rb +26 -0
- data/lib/rigor/protection/mutator.rb +3 -2
- data/lib/rigor/reflection.rb +64 -0
- data/lib/rigor/runtime/jit.rb +128 -0
- data/lib/rigor/scope/discovery_index.rb +12 -2
- data/lib/rigor/scope.rb +122 -28
- data/lib/rigor/sig_gen/classification.rb +5 -1
- data/lib/rigor/sig_gen/generator.rb +38 -6
- data/lib/rigor/sig_gen/observation_collector.rb +4 -3
- data/lib/rigor/sig_gen/rbs_validity.rb +44 -0
- data/lib/rigor/sig_gen/renderer.rb +10 -0
- data/lib/rigor/sig_gen/write_result.rb +9 -4
- data/lib/rigor/sig_gen/writer.rb +207 -49
- data/lib/rigor/source/node_children.rb +116 -0
- data/lib/rigor/source/node_locator.rb +3 -1
- data/lib/rigor/source/node_walker.rb +4 -2
- data/lib/rigor/source.rb +1 -0
- data/lib/rigor/type/difference.rb +28 -24
- data/lib/rigor/type/hash_shape.rb +34 -10
- data/lib/rigor/version.rb +1 -1
- data/lib/rigortype.rb +24 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +4 -2
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +51 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +6 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +5 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +3 -1
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +4 -2
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/analyzer.rb +3 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +4 -2
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +38 -6
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +3 -1
- data/plugins/rigor-hanami/lib/rigor/plugin/hanami/action_checker.rb +3 -1
- data/plugins/rigor-mangrove/lib/rigor/plugin/mangrove.rb +10 -0
- data/plugins/rigor-minitest/lib/rigor/plugin/minitest.rb +11 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +3 -1
- data/plugins/rigor-rails/lib/rigor-rails.rb +11 -12
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/grape_api_discoverer.rb +3 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_discoverer.rb +3 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +7 -5
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +83 -9
- data/plugins/rigor-rbs-inline/lib/rigor-rbs-inline.rb +9 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/let_scope_index.rb +3 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/scope_walker.rb +3 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +11 -0
- data/plugins/rigor-rspec-rails/lib/rigor/plugin/rspec_rails/http_status_codes.rb +4 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +4 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet/catalog_walker.rb +3 -1
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +74 -14
- data/sig/prism_node_children.rbs +9 -0
- data/sig/rigor/cache.rbs +6 -0
- data/sig/rigor/inference/void_origin.rbs +18 -0
- data/sig/rigor/inference.rbs +7 -0
- data/sig/rigor/plugin/base.rbs +6 -6
- data/sig/rigor/reflection.rbs +1 -0
- data/sig/rigor/scope.rbs +12 -1
- data/skills/rigor-ci-setup/SKILL.md +10 -0
- data/skills/rigor-editor-setup/SKILL.md +11 -0
- data/skills/rigor-next-steps/SKILL.md +23 -2
- data/skills/rigor-plugin-author/SKILL.md +3 -3
- data/skills/rigor-plugin-author/references/02-walker-and-types.md +2 -2
- data/skills/rigor-plugin-review/SKILL.md +2 -2
- data/skills/rigor-plugin-review/references/01-best-practices-checklist.md +5 -4
- data/skills/rigor-project-init/SKILL.md +29 -3
- data/skills/rigor-project-init/references/01-detect.md +1 -1
- data/skills/rigor-project-init/references/02-configure.md +39 -5
- data/skills/rigor-project-init/references/03-baseline-and-bugs.md +20 -0
- data/skills/rigor-project-init/references/05-jit-performance.md +125 -0
- metadata +48 -26
- data/lib/rigor/cli/ci_detector.rb +0 -89
|
@@ -8,6 +8,9 @@ require_relative "../environment"
|
|
|
8
8
|
require_relative "../scope"
|
|
9
9
|
require_relative "../cache/store"
|
|
10
10
|
require_relative "../cache/rbs_descriptor"
|
|
11
|
+
require_relative "../cache/file_digest"
|
|
12
|
+
require_relative "run_cache_key"
|
|
13
|
+
require_relative "path_expansion"
|
|
11
14
|
require_relative "../plugin"
|
|
12
15
|
require_relative "../plugin/source_rbs_synthesis_reporter"
|
|
13
16
|
require_relative "../rbs_extended/reporter"
|
|
@@ -15,6 +18,7 @@ require_relative "../rbs_extended/conformance_checker"
|
|
|
15
18
|
require_relative "../reflection"
|
|
16
19
|
require_relative "../type/combinator"
|
|
17
20
|
require_relative "../inference/coverage_scanner"
|
|
21
|
+
require_relative "../inference/parameter_inference_collector"
|
|
18
22
|
require_relative "../inference/scope_indexer"
|
|
19
23
|
require_relative "../inference/synthetic_method_scanner"
|
|
20
24
|
require_relative "../inference/project_patched_scanner"
|
|
@@ -40,12 +44,28 @@ require_relative "runner/diagnostic_aggregator"
|
|
|
40
44
|
module Rigor
|
|
41
45
|
module Analysis
|
|
42
46
|
class Runner # rubocop:disable Metrics/ClassLength
|
|
43
|
-
RUBY_GLOB = "**/*.rb"
|
|
44
47
|
DEFAULT_CACHE_ROOT = ".rigor/cache"
|
|
45
48
|
|
|
49
|
+
# ADR-89 WD2 — bounds on the persisted return summaries (per-def observed call keys, and total defs
|
|
50
|
+
# summarised) so the incremental snapshot stays small. A def observed under more keys than the per-def
|
|
51
|
+
# cap keeps only the first; a def past the total cap is dropped (its symbol dependents then always
|
|
52
|
+
# re-check — the conservative direction).
|
|
53
|
+
RETURN_SUMMARY_KEYS_PER_DEF = 8
|
|
54
|
+
RETURN_SUMMARY_TOTAL_CAP = 4000
|
|
55
|
+
|
|
46
56
|
attr_reader :cache_store, :plugin_registry, :dependency_source_index,
|
|
47
|
-
:rbs_extended_reporter, :boundary_cross_reporter,
|
|
48
|
-
:analyzed_files, :unresolved_self_calls
|
|
57
|
+
:rbs_extended_reporter, :boundary_cross_reporter,
|
|
58
|
+
:analyzed_files, :unresolved_self_calls, :seed_bundles
|
|
59
|
+
|
|
60
|
+
# ADR-46 — the per-file cross-file read records this run captured (empty unless
|
|
61
|
+
# `record_dependencies: true`). Sequential analysis records into `@file_dependencies` via
|
|
62
|
+
# {#analyze_file}; the fork pool records per-worker and marshals the records into the coordinator, so a
|
|
63
|
+
# pooled `--incremental` recheck refreshes the same dependency graph a sequential recheck would. A run
|
|
64
|
+
# is either sequential OR pooled for a given file set, so the two maps never carry the same key.
|
|
65
|
+
def file_dependencies
|
|
66
|
+
pooled = @pool_coordinator.collected_dependencies
|
|
67
|
+
pooled.empty? ? @file_dependencies : @file_dependencies.merge(pooled)
|
|
68
|
+
end
|
|
49
69
|
|
|
50
70
|
# @param configuration [Rigor::Configuration]
|
|
51
71
|
# @param explain [Boolean] surface fail-soft fallback events as `:info` diagnostics.
|
|
@@ -79,7 +99,8 @@ module Rigor
|
|
|
79
99
|
cache_store: Cache::Store.new(root: DEFAULT_CACHE_ROOT),
|
|
80
100
|
plugin_requirer: nil, workers: 0, collect_stats: true,
|
|
81
101
|
buffer: nil, prebuilt: nil, environment: nil,
|
|
82
|
-
record_dependencies: false, record_self_calls: false, analyze_only: nil
|
|
102
|
+
record_dependencies: false, record_self_calls: false, analyze_only: nil,
|
|
103
|
+
seed_bundles: nil, collect_seed_bundles: false)
|
|
83
104
|
@configuration = configuration
|
|
84
105
|
@explain = explain
|
|
85
106
|
@cache_store = enforce_read_only_cache(cache_store, buffer)
|
|
@@ -112,6 +133,13 @@ module Rigor
|
|
|
112
133
|
# set are analyzed for diagnostics — the body tier re-analyses the affected closure and serves the
|
|
113
134
|
# rest from the per-file cache. `nil` (the default) analyzes everything.
|
|
114
135
|
@analyze_only = analyze_only && Set.new(analyze_only)
|
|
136
|
+
# ADR-85 WD2 — seed-bundle discovery. When `collect_seed_bundles`, the cross-file discovery pre-pass
|
|
137
|
+
# rebuilds from the prior run's per-file bundles (`@restored_seed_bundles`, re-walking only changed
|
|
138
|
+
# files) instead of parsing every file, and exposes the refreshed set via `#seed_bundles` for the
|
|
139
|
+
# session to persist. Off by default — a plain `rigor check` keeps today's parse+walk.
|
|
140
|
+
@collect_seed_bundles = collect_seed_bundles
|
|
141
|
+
@restored_seed_bundles = seed_bundles || {}
|
|
142
|
+
@seed_bundles = {}.freeze
|
|
115
143
|
@file_dependencies = {}
|
|
116
144
|
@plugin_registry = Plugin::Registry::EMPTY
|
|
117
145
|
@dependency_source_index = DependencySourceInference::Index::EMPTY
|
|
@@ -130,6 +158,7 @@ module Rigor
|
|
|
130
158
|
@project_discovered_def_nodes = {}.freeze
|
|
131
159
|
@project_discovered_singleton_def_nodes = {}.freeze
|
|
132
160
|
@project_discovered_def_sources = {}.freeze
|
|
161
|
+
@project_discovered_singleton_def_sources = {}.freeze
|
|
133
162
|
@project_discovered_superclasses = {}.freeze
|
|
134
163
|
@project_discovered_includes = {}.freeze
|
|
135
164
|
@project_discovered_class_sources = {}.freeze
|
|
@@ -137,6 +166,16 @@ module Rigor
|
|
|
137
166
|
@project_discovered_methods = {}.freeze
|
|
138
167
|
@project_data_member_layouts = {}.freeze
|
|
139
168
|
@project_struct_member_layouts = {}.freeze
|
|
169
|
+
# ADR-67 WD6a — the call-site parameter-inference table, populated by the opt-in pre-pass in
|
|
170
|
+
# `assemble_run_diagnostics` when `parameter_inference:` is enabled, then seeded onto every per-file
|
|
171
|
+
# scope (sequential + fork-worker) through `project_scope_seed_tables`. Empty by default, so the gate-off
|
|
172
|
+
# run carries no table and is byte-identical.
|
|
173
|
+
@project_param_inferred_types = {}.freeze
|
|
174
|
+
# ADR-84 WD2 — per-run identity token for the user-method return memo's bucket (see
|
|
175
|
+
# Scope::DiscoveryIndex#run_generation). Minted fresh in `run_analysis` so the memo never serves an
|
|
176
|
+
# entry across a run boundary (LSP re-check, ADR-62 warm loop); nil until the first run so
|
|
177
|
+
# runner-less probes keep the per-file fallback.
|
|
178
|
+
@run_generation = nil
|
|
140
179
|
build_collaborators
|
|
141
180
|
end
|
|
142
181
|
|
|
@@ -150,6 +189,19 @@ module Rigor
|
|
|
150
189
|
# errors. The Environment is built once at run start through `Environment.for_project` so all files
|
|
151
190
|
# share the same RBS load.
|
|
152
191
|
def run(paths = @configuration.paths)
|
|
192
|
+
# One per-run file-digest memo spans the whole run, so a path is SHA-256'd at most once across the
|
|
193
|
+
# run-diagnostics dependency descriptor, its `fresh?` validation, the RBS signature tree, and every
|
|
194
|
+
# plugin producer's watched-glob validation (they overlap heavily on the warm path). The nested ADR-85
|
|
195
|
+
# WD3 memo yields one stable `Prism::DefNode` per resolved bundle handle for the run (both are no-ops
|
|
196
|
+
# outside their respective consumers — an empty thread-local table).
|
|
197
|
+
# ADR-87 WD1 — `cache.validation` (or the RIGOR_STRICT_VALIDATION env, which wins) selects the
|
|
198
|
+
# freshness path for this run; the `auto` default resolves strict in CI (#190), stat-first elsewhere.
|
|
199
|
+
Cache::FileDigest.with_run(strict: @configuration.cache_validation_strict?) do
|
|
200
|
+
Inference::DefNodeResolver.with_run { run_analysis(paths) }
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def run_analysis(paths)
|
|
153
205
|
Inference::MethodDispatcher::FileFolding.fold_platform_specific_paths =
|
|
154
206
|
@configuration.fold_platform_specific_paths
|
|
155
207
|
|
|
@@ -160,6 +212,12 @@ module Rigor
|
|
|
160
212
|
|
|
161
213
|
expansion = expand_paths(paths)
|
|
162
214
|
@snapshots.reset_for_run
|
|
215
|
+
# Per-run reset of the deferred-discovery memo (see `#ensure_project_discovery`).
|
|
216
|
+
@project_discovery_done = false
|
|
217
|
+
# ADR-84 WD2 — roll the return-memo bucket: a fresh frozen token per run makes every per-file scope
|
|
218
|
+
# of THIS run share one memo bucket while entries from any earlier run in this process (stale after
|
|
219
|
+
# an edit) become unreachable.
|
|
220
|
+
@run_generation = Object.new.freeze
|
|
163
221
|
|
|
164
222
|
if @prebuilt
|
|
165
223
|
adopt_prebuilt_project_scan(@prebuilt)
|
|
@@ -167,6 +225,11 @@ module Rigor
|
|
|
167
225
|
run_project_pre_passes(expansion: expansion)
|
|
168
226
|
end
|
|
169
227
|
|
|
228
|
+
# The recording / subset (ADR-46) modes read the discovery tables outside the analysis assembly, so
|
|
229
|
+
# they force the build eagerly here (matching pre-slice-1 timing); every other mode defers it to the
|
|
230
|
+
# miss path so a warm cache HIT skips the two whole-project parse passes entirely.
|
|
231
|
+
ensure_project_discovery(expansion) if force_eager_discovery?
|
|
232
|
+
|
|
170
233
|
diagnostics = compute_run_diagnostics(expansion)
|
|
171
234
|
|
|
172
235
|
Result.new(
|
|
@@ -207,7 +270,7 @@ module Rigor
|
|
|
207
270
|
# edges are NOT inverted here: they feed the structural tier (slice 3), which re-checks a consumer
|
|
208
271
|
# when a name it looked up and did not resolve later appears.
|
|
209
272
|
def file_dependents
|
|
210
|
-
Incremental.invert(
|
|
273
|
+
Incremental.invert(file_dependencies.transform_values(&:sources))
|
|
211
274
|
end
|
|
212
275
|
|
|
213
276
|
# ADR-46 slice 4 — per-symbol body fingerprints, computed from the project pre-pass def index. Returns
|
|
@@ -219,18 +282,35 @@ module Rigor
|
|
|
219
282
|
# frozen hash before the first run.
|
|
220
283
|
def symbol_fingerprints
|
|
221
284
|
result = Hash.new { |h, k| h[k] = {} }
|
|
222
|
-
@project_discovered_def_sources
|
|
285
|
+
collect_symbol_fingerprints(result, @project_discovered_def_sources, @project_discovered_def_nodes, "#")
|
|
286
|
+
# ADR-46 slice 4 (singleton) — class/singleton-method bodies live in the parallel singleton tables the
|
|
287
|
+
# instance loop never read (recon S5). Fingerprint them under a `"Class.method"` key (the format the
|
|
288
|
+
# `singleton_def_for` dependency edge uses) so a `def self.x` body edit produces a changed pair.
|
|
289
|
+
collect_symbol_fingerprints(result, @project_discovered_singleton_def_sources,
|
|
290
|
+
@project_discovered_singleton_def_nodes, ".")
|
|
291
|
+
result.transform_values(&:freeze).freeze
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Folds one def-source/def-node table pair into the per-file fingerprint map under `separator` (`#`
|
|
295
|
+
# instance, `.` singleton). `sources` supplies the `"path:line"` (a `Prism::Location` hides its file);
|
|
296
|
+
# the node supplies the body fingerprint.
|
|
297
|
+
def collect_symbol_fingerprints(result, sources, nodes, separator)
|
|
298
|
+
sources.each do |class_name, methods|
|
|
223
299
|
methods.each do |method_sym, path_line|
|
|
224
300
|
path = path_line.split(":", 2).first
|
|
225
|
-
node =
|
|
301
|
+
node = nodes.dig(class_name, method_sym)
|
|
226
302
|
next unless node
|
|
227
303
|
|
|
228
|
-
|
|
229
|
-
|
|
304
|
+
# ADR-85 WD3 — on the incremental warm path an unchanged file's def is a `DefHandle` carrying the
|
|
305
|
+
# slice fingerprint captured when its bundle was built (no re-parse); a live node (cold / re-walked
|
|
306
|
+
# file) is sliced as before. This is the only value-deref consumer of the def-node table besides the
|
|
307
|
+
# three accessor choke points.
|
|
308
|
+
result[path]["#{class_name}#{separator}#{method_sym}"] =
|
|
309
|
+
node.is_a?(Inference::DefHandle) ? node.fingerprint : Digest::SHA256.hexdigest(node.location.slice)
|
|
230
310
|
end
|
|
231
311
|
end
|
|
232
|
-
result.transform_values(&:freeze).freeze
|
|
233
312
|
end
|
|
313
|
+
private :collect_symbol_fingerprints
|
|
234
314
|
|
|
235
315
|
# ADR-46 slice 3 — per-file set of the qualified class/module names declared in that file. Used to
|
|
236
316
|
# detect a class that *appeared* in an edit so a subclass whose ancestor was previously undefined
|
|
@@ -244,6 +324,113 @@ module Rigor
|
|
|
244
324
|
result.transform_values(&:freeze).freeze
|
|
245
325
|
end
|
|
246
326
|
|
|
327
|
+
# ADR-89 WD2 — the per-method observed-key return summaries the run's ADR-84 return memo just captured.
|
|
328
|
+
# For each project method with live memo entries, a bounded `{ keys:, returns:, effects: }` summary the
|
|
329
|
+
# incremental session persists: `keys` the observed `[receiver, arg_types]` type tuples, `returns` their
|
|
330
|
+
# `describe(:short)` descriptors, `effects` the content-mutated parameter positions (a caller-visible
|
|
331
|
+
# arg-flooring surface). Only meaningful right after a run populated the memo (baseline / recheck),
|
|
332
|
+
# before the bucket rolls. Keys are held live here; the session Marshals them for the snapshot.
|
|
333
|
+
#
|
|
334
|
+
# @return [Hash] `{ [path, "Class#method" | "Class.method"] => { keys:, returns:, effects: } }`.
|
|
335
|
+
def return_summaries
|
|
336
|
+
memo = Inference::ExpressionTyper.harvest_return_memo
|
|
337
|
+
return {} if memo.empty?
|
|
338
|
+
|
|
339
|
+
result = {}
|
|
340
|
+
effects_evaluator = Inference::StatementEvaluator.new(scope: Scope.empty)
|
|
341
|
+
collect_return_summaries(result, @project_discovered_def_nodes, @project_discovered_def_sources,
|
|
342
|
+
"#", memo, effects_evaluator)
|
|
343
|
+
collect_return_summaries(result, @project_discovered_singleton_def_nodes,
|
|
344
|
+
@project_discovered_singleton_def_sources, ".", memo, effects_evaluator)
|
|
345
|
+
result
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# Folds one def-node/def-source table pair's memo entries into the return-summary result under
|
|
349
|
+
# `separator`. A live def node (a cold / re-walked file) is the identity every cross-file caller
|
|
350
|
+
# resolved through, so `memo[node]` holds those callers' observed keys; a {DefHandle} (an unchanged
|
|
351
|
+
# file on the warm path) never keyed a memo entry, so it is skipped (its summary is carried over from
|
|
352
|
+
# the snapshot). Bounded per def and in total so the snapshot stays small.
|
|
353
|
+
def collect_return_summaries(result, nodes, sources, separator, memo, effects_evaluator)
|
|
354
|
+
nodes.each do |class_name, methods|
|
|
355
|
+
methods.each do |method_name, node|
|
|
356
|
+
next if node.is_a?(Inference::DefHandle)
|
|
357
|
+
|
|
358
|
+
entries = memo[node]
|
|
359
|
+
next if entries.nil? || entries.empty?
|
|
360
|
+
|
|
361
|
+
path_line = sources.dig(class_name, method_name)
|
|
362
|
+
next if path_line.nil?
|
|
363
|
+
|
|
364
|
+
break if result.size >= RETURN_SUMMARY_TOTAL_CAP
|
|
365
|
+
|
|
366
|
+
capped = entries.first(RETURN_SUMMARY_KEYS_PER_DEF)
|
|
367
|
+
result[[path_line.split(":", 2).first, "#{class_name}#{separator}#{method_name}"]] = {
|
|
368
|
+
keys: capped.map { |entry| [entry.receiver, entry.arg_types] },
|
|
369
|
+
returns: capped.map { |entry| entry.result.describe(:short) },
|
|
370
|
+
effects: content_effects(effects_evaluator, node)
|
|
371
|
+
}
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# The content-mutated parameter positions of `node`, or `[]` if the computation raises (defensive: the
|
|
377
|
+
# effects surface only routes attention, never soundness — a missing set reads as "no floor").
|
|
378
|
+
def content_effects(effects_evaluator, node)
|
|
379
|
+
effects_evaluator.content_mutated_parameter_positions(node)
|
|
380
|
+
rescue StandardError
|
|
381
|
+
[]
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
# ADR-89 WD2 — re-evaluate the return type of specific project methods at previously-observed call keys,
|
|
385
|
+
# WITHOUT analyzing any file. The incremental session calls this session-side (before dispatching the
|
|
386
|
+
# recheck) to prove a declaration-stable, changed callee returns the same type at every key its baseline
|
|
387
|
+
# callers used, so those callers' re-analysis can be skipped. Builds the cross-file discovery index (the
|
|
388
|
+
# ADR-85 seed-bundle fold, re-walking only edited files) and the RBS environment (served from the cache
|
|
389
|
+
# store) exactly as a real run's setup does, then re-drives each spec's def through the ADR-84 return
|
|
390
|
+
# memo. Off any hot path — invoked only when declaration-stable changed pairs carry a persisted summary.
|
|
391
|
+
#
|
|
392
|
+
# @param paths [Array<String>, nil] analysis roots (nil → the configuration's `paths:`).
|
|
393
|
+
# @param specs [Array<Hash>] each `{ class_name:, method_name:, singleton:, keys: [[receiver, args], …] }`.
|
|
394
|
+
# @return [Hash] `{ [class_name, method_name, singleton] => [return_descriptor_or_nil, …] }`.
|
|
395
|
+
def evaluate_return_types(paths, specs)
|
|
396
|
+
return {} if specs.empty?
|
|
397
|
+
|
|
398
|
+
Cache::FileDigest.with_run(strict: @configuration.cache_validation_strict?) do
|
|
399
|
+
Inference::DefNodeResolver.with_run { evaluate_return_types_setup(paths, specs) }
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# Builds the discovery index + environment (a real run's prologue, minus per-file analysis) and
|
|
404
|
+
# re-evaluates each spec's def. Extracted so {#evaluate_return_types}'s `with_run` wrappers stay thin.
|
|
405
|
+
def evaluate_return_types_setup(paths, specs)
|
|
406
|
+
expansion = expand_paths(paths || @configuration.paths)
|
|
407
|
+
@project_discovery_done = false
|
|
408
|
+
@run_generation = Object.new.freeze
|
|
409
|
+
run_project_pre_passes(expansion: expansion)
|
|
410
|
+
ensure_project_discovery(expansion)
|
|
411
|
+
environment = @pool_coordinator.resolve_sequential_environment(source_files: target_files(expansion))
|
|
412
|
+
specs.to_h do |spec|
|
|
413
|
+
[[spec[:class_name], spec[:method_name], spec[:singleton]], evaluate_spec_returns(spec, environment)]
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# Re-evaluates one spec's def at each of its observed keys. Locates the (live) def node in the discovery
|
|
418
|
+
# index, builds a project-seeded scope, and returns one return descriptor per key (nil when the def is
|
|
419
|
+
# missing / bodyless or the memo refuses a transient result — the caller reads either as "not provably
|
|
420
|
+
# stable" and keeps the dependents).
|
|
421
|
+
def evaluate_spec_returns(spec, environment)
|
|
422
|
+
table = spec[:singleton] ? @project_discovered_singleton_def_nodes : @project_discovered_def_nodes
|
|
423
|
+
node = table.dig(spec[:class_name], spec[:method_name])
|
|
424
|
+
node = Inference::DefNodeResolver.resolve(node) if node.is_a?(Inference::DefHandle)
|
|
425
|
+
return spec[:keys].map { nil } if node.nil?
|
|
426
|
+
|
|
427
|
+
scope = seed_project_scope(Scope.empty(environment: environment, source_path: spec[:path]))
|
|
428
|
+
spec[:keys].map do |(receiver, arg_types)|
|
|
429
|
+
result = scope.user_method_return(node, receiver, arg_types)
|
|
430
|
+
result&.describe(:short)
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
|
|
247
434
|
# ADR-45 — unchanged-project fast path. Serves the whole run's (pre-severity-profile) diagnostics
|
|
248
435
|
# from one record-and-validate cache entry when every file the previous run read is unchanged,
|
|
249
436
|
# skipping the dominant per-file inference. The dependency set is collected AFTER the run (so it
|
|
@@ -255,13 +442,20 @@ module Rigor
|
|
|
255
442
|
return assemble_run_diagnostics(expansion) unless run_result_cacheable?
|
|
256
443
|
|
|
257
444
|
environment = @pool_coordinator.resolve_sequential_environment(source_files: target_files(expansion))
|
|
258
|
-
|
|
445
|
+
# Lazy-files descriptor: the cache KEY reads only `gems` + `configs`; the RBS signature-tree `files`
|
|
446
|
+
# are digested solely by `run_dependency_descriptor` on a MISS, so a warm HIT never walks the tree.
|
|
447
|
+
rbs_descriptor = if environment&.rbs_loader
|
|
448
|
+
Cache::RbsDescriptor.build_run(environment.rbs_loader)
|
|
449
|
+
else
|
|
450
|
+
Cache::Descriptor.new
|
|
451
|
+
end
|
|
259
452
|
key_descriptor = run_key_descriptor(expansion, rbs_descriptor)
|
|
260
453
|
return assemble_run_diagnostics(expansion, environment: environment) if key_descriptor.nil?
|
|
261
454
|
|
|
262
455
|
computed = false
|
|
263
456
|
diagnostics = @cache_store.fetch_or_validate(
|
|
264
|
-
producer_id:
|
|
457
|
+
producer_id: RunCacheKey::RUN_DIAGNOSTICS_PRODUCER_ID, key_descriptor: key_descriptor,
|
|
458
|
+
generation_cap: RunCacheKey::GENERATION_CAP
|
|
265
459
|
) do
|
|
266
460
|
computed = true
|
|
267
461
|
diags = assemble_run_diagnostics(expansion, environment: environment)
|
|
@@ -277,6 +471,17 @@ module Rigor
|
|
|
277
471
|
end
|
|
278
472
|
|
|
279
473
|
def assemble_run_diagnostics(expansion, environment: nil)
|
|
474
|
+
# Force the deferred cross-file discovery pre-pass on the analysis (miss) path. Memoised, so the
|
|
475
|
+
# eager force in `#run` (recording / subset modes) makes this a no-op. A warm cache HIT never calls
|
|
476
|
+
# `assemble_run_diagnostics`, so it never runs the two whole-project parse passes. Runs over the FULL
|
|
477
|
+
# expansion — subset (`analyze_only`) mode still needs the complete cross-file index (ADR-46 §2).
|
|
478
|
+
ensure_project_discovery(expansion)
|
|
479
|
+
# ADR-67 WD6a — the opt-in call-site parameter-inference pre-pass. Runs on the parent BEFORE the pool
|
|
480
|
+
# split (so every worker sees the same frozen table — the seed-before-fork determinism the discovery
|
|
481
|
+
# tables use) and only on the analysis (miss / non-cacheable) path (a warm ADR-45 cache HIT never
|
|
482
|
+
# assembles). Gate off → no-op, and `environment` is left untouched so its lazy build timing is
|
|
483
|
+
# unchanged. Returns the resolved environment so the sequential dispatch reuses it (no double build).
|
|
484
|
+
environment = seed_parameter_inference(expansion, environment)
|
|
280
485
|
diagnostics = @diagnostic_aggregator.pre_file_diagnostics(expansion)
|
|
281
486
|
# ADR-46 — record which project files this run actually analyzed (the `analyze_only` subset, or
|
|
282
487
|
# all of them). The incremental orchestrator serves every analyzed-but-not-affected file from the
|
|
@@ -284,6 +489,8 @@ module Rigor
|
|
|
284
489
|
targets = target_files(expansion)
|
|
285
490
|
@analyzed_files = targets
|
|
286
491
|
diagnostics += @pool_coordinator.analyze_files(targets, environment: environment)
|
|
492
|
+
diagnostics += @diagnostic_aggregator.rbs_quarantined_signature_diagnostics
|
|
493
|
+
diagnostics += @diagnostic_aggregator.rbs_environment_build_failed_diagnostics
|
|
287
494
|
diagnostics += @diagnostic_aggregator.rbs_synthesized_namespace_diagnostics
|
|
288
495
|
diagnostics += @diagnostic_aggregator.conforms_to_diagnostics
|
|
289
496
|
diagnostics += @diagnostic_aggregator.rbs_extended_reporter_diagnostics
|
|
@@ -291,6 +498,31 @@ module Rigor
|
|
|
291
498
|
diagnostics + @diagnostic_aggregator.source_rbs_synthesis_diagnostics
|
|
292
499
|
end
|
|
293
500
|
|
|
501
|
+
# ADR-67 WD6a — the check-walk parameter-inference pre-pass. Populates `@project_param_inferred_types`
|
|
502
|
+
# (read by `project_scope_seed_tables`) with the call-site union of every undeclared parameter, running
|
|
503
|
+
# ONE round (a single hop of call-site → param typing; the protection scan's three-round fixpoint stays a
|
|
504
|
+
# protection-surface luxury until measured). No-op unless `parameter_inference:` is enabled, so the
|
|
505
|
+
# default run pays exactly nothing here and `environment` passes through unchanged (preserving the lazy
|
|
506
|
+
# env-build timing). When enabled, it resolves the environment once — the collector types call-site
|
|
507
|
+
# arguments against the same RBS / plugin surface the check uses — and returns it so the sequential
|
|
508
|
+
# dispatch reuses that build. The whole-project file set (not the `analyze_only` subset) is scanned: the
|
|
509
|
+
# inference is cross-file (a call site in one file types a parameter in another). Fails soft — a collector
|
|
510
|
+
# error must never break a run, so the table stays empty and the run proceeds unseeded.
|
|
511
|
+
def seed_parameter_inference(expansion, environment)
|
|
512
|
+
return environment unless @configuration.parameter_inference
|
|
513
|
+
|
|
514
|
+
files = expansion.fetch(:files)
|
|
515
|
+
environment ||= @pool_coordinator.resolve_sequential_environment(source_files: files)
|
|
516
|
+
@project_param_inferred_types = Inference::ParameterInferenceCollector.collect(
|
|
517
|
+
files: files, environment: environment,
|
|
518
|
+
target_ruby: @configuration.target_ruby, max_rounds: 1, workers: @workers
|
|
519
|
+
)
|
|
520
|
+
environment
|
|
521
|
+
rescue StandardError
|
|
522
|
+
@project_param_inferred_types = {}.freeze
|
|
523
|
+
environment
|
|
524
|
+
end
|
|
525
|
+
|
|
294
526
|
# A cache hit skipped the analysis, so the per-run stats (wall split, RBS-class counts, …) were never
|
|
295
527
|
# gathered — report none rather than the stale snapshot defaults.
|
|
296
528
|
def stats_for_run(wall_started_at:, expansion:)
|
|
@@ -303,9 +535,18 @@ module Rigor
|
|
|
303
535
|
# Cacheable only for a full sequential project run with a writable cache and no per-buffer /
|
|
304
536
|
# prebuilt override — every other mode has a different result identity (pool workers read in
|
|
305
537
|
# separate processes; editor mode is per-buffer; prebuilt is the LSP path).
|
|
538
|
+
#
|
|
539
|
+
# The ADR-46 incremental modes are excluded too, now that they carry a real cache store (ADR-85
|
|
540
|
+
# WD1): a `record_dependencies` run MUST perform per-file analysis to capture the dependency
|
|
541
|
+
# graph (a cache-served run records nothing, leaving the next recheck's dependents empty —
|
|
542
|
+
# unsound), and an `analyze_only` subset run produces intentionally partial diagnostics that
|
|
543
|
+
# share the full run's result key (`run_key_descriptor` keys on the whole expansion, not the
|
|
544
|
+
# subset), so serving one as the other would manufacture a wrong result. Both instead use the
|
|
545
|
+
# store for the RBS-env + plugin-producer tiers, where the incremental win actually lives.
|
|
306
546
|
def run_result_cacheable?
|
|
307
547
|
!@cache_store.nil? && !@cache_store.read_only? &&
|
|
308
|
-
@buffer.nil? && @prebuilt.nil? && !pool_mode?
|
|
548
|
+
@buffer.nil? && @prebuilt.nil? && !pool_mode? &&
|
|
549
|
+
!@record_dependencies && @analyze_only.nil?
|
|
309
550
|
end
|
|
310
551
|
|
|
311
552
|
# Stable cache key inputs — known before the run: a digest of the resolved configuration, the engine
|
|
@@ -313,16 +554,13 @@ module Rigor
|
|
|
313
554
|
# editing one is caught by dependency validation). nil disables the cache for this run rather than
|
|
314
555
|
# risking a malformed key.
|
|
315
556
|
def run_key_descriptor(expansion, rbs_descriptor)
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
]
|
|
557
|
+
# ADR-87 WD4 — the key is built through the shared {RunCacheKey} builder the boot-slimming probe also
|
|
558
|
+
# uses, so the miss path (here, passing the loader's `rbs_descriptor.configs`) and the hit path (which
|
|
559
|
+
# reconstructs `rbs.libraries` from config) can never drift out of key agreement.
|
|
560
|
+
RunCacheKey.descriptor(
|
|
561
|
+
configuration: @configuration, files: expansion.fetch(:files),
|
|
562
|
+
explain: @explain, rbs_config_entries: rbs_descriptor.configs
|
|
323
563
|
)
|
|
324
|
-
rescue StandardError
|
|
325
|
-
nil
|
|
326
564
|
end
|
|
327
565
|
|
|
328
566
|
# Files the run actually depended on, collected AFTER it ran: every analyzed file, every RBS `sig`
|
|
@@ -343,16 +581,12 @@ module Rigor
|
|
|
343
581
|
def analyzed_file_entries(expansion)
|
|
344
582
|
expansion.fetch(:files).map do |path|
|
|
345
583
|
physical = @buffer ? @buffer.resolve(path) : path
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
)
|
|
584
|
+
# ADR-87 WD1 — validation-only dependency descriptor, so the stat-then-digest `:stat` comparator
|
|
585
|
+
# applies (the env-cache KEY files stay `:digest`).
|
|
586
|
+
Cache::Descriptor::FileEntry.stat(path: physical, digest: Cache::FileDigest.hexdigest(physical))
|
|
349
587
|
end
|
|
350
588
|
end
|
|
351
589
|
|
|
352
|
-
def config_hash_entry(key, payload)
|
|
353
|
-
Cache::Descriptor::ConfigEntry.new(key: key, value_hash: Digest::SHA256.hexdigest(payload))
|
|
354
|
-
end
|
|
355
|
-
|
|
356
590
|
# Runs every project-wide pre-pass (`load_plugins` +
|
|
357
591
|
# `plugin#prepare` + dependency-source builder +
|
|
358
592
|
# synthetic-method scanner + project-patched scanner)
|
|
@@ -399,35 +633,73 @@ module Rigor
|
|
|
399
633
|
apply_pre_passes_result(@pre_passes.adopt_prebuilt(scan))
|
|
400
634
|
end
|
|
401
635
|
|
|
402
|
-
# Internal: copies a {ProjectPrePasses::Result} bundle onto the runner's
|
|
403
|
-
#
|
|
404
|
-
#
|
|
405
|
-
#
|
|
406
|
-
#
|
|
407
|
-
|
|
636
|
+
# Internal: copies a {ProjectPrePasses::Result} bundle's EAGER (env-input) slots onto the runner's
|
|
637
|
+
# ivars, so every downstream reader (pool environment build, diagnostic aggregator) sees the same ivar
|
|
638
|
+
# surface. The cross-file discovery tables are NOT carried here — `#run` (prebuilt-less) and
|
|
639
|
+
# `adopt_prebuilt` both leave them at their frozen-empty constructor defaults, and the analysis path
|
|
640
|
+
# fills them lazily via {#ensure_project_discovery}. The prebuilt (LSP) path never fills them, matching
|
|
641
|
+
# the original adopt behaviour that seeded an empty project scope.
|
|
642
|
+
def apply_pre_passes_result(result)
|
|
408
643
|
@plugin_registry = result.plugin_registry
|
|
409
644
|
@dependency_source_index = result.dependency_source_index
|
|
410
645
|
@cached_plugin_prepare_diagnostics = result.cached_plugin_prepare_diagnostics
|
|
411
646
|
@synthetic_method_index = result.synthetic_method_index
|
|
412
647
|
@project_patched_methods = result.project_patched_methods
|
|
413
648
|
@pre_eval_diagnostics_from_scanner = result.pre_eval_diagnostics_from_scanner
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
@
|
|
421
|
-
@
|
|
422
|
-
@
|
|
423
|
-
|
|
424
|
-
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
# Internal: adopts a {ProjectPrePasses::Discovery} bundle (the two whole-project discovery passes)
|
|
652
|
+
# onto the runner's discovery ivars, in the same assignment order the original inline pre-pass used.
|
|
653
|
+
# Called only from {#ensure_project_discovery}.
|
|
654
|
+
def apply_discovery_result(discovery)
|
|
655
|
+
@project_discovered_classes = discovery.discovered_classes
|
|
656
|
+
@project_discovered_def_nodes = discovery.discovered_def_nodes
|
|
657
|
+
@project_discovered_singleton_def_nodes = discovery.discovered_singleton_def_nodes
|
|
658
|
+
@project_discovered_def_sources = discovery.discovered_def_sources
|
|
659
|
+
@project_discovered_singleton_def_sources = discovery.discovered_singleton_def_sources
|
|
660
|
+
@project_discovered_superclasses = discovery.discovered_superclasses
|
|
661
|
+
@project_discovered_includes = discovery.discovered_includes
|
|
662
|
+
@project_discovered_class_sources = discovery.discovered_class_sources
|
|
663
|
+
@project_discovered_method_visibilities = discovery.discovered_method_visibilities
|
|
664
|
+
@project_discovered_methods = discovery.discovered_methods
|
|
665
|
+
@project_data_member_layouts = discovery.data_member_layouts
|
|
666
|
+
@project_struct_member_layouts = discovery.struct_member_layouts
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
# Internal: builds the deferred cross-file discovery tables at most once per run and adopts them.
|
|
670
|
+
# Memoised on `@project_discovery_done` (reset at the start of `#run`). No-op under `@prebuilt` — the
|
|
671
|
+
# LSP path deliberately seeds an empty project scope from a snapshot that carries no discovery tables,
|
|
672
|
+
# so forcing a build there would change that contract. Called eagerly from `#run` for the recording /
|
|
673
|
+
# subset (ADR-46) modes and lazily from `#assemble_run_diagnostics` on the analysis path, so a warm
|
|
674
|
+
# cache HIT (which never assembles) never pays the double parse.
|
|
675
|
+
def ensure_project_discovery(expansion)
|
|
676
|
+
return if @prebuilt
|
|
677
|
+
return if @project_discovery_done
|
|
678
|
+
|
|
679
|
+
@project_discovery_done = true
|
|
680
|
+
if @collect_seed_bundles
|
|
681
|
+
# ADR-85 WD2 — rebuild discovery from the prior run's bundles (re-walking only changed files) and
|
|
682
|
+
# capture the refreshed bundle set for the session to persist.
|
|
683
|
+
discovery, @seed_bundles = @pre_passes.discover_from_bundles(
|
|
684
|
+
expansion: expansion, seed_bundles: @restored_seed_bundles
|
|
685
|
+
)
|
|
686
|
+
apply_discovery_result(discovery)
|
|
687
|
+
else
|
|
688
|
+
apply_discovery_result(@pre_passes.discover(expansion: expansion))
|
|
425
689
|
end
|
|
426
|
-
@project_discovered_methods = result.discovered_methods if result.discovered_methods
|
|
427
|
-
@project_data_member_layouts = result.data_member_layouts if result.data_member_layouts
|
|
428
|
-
@project_struct_member_layouts = result.struct_member_layouts if result.struct_member_layouts
|
|
429
690
|
end
|
|
430
|
-
|
|
691
|
+
|
|
692
|
+
# ADR-46 — the dependency-recording and subset-analysis modes read the discovery tables OUTSIDE the
|
|
693
|
+
# analysis assembly (`Runner#symbol_fingerprints` / `#class_declarations`, consumed by
|
|
694
|
+
# {IncrementalSession} after the run), so they force the build eagerly — matching the pre-slice-1
|
|
695
|
+
# timing where discovery always ran before `compute_run_diagnostics`. Every other mode defers to the
|
|
696
|
+
# lazy build inside `#assemble_run_diagnostics`.
|
|
697
|
+
def force_eager_discovery?
|
|
698
|
+
@record_dependencies || !@analyze_only.nil?
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
private :run_project_pre_passes, :adopt_prebuilt_project_scan, :apply_pre_passes_result,
|
|
702
|
+
:apply_discovery_result, :ensure_project_discovery, :force_eager_discovery?
|
|
431
703
|
|
|
432
704
|
# Ruby versions probed (ascending) to discover the lowest one this Prism build accepts for
|
|
433
705
|
# `version:`. Prism exposes no version list, so the floor is found empirically — only when a
|
|
@@ -518,6 +790,7 @@ module Rigor
|
|
|
518
790
|
@pool_coordinator = PoolCoordinator.new(
|
|
519
791
|
configuration: @configuration, cache_store: @cache_store, explain: @explain,
|
|
520
792
|
workers: @workers, collect_stats: @collect_stats, buffer: @buffer,
|
|
793
|
+
record_dependencies: @record_dependencies,
|
|
521
794
|
environment_override: @environment_override,
|
|
522
795
|
rbs_extended_reporter: @rbs_extended_reporter,
|
|
523
796
|
boundary_cross_reporter: @boundary_cross_reporter,
|
|
@@ -541,6 +814,8 @@ module Rigor
|
|
|
541
814
|
cached_plugin_prepare_diagnostics: -> { @cached_plugin_prepare_diagnostics },
|
|
542
815
|
pre_eval_diagnostics_from_scanner: -> { @pre_eval_diagnostics_from_scanner },
|
|
543
816
|
synthesized_namespaces_snapshot: -> { @snapshots.synthesized_namespaces },
|
|
817
|
+
quarantined_signatures_snapshot: -> { @snapshots.quarantined_signatures },
|
|
818
|
+
env_build_failure_snapshot: -> { @snapshots.env_build_failure },
|
|
544
819
|
conformance_results_snapshot: -> { @snapshots.conformance_results }
|
|
545
820
|
)
|
|
546
821
|
end
|
|
@@ -678,7 +953,7 @@ module Rigor
|
|
|
678
953
|
bad = []
|
|
679
954
|
Array(paths).each do |path|
|
|
680
955
|
if File.directory?(path)
|
|
681
|
-
files.concat(
|
|
956
|
+
files.concat(PathExpansion.directory_files(path, @configuration.exclude_patterns))
|
|
682
957
|
# Editor-mode bypass: the buffer's logical path is treated as a real `.rb` file regardless of
|
|
683
958
|
# on-disk presence — `parse_source` reads bytes from the buffer's physical path. Common case: LSP
|
|
684
959
|
# client editing a brand-new file.
|
|
@@ -710,22 +985,6 @@ module Rigor
|
|
|
710
985
|
@in_memory_sources&.key?(path)
|
|
711
986
|
end
|
|
712
987
|
|
|
713
|
-
# `Configuration#exclude_patterns` is a list of glob patterns checked against each globbed path via
|
|
714
|
-
# `File.fnmatch?` (without `FNM_PATHNAME`, so `**` and `*` both span path separators — the patterns
|
|
715
|
-
# behave like substring globs). Built-in defaults exclude `vendor/bundle`, `.bundle`, `node_modules`,
|
|
716
|
-
# and `tmp` so the analyser never walks into vendored deps or build artefacts. User-supplied entries
|
|
717
|
-
# (`.rigor.yml` `exclude:`) layer on top. Explicit file arguments to the CLI bypass this filter — only
|
|
718
|
-
# the directory-glob expansion is filtered.
|
|
719
|
-
def reject_excluded(file_list)
|
|
720
|
-
return file_list if @configuration.exclude_patterns.empty?
|
|
721
|
-
|
|
722
|
-
file_list.reject { |path| excluded?(path) }
|
|
723
|
-
end
|
|
724
|
-
|
|
725
|
-
def excluded?(path)
|
|
726
|
-
@configuration.exclude_patterns.any? { |pattern| File.fnmatch?(pattern, path) }
|
|
727
|
-
end
|
|
728
|
-
|
|
729
988
|
def path_error(path, message, severity: :error)
|
|
730
989
|
Diagnostic.new(
|
|
731
990
|
path: path,
|
|
@@ -766,12 +1025,15 @@ module Rigor
|
|
|
766
1025
|
# cross-file def — ADR-15 sequential-equivalence contract).
|
|
767
1026
|
def project_scope_seed_tables
|
|
768
1027
|
tables = {}
|
|
1028
|
+
# ADR-84 WD2 — the run-scope token rides the same seed so the fork/Ractor `WorkerSession` scopes
|
|
1029
|
+
# bucket identically to the sequential path.
|
|
1030
|
+
tables[:run_generation] = @run_generation if @run_generation
|
|
769
1031
|
tables[:discovered_classes] = @project_discovered_classes unless @project_discovered_classes.empty?
|
|
770
1032
|
tables[:discovered_def_nodes] = @project_discovered_def_nodes unless @project_discovered_def_nodes.empty?
|
|
771
1033
|
unless @project_discovered_singleton_def_nodes.empty?
|
|
772
1034
|
tables[:discovered_singleton_def_nodes] = @project_discovered_singleton_def_nodes
|
|
773
1035
|
end
|
|
774
|
-
tables
|
|
1036
|
+
seed_def_source_tables(tables)
|
|
775
1037
|
unless @project_discovered_superclasses.empty?
|
|
776
1038
|
tables[:discovered_superclasses] = @project_discovered_superclasses
|
|
777
1039
|
end
|
|
@@ -780,6 +1042,10 @@ module Rigor
|
|
|
780
1042
|
tables[:discovered_method_visibilities] = @project_discovered_method_visibilities
|
|
781
1043
|
end
|
|
782
1044
|
tables[:discovered_methods] = @project_discovered_methods unless @project_discovered_methods.empty?
|
|
1045
|
+
# ADR-67 WD6a — the call-site parameter-inference table rides the same seed so a pooled `WorkerSession`
|
|
1046
|
+
# scope seeds inferred parameters identically to the sequential path. Empty (and absent) unless the
|
|
1047
|
+
# `parameter_inference:` gate ran the pre-pass.
|
|
1048
|
+
tables[:param_inferred_types] = @project_param_inferred_types unless @project_param_inferred_types.empty?
|
|
783
1049
|
seed_member_layout_tables(tables)
|
|
784
1050
|
# ADR-46 slice 1 — the class-declaration source map is read only by the ancestry accessors during
|
|
785
1051
|
# dependency recording, so seed it only when recording is on; a normal run never carries it.
|
|
@@ -789,6 +1055,16 @@ module Rigor
|
|
|
789
1055
|
tables
|
|
790
1056
|
end
|
|
791
1057
|
|
|
1058
|
+
# ADR-46 — seed the instance + singleton `"path:line"` def-source tables (each only when non-empty).
|
|
1059
|
+
# Extracted to keep {#project_scope_seed_tables} under the complexity budget. The singleton table (slice 4
|
|
1060
|
+
# extension) rides the same seed so a pooled `WorkerSession` records singleton symbol edges identically.
|
|
1061
|
+
def seed_def_source_tables(tables)
|
|
1062
|
+
tables[:discovered_def_sources] = @project_discovered_def_sources unless @project_discovered_def_sources.empty?
|
|
1063
|
+
return if @project_discovered_singleton_def_sources.empty?
|
|
1064
|
+
|
|
1065
|
+
tables[:discovered_singleton_def_sources] = @project_discovered_singleton_def_sources
|
|
1066
|
+
end
|
|
1067
|
+
|
|
792
1068
|
# ADR-48 — seed the Data + Struct member-layout tables (each only when non-empty). Extracted to keep
|
|
793
1069
|
# {#project_scope_seed_tables} under the complexity budget.
|
|
794
1070
|
def seed_member_layout_tables(tables)
|