rigortype 0.2.8 → 0.3.0
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/core_overlay/csv.rbs +28 -0
- data/data/core_overlay/psych.rbs +22 -0
- data/docs/handbook/01-getting-started.md +9 -1
- data/docs/handbook/02-everyday-types.md +4 -1
- data/docs/handbook/03-narrowing.md +2 -1
- data/docs/handbook/04-tuples-and-shapes.md +7 -3
- data/docs/handbook/06-classes.md +18 -9
- data/docs/handbook/08-understanding-errors.md +4 -3
- data/docs/handbook/11-sig-gen.md +11 -1
- data/docs/handbook/appendix-mypy.md +4 -3
- data/docs/install.md +14 -6
- data/docs/manual/01-installation.md +99 -4
- data/docs/manual/02-cli-reference.md +65 -19
- data/docs/manual/03-configuration.md +34 -2
- data/docs/manual/04-diagnostics.md +23 -0
- data/docs/manual/07-plugins.md +5 -5
- data/docs/manual/09-editor-integration.md +4 -4
- data/docs/manual/11-ci.md +51 -0
- data/docs/manual/12-caching.md +65 -0
- data/docs/manual/14-rails-quickstart.md +4 -2
- data/docs/manual/15-type-protection-coverage.md +21 -0
- data/docs/manual/plugins/rigor-actionpack.md +1 -1
- data/docs/manual/plugins/rigor-activerecord.md +12 -5
- data/docs/manual/plugins/rigor-rails-routes.md +11 -0
- 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 +93 -0
- data/lib/rigor/analysis/check_rules.rb +557 -95
- 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 +68 -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 +343 -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_descriptor.rb +56 -10
- data/lib/rigor/cache/store.rb +207 -60
- 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 +212 -27
- data/lib/rigor/cli/coverage_command.rb +56 -30
- data/lib/rigor/cli/diagnostic_formats.rb +4 -1
- data/lib/rigor/cli/docs_command.rb +0 -30
- data/lib/rigor/cli/doctor_command.rb +163 -23
- 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/protection_fork_scan.rb +55 -0
- data/lib/rigor/cli/protection_report.rb +7 -1
- data/lib/rigor/cli/sig_gen_command.rb +36 -9
- data/lib/rigor/cli/skill_command.rb +1 -32
- 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 +35 -0
- data/lib/rigor/environment/missing_gem_constant_index.rb +128 -0
- data/lib/rigor/environment/rbs_loader.rb +164 -5
- data/lib/rigor/environment.rb +71 -43
- 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 +379 -113
- data/lib/rigor/inference/fork_map.rb +87 -0
- 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 +11 -0
- 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 +245 -17
- data/lib/rigor/inference/parameter_inference_collector.rb +70 -10
- 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 +460 -49
- data/lib/rigor/inference/statement_evaluator.rb +111 -17
- 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/completion_provider.rb +2 -1
- 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/selection_range_provider.rb +2 -1
- data/lib/rigor/language_server/signature_help_provider.rb +2 -1
- data/lib/rigor/plugin/base.rb +9 -22
- 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 +24 -2
- 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-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +6 -4
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/analyzer.rb +23 -8
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +5 -3
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +21 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/analyzer.rb +13 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +65 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_index.rb +20 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/schema_parser.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/structure_sql_parser.rb +172 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +22 -8
- 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-activesupport-core-ext/sig/active_support/core_ext.rbs +32 -0
- 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 +191 -0
- 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/helper_table.rb +19 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +48 -15
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +42 -12
- 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/environment.rbs +1 -0
- data/sig/rigor/inference.rbs +7 -0
- data/sig/rigor/plugin/base.rbs +2 -3
- 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 +49 -24
- 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,19 @@ 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
|
|
265
458
|
) do
|
|
266
459
|
computed = true
|
|
267
460
|
diags = assemble_run_diagnostics(expansion, environment: environment)
|
|
@@ -277,6 +470,17 @@ module Rigor
|
|
|
277
470
|
end
|
|
278
471
|
|
|
279
472
|
def assemble_run_diagnostics(expansion, environment: nil)
|
|
473
|
+
# Force the deferred cross-file discovery pre-pass on the analysis (miss) path. Memoised, so the
|
|
474
|
+
# eager force in `#run` (recording / subset modes) makes this a no-op. A warm cache HIT never calls
|
|
475
|
+
# `assemble_run_diagnostics`, so it never runs the two whole-project parse passes. Runs over the FULL
|
|
476
|
+
# expansion — subset (`analyze_only`) mode still needs the complete cross-file index (ADR-46 §2).
|
|
477
|
+
ensure_project_discovery(expansion)
|
|
478
|
+
# ADR-67 WD6a — the opt-in call-site parameter-inference pre-pass. Runs on the parent BEFORE the pool
|
|
479
|
+
# split (so every worker sees the same frozen table — the seed-before-fork determinism the discovery
|
|
480
|
+
# tables use) and only on the analysis (miss / non-cacheable) path (a warm ADR-45 cache HIT never
|
|
481
|
+
# assembles). Gate off → no-op, and `environment` is left untouched so its lazy build timing is
|
|
482
|
+
# unchanged. Returns the resolved environment so the sequential dispatch reuses it (no double build).
|
|
483
|
+
environment = seed_parameter_inference(expansion, environment)
|
|
280
484
|
diagnostics = @diagnostic_aggregator.pre_file_diagnostics(expansion)
|
|
281
485
|
# ADR-46 — record which project files this run actually analyzed (the `analyze_only` subset, or
|
|
282
486
|
# all of them). The incremental orchestrator serves every analyzed-but-not-affected file from the
|
|
@@ -284,6 +488,8 @@ module Rigor
|
|
|
284
488
|
targets = target_files(expansion)
|
|
285
489
|
@analyzed_files = targets
|
|
286
490
|
diagnostics += @pool_coordinator.analyze_files(targets, environment: environment)
|
|
491
|
+
diagnostics += @diagnostic_aggregator.rbs_quarantined_signature_diagnostics
|
|
492
|
+
diagnostics += @diagnostic_aggregator.rbs_environment_build_failed_diagnostics
|
|
287
493
|
diagnostics += @diagnostic_aggregator.rbs_synthesized_namespace_diagnostics
|
|
288
494
|
diagnostics += @diagnostic_aggregator.conforms_to_diagnostics
|
|
289
495
|
diagnostics += @diagnostic_aggregator.rbs_extended_reporter_diagnostics
|
|
@@ -291,6 +497,31 @@ module Rigor
|
|
|
291
497
|
diagnostics + @diagnostic_aggregator.source_rbs_synthesis_diagnostics
|
|
292
498
|
end
|
|
293
499
|
|
|
500
|
+
# ADR-67 WD6a — the check-walk parameter-inference pre-pass. Populates `@project_param_inferred_types`
|
|
501
|
+
# (read by `project_scope_seed_tables`) with the call-site union of every undeclared parameter, running
|
|
502
|
+
# ONE round (a single hop of call-site → param typing; the protection scan's three-round fixpoint stays a
|
|
503
|
+
# protection-surface luxury until measured). No-op unless `parameter_inference:` is enabled, so the
|
|
504
|
+
# default run pays exactly nothing here and `environment` passes through unchanged (preserving the lazy
|
|
505
|
+
# env-build timing). When enabled, it resolves the environment once — the collector types call-site
|
|
506
|
+
# arguments against the same RBS / plugin surface the check uses — and returns it so the sequential
|
|
507
|
+
# dispatch reuses that build. The whole-project file set (not the `analyze_only` subset) is scanned: the
|
|
508
|
+
# inference is cross-file (a call site in one file types a parameter in another). Fails soft — a collector
|
|
509
|
+
# error must never break a run, so the table stays empty and the run proceeds unseeded.
|
|
510
|
+
def seed_parameter_inference(expansion, environment)
|
|
511
|
+
return environment unless @configuration.parameter_inference
|
|
512
|
+
|
|
513
|
+
files = expansion.fetch(:files)
|
|
514
|
+
environment ||= @pool_coordinator.resolve_sequential_environment(source_files: files)
|
|
515
|
+
@project_param_inferred_types = Inference::ParameterInferenceCollector.collect(
|
|
516
|
+
files: files, environment: environment,
|
|
517
|
+
target_ruby: @configuration.target_ruby, max_rounds: 1, workers: @workers
|
|
518
|
+
)
|
|
519
|
+
environment
|
|
520
|
+
rescue StandardError
|
|
521
|
+
@project_param_inferred_types = {}.freeze
|
|
522
|
+
environment
|
|
523
|
+
end
|
|
524
|
+
|
|
294
525
|
# A cache hit skipped the analysis, so the per-run stats (wall split, RBS-class counts, …) were never
|
|
295
526
|
# gathered — report none rather than the stale snapshot defaults.
|
|
296
527
|
def stats_for_run(wall_started_at:, expansion:)
|
|
@@ -303,9 +534,18 @@ module Rigor
|
|
|
303
534
|
# Cacheable only for a full sequential project run with a writable cache and no per-buffer /
|
|
304
535
|
# prebuilt override — every other mode has a different result identity (pool workers read in
|
|
305
536
|
# separate processes; editor mode is per-buffer; prebuilt is the LSP path).
|
|
537
|
+
#
|
|
538
|
+
# The ADR-46 incremental modes are excluded too, now that they carry a real cache store (ADR-85
|
|
539
|
+
# WD1): a `record_dependencies` run MUST perform per-file analysis to capture the dependency
|
|
540
|
+
# graph (a cache-served run records nothing, leaving the next recheck's dependents empty —
|
|
541
|
+
# unsound), and an `analyze_only` subset run produces intentionally partial diagnostics that
|
|
542
|
+
# share the full run's result key (`run_key_descriptor` keys on the whole expansion, not the
|
|
543
|
+
# subset), so serving one as the other would manufacture a wrong result. Both instead use the
|
|
544
|
+
# store for the RBS-env + plugin-producer tiers, where the incremental win actually lives.
|
|
306
545
|
def run_result_cacheable?
|
|
307
546
|
!@cache_store.nil? && !@cache_store.read_only? &&
|
|
308
|
-
@buffer.nil? && @prebuilt.nil? && !pool_mode?
|
|
547
|
+
@buffer.nil? && @prebuilt.nil? && !pool_mode? &&
|
|
548
|
+
!@record_dependencies && @analyze_only.nil?
|
|
309
549
|
end
|
|
310
550
|
|
|
311
551
|
# Stable cache key inputs — known before the run: a digest of the resolved configuration, the engine
|
|
@@ -313,16 +553,13 @@ module Rigor
|
|
|
313
553
|
# editing one is caught by dependency validation). nil disables the cache for this run rather than
|
|
314
554
|
# risking a malformed key.
|
|
315
555
|
def run_key_descriptor(expansion, rbs_descriptor)
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
]
|
|
556
|
+
# ADR-87 WD4 — the key is built through the shared {RunCacheKey} builder the boot-slimming probe also
|
|
557
|
+
# uses, so the miss path (here, passing the loader's `rbs_descriptor.configs`) and the hit path (which
|
|
558
|
+
# reconstructs `rbs.libraries` from config) can never drift out of key agreement.
|
|
559
|
+
RunCacheKey.descriptor(
|
|
560
|
+
configuration: @configuration, files: expansion.fetch(:files),
|
|
561
|
+
explain: @explain, rbs_config_entries: rbs_descriptor.configs
|
|
323
562
|
)
|
|
324
|
-
rescue StandardError
|
|
325
|
-
nil
|
|
326
563
|
end
|
|
327
564
|
|
|
328
565
|
# Files the run actually depended on, collected AFTER it ran: every analyzed file, every RBS `sig`
|
|
@@ -343,16 +580,12 @@ module Rigor
|
|
|
343
580
|
def analyzed_file_entries(expansion)
|
|
344
581
|
expansion.fetch(:files).map do |path|
|
|
345
582
|
physical = @buffer ? @buffer.resolve(path) : path
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
)
|
|
583
|
+
# ADR-87 WD1 — validation-only dependency descriptor, so the stat-then-digest `:stat` comparator
|
|
584
|
+
# applies (the env-cache KEY files stay `:digest`).
|
|
585
|
+
Cache::Descriptor::FileEntry.stat(path: physical, digest: Cache::FileDigest.hexdigest(physical))
|
|
349
586
|
end
|
|
350
587
|
end
|
|
351
588
|
|
|
352
|
-
def config_hash_entry(key, payload)
|
|
353
|
-
Cache::Descriptor::ConfigEntry.new(key: key, value_hash: Digest::SHA256.hexdigest(payload))
|
|
354
|
-
end
|
|
355
|
-
|
|
356
589
|
# Runs every project-wide pre-pass (`load_plugins` +
|
|
357
590
|
# `plugin#prepare` + dependency-source builder +
|
|
358
591
|
# synthetic-method scanner + project-patched scanner)
|
|
@@ -399,35 +632,73 @@ module Rigor
|
|
|
399
632
|
apply_pre_passes_result(@pre_passes.adopt_prebuilt(scan))
|
|
400
633
|
end
|
|
401
634
|
|
|
402
|
-
# Internal: copies a {ProjectPrePasses::Result} bundle onto the runner's
|
|
403
|
-
#
|
|
404
|
-
#
|
|
405
|
-
#
|
|
406
|
-
#
|
|
407
|
-
|
|
635
|
+
# Internal: copies a {ProjectPrePasses::Result} bundle's EAGER (env-input) slots onto the runner's
|
|
636
|
+
# ivars, so every downstream reader (pool environment build, diagnostic aggregator) sees the same ivar
|
|
637
|
+
# surface. The cross-file discovery tables are NOT carried here — `#run` (prebuilt-less) and
|
|
638
|
+
# `adopt_prebuilt` both leave them at their frozen-empty constructor defaults, and the analysis path
|
|
639
|
+
# fills them lazily via {#ensure_project_discovery}. The prebuilt (LSP) path never fills them, matching
|
|
640
|
+
# the original adopt behaviour that seeded an empty project scope.
|
|
641
|
+
def apply_pre_passes_result(result)
|
|
408
642
|
@plugin_registry = result.plugin_registry
|
|
409
643
|
@dependency_source_index = result.dependency_source_index
|
|
410
644
|
@cached_plugin_prepare_diagnostics = result.cached_plugin_prepare_diagnostics
|
|
411
645
|
@synthetic_method_index = result.synthetic_method_index
|
|
412
646
|
@project_patched_methods = result.project_patched_methods
|
|
413
647
|
@pre_eval_diagnostics_from_scanner = result.pre_eval_diagnostics_from_scanner
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
@
|
|
421
|
-
@
|
|
422
|
-
@
|
|
423
|
-
|
|
424
|
-
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
# Internal: adopts a {ProjectPrePasses::Discovery} bundle (the two whole-project discovery passes)
|
|
651
|
+
# onto the runner's discovery ivars, in the same assignment order the original inline pre-pass used.
|
|
652
|
+
# Called only from {#ensure_project_discovery}.
|
|
653
|
+
def apply_discovery_result(discovery)
|
|
654
|
+
@project_discovered_classes = discovery.discovered_classes
|
|
655
|
+
@project_discovered_def_nodes = discovery.discovered_def_nodes
|
|
656
|
+
@project_discovered_singleton_def_nodes = discovery.discovered_singleton_def_nodes
|
|
657
|
+
@project_discovered_def_sources = discovery.discovered_def_sources
|
|
658
|
+
@project_discovered_singleton_def_sources = discovery.discovered_singleton_def_sources
|
|
659
|
+
@project_discovered_superclasses = discovery.discovered_superclasses
|
|
660
|
+
@project_discovered_includes = discovery.discovered_includes
|
|
661
|
+
@project_discovered_class_sources = discovery.discovered_class_sources
|
|
662
|
+
@project_discovered_method_visibilities = discovery.discovered_method_visibilities
|
|
663
|
+
@project_discovered_methods = discovery.discovered_methods
|
|
664
|
+
@project_data_member_layouts = discovery.data_member_layouts
|
|
665
|
+
@project_struct_member_layouts = discovery.struct_member_layouts
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
# Internal: builds the deferred cross-file discovery tables at most once per run and adopts them.
|
|
669
|
+
# Memoised on `@project_discovery_done` (reset at the start of `#run`). No-op under `@prebuilt` — the
|
|
670
|
+
# LSP path deliberately seeds an empty project scope from a snapshot that carries no discovery tables,
|
|
671
|
+
# so forcing a build there would change that contract. Called eagerly from `#run` for the recording /
|
|
672
|
+
# subset (ADR-46) modes and lazily from `#assemble_run_diagnostics` on the analysis path, so a warm
|
|
673
|
+
# cache HIT (which never assembles) never pays the double parse.
|
|
674
|
+
def ensure_project_discovery(expansion)
|
|
675
|
+
return if @prebuilt
|
|
676
|
+
return if @project_discovery_done
|
|
677
|
+
|
|
678
|
+
@project_discovery_done = true
|
|
679
|
+
if @collect_seed_bundles
|
|
680
|
+
# ADR-85 WD2 — rebuild discovery from the prior run's bundles (re-walking only changed files) and
|
|
681
|
+
# capture the refreshed bundle set for the session to persist.
|
|
682
|
+
discovery, @seed_bundles = @pre_passes.discover_from_bundles(
|
|
683
|
+
expansion: expansion, seed_bundles: @restored_seed_bundles
|
|
684
|
+
)
|
|
685
|
+
apply_discovery_result(discovery)
|
|
686
|
+
else
|
|
687
|
+
apply_discovery_result(@pre_passes.discover(expansion: expansion))
|
|
425
688
|
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
689
|
end
|
|
430
|
-
|
|
690
|
+
|
|
691
|
+
# ADR-46 — the dependency-recording and subset-analysis modes read the discovery tables OUTSIDE the
|
|
692
|
+
# analysis assembly (`Runner#symbol_fingerprints` / `#class_declarations`, consumed by
|
|
693
|
+
# {IncrementalSession} after the run), so they force the build eagerly — matching the pre-slice-1
|
|
694
|
+
# timing where discovery always ran before `compute_run_diagnostics`. Every other mode defers to the
|
|
695
|
+
# lazy build inside `#assemble_run_diagnostics`.
|
|
696
|
+
def force_eager_discovery?
|
|
697
|
+
@record_dependencies || !@analyze_only.nil?
|
|
698
|
+
end
|
|
699
|
+
|
|
700
|
+
private :run_project_pre_passes, :adopt_prebuilt_project_scan, :apply_pre_passes_result,
|
|
701
|
+
:apply_discovery_result, :ensure_project_discovery, :force_eager_discovery?
|
|
431
702
|
|
|
432
703
|
# Ruby versions probed (ascending) to discover the lowest one this Prism build accepts for
|
|
433
704
|
# `version:`. Prism exposes no version list, so the floor is found empirically — only when a
|
|
@@ -518,6 +789,7 @@ module Rigor
|
|
|
518
789
|
@pool_coordinator = PoolCoordinator.new(
|
|
519
790
|
configuration: @configuration, cache_store: @cache_store, explain: @explain,
|
|
520
791
|
workers: @workers, collect_stats: @collect_stats, buffer: @buffer,
|
|
792
|
+
record_dependencies: @record_dependencies,
|
|
521
793
|
environment_override: @environment_override,
|
|
522
794
|
rbs_extended_reporter: @rbs_extended_reporter,
|
|
523
795
|
boundary_cross_reporter: @boundary_cross_reporter,
|
|
@@ -541,6 +813,8 @@ module Rigor
|
|
|
541
813
|
cached_plugin_prepare_diagnostics: -> { @cached_plugin_prepare_diagnostics },
|
|
542
814
|
pre_eval_diagnostics_from_scanner: -> { @pre_eval_diagnostics_from_scanner },
|
|
543
815
|
synthesized_namespaces_snapshot: -> { @snapshots.synthesized_namespaces },
|
|
816
|
+
quarantined_signatures_snapshot: -> { @snapshots.quarantined_signatures },
|
|
817
|
+
env_build_failure_snapshot: -> { @snapshots.env_build_failure },
|
|
544
818
|
conformance_results_snapshot: -> { @snapshots.conformance_results }
|
|
545
819
|
)
|
|
546
820
|
end
|
|
@@ -678,7 +952,7 @@ module Rigor
|
|
|
678
952
|
bad = []
|
|
679
953
|
Array(paths).each do |path|
|
|
680
954
|
if File.directory?(path)
|
|
681
|
-
files.concat(
|
|
955
|
+
files.concat(PathExpansion.directory_files(path, @configuration.exclude_patterns))
|
|
682
956
|
# Editor-mode bypass: the buffer's logical path is treated as a real `.rb` file regardless of
|
|
683
957
|
# on-disk presence — `parse_source` reads bytes from the buffer's physical path. Common case: LSP
|
|
684
958
|
# client editing a brand-new file.
|
|
@@ -710,22 +984,6 @@ module Rigor
|
|
|
710
984
|
@in_memory_sources&.key?(path)
|
|
711
985
|
end
|
|
712
986
|
|
|
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
987
|
def path_error(path, message, severity: :error)
|
|
730
988
|
Diagnostic.new(
|
|
731
989
|
path: path,
|
|
@@ -766,12 +1024,15 @@ module Rigor
|
|
|
766
1024
|
# cross-file def — ADR-15 sequential-equivalence contract).
|
|
767
1025
|
def project_scope_seed_tables
|
|
768
1026
|
tables = {}
|
|
1027
|
+
# ADR-84 WD2 — the run-scope token rides the same seed so the fork/Ractor `WorkerSession` scopes
|
|
1028
|
+
# bucket identically to the sequential path.
|
|
1029
|
+
tables[:run_generation] = @run_generation if @run_generation
|
|
769
1030
|
tables[:discovered_classes] = @project_discovered_classes unless @project_discovered_classes.empty?
|
|
770
1031
|
tables[:discovered_def_nodes] = @project_discovered_def_nodes unless @project_discovered_def_nodes.empty?
|
|
771
1032
|
unless @project_discovered_singleton_def_nodes.empty?
|
|
772
1033
|
tables[:discovered_singleton_def_nodes] = @project_discovered_singleton_def_nodes
|
|
773
1034
|
end
|
|
774
|
-
tables
|
|
1035
|
+
seed_def_source_tables(tables)
|
|
775
1036
|
unless @project_discovered_superclasses.empty?
|
|
776
1037
|
tables[:discovered_superclasses] = @project_discovered_superclasses
|
|
777
1038
|
end
|
|
@@ -780,6 +1041,10 @@ module Rigor
|
|
|
780
1041
|
tables[:discovered_method_visibilities] = @project_discovered_method_visibilities
|
|
781
1042
|
end
|
|
782
1043
|
tables[:discovered_methods] = @project_discovered_methods unless @project_discovered_methods.empty?
|
|
1044
|
+
# ADR-67 WD6a — the call-site parameter-inference table rides the same seed so a pooled `WorkerSession`
|
|
1045
|
+
# scope seeds inferred parameters identically to the sequential path. Empty (and absent) unless the
|
|
1046
|
+
# `parameter_inference:` gate ran the pre-pass.
|
|
1047
|
+
tables[:param_inferred_types] = @project_param_inferred_types unless @project_param_inferred_types.empty?
|
|
783
1048
|
seed_member_layout_tables(tables)
|
|
784
1049
|
# ADR-46 slice 1 — the class-declaration source map is read only by the ancestry accessors during
|
|
785
1050
|
# dependency recording, so seed it only when recording is on; a normal run never carries it.
|
|
@@ -789,6 +1054,16 @@ module Rigor
|
|
|
789
1054
|
tables
|
|
790
1055
|
end
|
|
791
1056
|
|
|
1057
|
+
# ADR-46 — seed the instance + singleton `"path:line"` def-source tables (each only when non-empty).
|
|
1058
|
+
# Extracted to keep {#project_scope_seed_tables} under the complexity budget. The singleton table (slice 4
|
|
1059
|
+
# extension) rides the same seed so a pooled `WorkerSession` records singleton symbol edges identically.
|
|
1060
|
+
def seed_def_source_tables(tables)
|
|
1061
|
+
tables[:discovered_def_sources] = @project_discovered_def_sources unless @project_discovered_def_sources.empty?
|
|
1062
|
+
return if @project_discovered_singleton_def_sources.empty?
|
|
1063
|
+
|
|
1064
|
+
tables[:discovered_singleton_def_sources] = @project_discovered_singleton_def_sources
|
|
1065
|
+
end
|
|
1066
|
+
|
|
792
1067
|
# ADR-48 — seed the Data + Struct member-layout tables (each only when non-empty). Extracted to keep
|
|
793
1068
|
# {#project_scope_seed_tables} under the complexity budget.
|
|
794
1069
|
def seed_member_layout_tables(tables)
|