rigortype 0.3.3 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/data/builtins/ruby_core/array.yml +11 -24
- data/data/builtins/ruby_core/enumerable.yml +4 -8
- data/data/builtins/ruby_core/exception.yml +6 -9
- data/data/builtins/ruby_core/hash.yml +13 -20
- data/data/builtins/ruby_core/io.yml +6 -9
- data/data/builtins/ruby_core/numeric.yml +2 -3
- data/data/builtins/ruby_core/proc.yml +4 -8
- data/data/builtins/ruby_core/re.yml +6 -9
- data/data/builtins/ruby_core/set.yml +2 -3
- data/data/builtins/ruby_core/string.yml +4 -7
- data/data/builtins/ruby_core/struct.yml +0 -6
- data/data/builtins/ruby_core/time.yml +3 -6
- data/data/effects/core.yml +860 -0
- data/data/effects/registry.yml +95 -0
- data/docs/handbook/07-rbs-and-extended.md +106 -5
- data/docs/manual/02-cli-reference.md +359 -2
- data/docs/manual/03-configuration.md +101 -1
- data/docs/manual/04-diagnostics.md +8 -3
- data/docs/manual/11-ci.md +37 -0
- data/docs/manual/12-caching.md +39 -0
- data/docs/manual/16-rbs-extended-annotations.md +90 -0
- data/docs/manual/18-removing-dead-code.md +326 -0
- data/docs/manual/19-effect-labels.md +671 -0
- data/docs/manual/README.md +10 -0
- data/docs/manual/ci-templates/README.md +9 -0
- data/docs/manual/plugins/rigor-actionmailer.md +5 -0
- data/docs/manual/plugins/rigor-activejob.md +47 -0
- data/docs/manual/plugins/rigor-factorybot.md +16 -0
- data/docs/manual/plugins/rigor-pundit.md +26 -0
- data/docs/manual/plugins/rigor-rails-routes.md +41 -2
- data/docs/manual/plugins/rigor-rspec-rails.md +5 -0
- data/docs/manual/plugins/rigor-rspec.md +8 -0
- data/docs/manual/plugins/rigor-sidekiq.md +54 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +28 -2
- data/lib/rigor/analysis/incremental_session.rb +140 -6
- data/lib/rigor/analysis/reachability/graph.rb +244 -0
- data/lib/rigor/analysis/reachability/plugin_roots.rb +164 -0
- data/lib/rigor/analysis/reachability/project_files.rb +58 -0
- data/lib/rigor/analysis/reachability/scan.rb +241 -0
- data/lib/rigor/analysis/reachability/signature_scan.rb +134 -0
- data/lib/rigor/analysis/rule_catalog.rb +168 -3
- data/lib/rigor/analysis/run_cache_key.rb +16 -0
- data/lib/rigor/analysis/run_cache_probe.rb +69 -1
- data/lib/rigor/analysis/runner/declaration_position.rb +38 -0
- data/lib/rigor/analysis/runner/effect_annotation_residual_pass.rb +108 -0
- data/lib/rigor/analysis/runner/effect_envelope_pass.rb +358 -0
- data/lib/rigor/analysis/runner/envelope_messages.rb +61 -0
- data/lib/rigor/analysis/runner/pool_coordinator.rb +93 -10
- data/lib/rigor/analysis/runner/run_snapshots.rb +7 -4
- data/lib/rigor/analysis/runner.rb +429 -10
- data/lib/rigor/analysis/worker_session.rb +60 -2
- data/lib/rigor/bleeding_edge.rb +22 -0
- data/lib/rigor/cache/incremental_snapshot.rb +24 -5
- data/lib/rigor/cli/check_command.rb +13 -1
- data/lib/rigor/cli/check_runner_factory.rb +4 -1
- data/lib/rigor/cli/effects_command.rb +245 -0
- data/lib/rigor/cli/effects_diff_renderer.rb +127 -0
- data/lib/rigor/cli/effects_explain_renderer.rb +68 -0
- data/lib/rigor/cli/effects_renderer.rb +92 -0
- data/lib/rigor/cli/effects_report.rb +163 -0
- data/lib/rigor/cli/effects_snapshot_command.rb +275 -0
- data/lib/rigor/cli/unused_command.rb +288 -0
- data/lib/rigor/cli.rb +28 -1
- data/lib/rigor/configuration/severity_profile.rb +23 -0
- data/lib/rigor/configuration.rb +322 -28
- data/lib/rigor/configuration_error.rb +20 -0
- data/lib/rigor/effects/attribution.rb +76 -0
- data/lib/rigor/effects/catalog.rb +275 -0
- data/lib/rigor/effects/collector.rb +219 -0
- data/lib/rigor/effects/config_envelopes.rb +185 -0
- data/lib/rigor/effects/discharge.rb +69 -0
- data/lib/rigor/effects/effect_table.rb +92 -0
- data/lib/rigor/effects/entry_points.rb +139 -0
- data/lib/rigor/effects/envelope.rb +86 -0
- data/lib/rigor/effects/envelope_check.rb +172 -0
- data/lib/rigor/effects/envelope_index.rb +157 -0
- data/lib/rigor/effects/file_collection.rb +174 -0
- data/lib/rigor/effects/framework_units.rb +222 -0
- data/lib/rigor/effects/identity.rb +104 -0
- data/lib/rigor/effects/inline_anchor.rb +134 -0
- data/lib/rigor/effects/label.rb +77 -0
- data/lib/rigor/effects/label_intent.rb +73 -0
- data/lib/rigor/effects/label_set.rb +136 -0
- data/lib/rigor/effects/liskov_check.rb +167 -0
- data/lib/rigor/effects/local_ownership.rb +132 -0
- data/lib/rigor/effects/method_key.rb +40 -0
- data/lib/rigor/effects/mutation_classifier.rb +92 -0
- data/lib/rigor/effects/narrowing.rb +202 -0
- data/lib/rigor/effects/origin.rb +66 -0
- data/lib/rigor/effects/path_finder.rb +89 -0
- data/lib/rigor/effects/plugin_facts.rb +384 -0
- data/lib/rigor/effects/propagator.rb +335 -0
- data/lib/rigor/effects/registry.rb +200 -0
- data/lib/rigor/effects/scanner.rb +302 -0
- data/lib/rigor/effects/signature_sources.rb +90 -0
- data/lib/rigor/effects/snapshot.rb +396 -0
- data/lib/rigor/effects/snapshot_diff.rb +265 -0
- data/lib/rigor/effects/summary.rb +154 -0
- data/lib/rigor/effects/taint_cause.rb +39 -0
- data/lib/rigor/effects/unit_scan.rb +647 -0
- data/lib/rigor/effects/unknown_label_check.rb +86 -0
- data/lib/rigor/effects/unknown_label_report.rb +59 -0
- data/lib/rigor/effects.rb +47 -0
- data/lib/rigor/environment/rbs_loader.rb +34 -0
- data/lib/rigor/environment.rb +8 -1
- data/lib/rigor/flow_contribution/element.rb +1 -0
- data/lib/rigor/flow_contribution/merge_result.rb +5 -3
- data/lib/rigor/flow_contribution/merger.rb +16 -1
- data/lib/rigor/flow_contribution.rb +20 -4
- data/lib/rigor/inference/expression_typer.rb +18 -4
- data/lib/rigor/inference/pre_eval_constants.rb +189 -0
- data/lib/rigor/inference/scope_indexer.rb +15 -3
- data/lib/rigor/plugin/base.rb +28 -0
- data/lib/rigor/plugin/box.rb +18 -2
- data/lib/rigor/plugin/effect_ancestry.rb +80 -0
- data/lib/rigor/plugin/effect_attribution.rb +208 -0
- data/lib/rigor/plugin/effect_edge.rb +101 -0
- data/lib/rigor/plugin/effect_entry_points.rb +51 -0
- data/lib/rigor/plugin/first_party.rb +57 -0
- data/lib/rigor/plugin/isolation.rb +4 -1
- data/lib/rigor/plugin/manifest.rb +127 -3
- data/lib/rigor/plugin/registry.rb +55 -0
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/rbs_extended/envelope_scanner.rb +160 -0
- data/lib/rigor/rbs_extended.rb +188 -0
- data/lib/rigor/reflection.rb +131 -30
- data/lib/rigor/version.rb +1 -1
- data/lib/rigor.rb +1 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/effects.rb +90 -0
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable.rb +7 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/effects.rb +85 -0
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +8 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/effects.rb +152 -0
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +9 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +156 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/recurring_scan.rb +113 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +68 -3
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/effects.rb +186 -0
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord.rb +12 -1
- data/plugins/rigor-activerecord/sig/active_record/relation.rbs +145 -0
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/effects.rb +77 -0
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage.rb +6 -1
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext/effects.rb +143 -0
- data/plugins/rigor-activesupport-core-ext/lib/rigor/plugin/activesupport_core_ext.rb +9 -1
- data/plugins/rigor-activesupport-core-ext/sig/active_support/core_ext.rbs +401 -29
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +27 -0
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot.rb +30 -2
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +151 -0
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit.rb +46 -3
- data/plugins/rigor-rails/lib/rigor-rails.rb +1 -0
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/effects.rb +47 -0
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +7 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/acronyms.rb +81 -0
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/helper_table.rb +13 -1
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes/routes_parser.rb +202 -15
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +43 -3
- data/plugins/rigor-railties/lib/rigor/plugin/railties/effects.rb +171 -0
- data/plugins/rigor-railties/lib/rigor/plugin/railties.rb +62 -0
- data/plugins/rigor-railties/lib/rigor-railties.rb +3 -0
- data/plugins/rigor-rbs-inline/lib/rigor/plugin/rbs_inline.rb +56 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/effects.rb +75 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +122 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +68 -3
- data/sig/rigor/analysis/fact_store.rbs +1 -0
- data/sig/rigor/inference.rbs +2 -0
- data/sig/rigor/rbs_extended.rbs +6 -0
- data/sig/rigor.rbs +17 -1
- data/skills/rigor-unused-adjudicate/SKILL.md +90 -0
- metadata +84 -1
|
@@ -18,7 +18,13 @@ require_relative "../rbs_extended/conformance_checker"
|
|
|
18
18
|
require_relative "../reflection"
|
|
19
19
|
require_relative "../type/combinator"
|
|
20
20
|
require_relative "../inference/coverage_scanner"
|
|
21
|
+
require_relative "../effects/attribution"
|
|
22
|
+
require_relative "../effects/collector"
|
|
23
|
+
require_relative "../effects/envelope_index"
|
|
24
|
+
require_relative "../effects/identity"
|
|
25
|
+
require_relative "../effects/propagator"
|
|
21
26
|
require_relative "../inference/parameter_inference_collector"
|
|
27
|
+
require_relative "../inference/pre_eval_constants"
|
|
22
28
|
require_relative "../inference/scope_indexer"
|
|
23
29
|
require_relative "../inference/synthetic_method_scanner"
|
|
24
30
|
require_relative "../inference/project_patched_scanner"
|
|
@@ -40,6 +46,8 @@ require_relative "runner/run_snapshots"
|
|
|
40
46
|
require_relative "runner/project_pre_passes"
|
|
41
47
|
require_relative "runner/pool_coordinator"
|
|
42
48
|
require_relative "runner/diagnostic_aggregator"
|
|
49
|
+
require_relative "runner/effect_envelope_pass"
|
|
50
|
+
require_relative "runner/effect_annotation_residual_pass"
|
|
43
51
|
require_relative "runner/buffer_pool_dispatcher"
|
|
44
52
|
|
|
45
53
|
module Rigor
|
|
@@ -68,6 +76,92 @@ module Rigor
|
|
|
68
76
|
pooled.empty? ? @file_dependencies : @file_dependencies.merge(pooled)
|
|
69
77
|
end
|
|
70
78
|
|
|
79
|
+
# ADR-103 — the run's propagated effect graph, or {Effects::EffectTable.empty} when collection did
|
|
80
|
+
# not run. Reconciled from the sequential and pooled halves exactly as {#file_dependencies} is, then
|
|
81
|
+
# closed by the post-pool fixpoint in {#assemble_run_diagnostics}.
|
|
82
|
+
#
|
|
83
|
+
# This is a **report surface, not a diagnostic one**: nothing here enters the diagnostic stream, and
|
|
84
|
+
# `rigor check`'s output is identical whether or not it was computed (ADR-102's line).
|
|
85
|
+
def effect_table
|
|
86
|
+
@effect_table || Effects::EffectTable.empty
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# ADR-103 WD2 / WD6 / WD10 / #387 — the loaded plugins' effect contributions, compiled once per
|
|
90
|
+
# process. Memoised on first use rather than built in the constructor for two reasons: the plugin
|
|
91
|
+
# registry is adopted after construction (`apply_prebuilt` / the plugin-load pre-pass), and the
|
|
92
|
+
# project's as-written superclass table — which is what makes an `ActiveRecord::Base` row reach
|
|
93
|
+
# `User.find` — is filled by the cross-file discovery pre-pass, which on the sequential path runs
|
|
94
|
+
# AFTER the first file has already asked for these facts.
|
|
95
|
+
#
|
|
96
|
+
# So the memo is keyed on the ancestry table's identity rather than being unconditional, exactly as
|
|
97
|
+
# {#effect_envelope_index}'s is keyed on the environment's. An unconditional `||=` pinned the whole
|
|
98
|
+
# run to the empty table the first file saw, and the measured symptom was the Rails layer's central
|
|
99
|
+
# claim silently failing: `Issue.find` on a `Issue < ApplicationRecord < ActiveRecord::Base` found no
|
|
100
|
+
# row on a sequential run, while a pooled one (whose workers are seeded with the finished table
|
|
101
|
+
# before they fork) worked.
|
|
102
|
+
#
|
|
103
|
+
# Public and declared here beside the other effect surfaces: `rigor effects` builds its snapshot off
|
|
104
|
+
# the run's own vocabulary and has to read the same compiled tables the collection window scanned
|
|
105
|
+
# under.
|
|
106
|
+
def effect_plugin_facts
|
|
107
|
+
table = @project_discovered_superclasses
|
|
108
|
+
return @effect_plugin_facts if @effect_plugin_facts && @effect_plugin_facts_ancestry.equal?(table)
|
|
109
|
+
|
|
110
|
+
@effect_plugin_facts_ancestry = table
|
|
111
|
+
@effect_plugin_facts = Effects::PluginFacts.build(
|
|
112
|
+
@plugin_registry, superclasses: table, includes: @project_discovered_includes
|
|
113
|
+
)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# The merged per-file collections behind {#effect_table} — the *direct* summaries, before the graph
|
|
117
|
+
# closure. #381's snapshot records these, because a diff over direct summaries stays attributable to
|
|
118
|
+
# the pull request's own lines.
|
|
119
|
+
def effect_collection
|
|
120
|
+
Effects::FileCollection.merge_all(effect_collections)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Issue #382 — the same collections keyed by the path that produced each, which is the form the two
|
|
124
|
+
# caches persist: `{ "lib/a.rb" => FileCollection, … }`. The merged {#effect_collection} cannot be
|
|
125
|
+
# persisted per file (merging is where the path is deliberately dropped), and per file is what an
|
|
126
|
+
# ADR-46 recheck needs — it re-collects the changed closure and serves the rest from the snapshot.
|
|
127
|
+
def effect_collections_by_path
|
|
128
|
+
pooled = @pool_coordinator.collected_effects
|
|
129
|
+
pooled.empty? ? @file_effects.dup : @file_effects.merge(pooled)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Issue #382 — adopt persisted collections as this run's own and close the graph over them. The
|
|
133
|
+
# warm-hit half of the whole-run effects slot: the analysis did not run, so `#assemble_run_diagnostics`
|
|
134
|
+
# never reached {#close_effect_graph} and the fixpoint is run here instead. The fixpoint is ALWAYS
|
|
135
|
+
# re-run rather than persisted — it is the cheap half, and a stored table would have to be invalidated
|
|
136
|
+
# by every input a summary has.
|
|
137
|
+
def adopt_effect_collections(collections)
|
|
138
|
+
@file_effects = collections
|
|
139
|
+
close_effect_graph
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Issue #382 — whether this run's effect collections came from the whole-run effects slot rather than
|
|
143
|
+
# from a fresh collection pass. Read by the specs and by nothing in the engine; `#effect_table` reads
|
|
144
|
+
# the same either way, which is the property the slot exists to provide.
|
|
145
|
+
def effects_served_from_cache?
|
|
146
|
+
@effects_served_from_cache
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Which file each effect unit was defined in — `{ "Class#m" => [path, …] }`, sorted, one entry per
|
|
150
|
+
# file that contributes a `def` to the key (a reopening spans several).
|
|
151
|
+
#
|
|
152
|
+
# The merged {#effect_collection} cannot answer this: merging drops the per-file path, deliberately,
|
|
153
|
+
# because a summary is line- and file-free by design. The snapshot's `reach:` table needs it anyway —
|
|
154
|
+
# its entry points are named by *file* globs (`effects.snapshot.reach:`, the `unused --entry-point`
|
|
155
|
+
# syntax), so the key has to be traced back to the file it was written in.
|
|
156
|
+
def effect_sources
|
|
157
|
+
effect_collections.each_with_object({}) do |collection, out|
|
|
158
|
+
path = collection.path
|
|
159
|
+
next if path.nil?
|
|
160
|
+
|
|
161
|
+
collection.summaries.each_key { |key| (out[key] ||= []) << path }
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
71
165
|
# @param configuration [Rigor::Configuration]
|
|
72
166
|
# @param explain [Boolean] surface fail-soft fallback events as `:info` diagnostics.
|
|
73
167
|
# @param cache_store [Rigor::Cache::Store, nil] the persistent cache the runner exposes to producers
|
|
@@ -102,13 +196,16 @@ module Rigor
|
|
|
102
196
|
# discovery tables": {Protection::DiagnosticOracle} threads the table set Tier 2's site filter already
|
|
103
197
|
# judges anchors against, so a site admitted because a sibling-file class resolved is also a site the
|
|
104
198
|
# oracle can kill at. nil (the default) leaves the prebuilt/LSP contract byte-identical.
|
|
199
|
+
# @param no_tolerated_effects [Boolean] ADR-103 WD1 / #385 — `rigor check --no-tolerated-effects`.
|
|
200
|
+
# Judges effect envelopes as if `effects.tolerated:` were empty. A judgment-time switch only: the
|
|
201
|
+
# run, its collection and its cache identity are unchanged.
|
|
105
202
|
def initialize(configuration:, explain: false, # rubocop:disable Metrics/ParameterLists,Metrics/AbcSize,Metrics/MethodLength
|
|
106
203
|
cache_store: Cache::Store.new(root: DEFAULT_CACHE_ROOT),
|
|
107
204
|
plugin_requirer: nil, workers: 0, collect_stats: true,
|
|
108
205
|
buffer: nil, prebuilt: nil, environment: nil,
|
|
109
206
|
record_dependencies: false, record_self_calls: false, analyze_only: nil,
|
|
110
207
|
seed_bundles: nil, collect_seed_bundles: false, param_inferred_types: nil,
|
|
111
|
-
discovery_seed: nil)
|
|
208
|
+
discovery_seed: nil, no_tolerated_effects: false)
|
|
112
209
|
@configuration = configuration
|
|
113
210
|
@explain = explain
|
|
114
211
|
@cache_store = enforce_read_only_cache(cache_store, buffer)
|
|
@@ -128,6 +225,30 @@ module Rigor
|
|
|
128
225
|
# diagnostics are byte-identical.
|
|
129
226
|
@record_self_calls = record_self_calls
|
|
130
227
|
@unresolved_self_calls = {}
|
|
228
|
+
# ADR-103 WD13 — effect collection. Derived from the configuration, never from a flag: the
|
|
229
|
+
# `effects:` block (or an implicit one, which is how `rigor effects` opts in) is the ONLY switch.
|
|
230
|
+
# Observational — diagnostics are byte-identical whichever way it resolves.
|
|
231
|
+
@record_effects = configuration.effects_enabled?
|
|
232
|
+
# ADR-103 WD6 / #385 — the project's `effects.attribution:` table, built once and carried on the
|
|
233
|
+
# collection window so a fork-pool worker scans under the same claims the parent does.
|
|
234
|
+
@effect_attribution = Effects::Attribution.build(configuration.effects_attribution)
|
|
235
|
+
# ADR-103 WD6 / #386 — the call-site envelope index, built lazily off the run's environment
|
|
236
|
+
# (`#effect_envelope_index`) because the strata it reads include the built RBS one.
|
|
237
|
+
@effect_envelope_index = nil
|
|
238
|
+
@effect_envelope_index_env = nil
|
|
239
|
+
# #387 — the compiled plugin effect tables, memoised on the ancestry table they were built from
|
|
240
|
+
# (see `#effect_plugin_facts`).
|
|
241
|
+
@effect_plugin_facts = nil
|
|
242
|
+
@effect_plugin_facts_ancestry = nil
|
|
243
|
+
# ADR-103 WD1 invariant 3 / #385 — `--no-tolerated-effects`, the audit switch. It changes the
|
|
244
|
+
# JUDGMENT only: collection, the propagated table and the cache identity are all untouched, so an
|
|
245
|
+
# audit run and an ordinary one share a cache entry and differ solely in which lane the envelope
|
|
246
|
+
# check reads.
|
|
247
|
+
@no_tolerated_effects = no_tolerated_effects
|
|
248
|
+
@file_effects = {}
|
|
249
|
+
@effect_table = nil
|
|
250
|
+
@effects_served_from_cache = false
|
|
251
|
+
@run_dependency_descriptor = nil
|
|
131
252
|
# Memoised activation decision for the `call.self-undefined-method` rule (nil = not yet computed).
|
|
132
253
|
# See `self_undefined_rule_active?`.
|
|
133
254
|
@self_undefined_rule_active = nil
|
|
@@ -189,6 +310,11 @@ module Rigor
|
|
|
189
310
|
# scope (sequential + fork-worker) through `project_scope_seed_tables`. Empty by default, so the gate-off
|
|
190
311
|
# run carries no table and is byte-identical.
|
|
191
312
|
@project_param_inferred_types = {}.freeze
|
|
313
|
+
# Issue #352 / ADR-17 — the `pre_eval:` constant publication table, populated by
|
|
314
|
+
# `seed_pre_eval_constants` in `assemble_run_diagnostics` and seeded onto every per-file scope
|
|
315
|
+
# (sequential + fork-worker) through `project_scope_seed_tables`. Empty unless the project lists
|
|
316
|
+
# `pre_eval:` files that declare publishable constants, so a project without one is byte-identical.
|
|
317
|
+
@project_pre_eval_constants = {}.freeze
|
|
192
318
|
# ADR-84 WD2 — per-run identity token for the user-method return memo's bucket (see
|
|
193
319
|
# Scope::DiscoveryIndex#run_generation). Minted fresh in `run_analysis` so the memo never serves an
|
|
194
320
|
# entry across a run boundary (LSP re-check, ADR-62 warm loop); nil until the first run so
|
|
@@ -232,6 +358,9 @@ module Rigor
|
|
|
232
358
|
@snapshots.reset_for_run
|
|
233
359
|
# Per-run reset of the deferred-discovery memo (see `#ensure_project_discovery`).
|
|
234
360
|
@project_discovery_done = false
|
|
361
|
+
# Per-run reset of the environment the cacheable path resolves, reused by the envelope pass so a
|
|
362
|
+
# run never builds two.
|
|
363
|
+
@run_environment = nil
|
|
235
364
|
# ADR-84 WD2 — roll the return-memo bucket: a fresh frozen token per run makes every per-file scope
|
|
236
365
|
# of THIS run share one memo bucket while entries from any earlier run in this process (stale after
|
|
237
366
|
# an edit) become unreachable.
|
|
@@ -249,6 +378,16 @@ module Rigor
|
|
|
249
378
|
ensure_project_discovery(expansion) if force_eager_discovery?
|
|
250
379
|
|
|
251
380
|
diagnostics = compute_run_diagnostics(expansion)
|
|
381
|
+
# ADR-103 WD12 / #383 — `effect.envelope-exceeded`, recomputed every run from the (possibly
|
|
382
|
+
# cached) effect table and never stored. It sits OUTSIDE `compute_run_diagnostics` on purpose:
|
|
383
|
+
# the `effects:` block is absent from the diagnostics cache identity, so a finding written into
|
|
384
|
+
# that entry would outlive the configuration that produced it. See {EffectEnvelopePass}.
|
|
385
|
+
diagnostics += effect_envelope_diagnostics(expansion)
|
|
386
|
+
# ADR-103 WD13 commitment 1 / #384 — the mirror image: a project that wrote effect annotations
|
|
387
|
+
# and NO `effects:` block. Same placement rationale (the block is absent from the diagnostics
|
|
388
|
+
# cache identity, so a residual stored there would outlive the edit that answers it), and the
|
|
389
|
+
# opposite gate, so exactly one of the two ever runs.
|
|
390
|
+
diagnostics += effect_annotation_residual_diagnostics
|
|
252
391
|
|
|
253
392
|
Result.new(
|
|
254
393
|
diagnostics: @diagnostic_aggregator.apply_severity_profile(diagnostics),
|
|
@@ -493,19 +632,27 @@ module Rigor
|
|
|
493
632
|
else
|
|
494
633
|
Cache::Descriptor.new
|
|
495
634
|
end
|
|
635
|
+
@run_environment = environment
|
|
496
636
|
key_descriptor = run_key_descriptor(expansion, rbs_descriptor)
|
|
497
637
|
return assemble_run_diagnostics(expansion, environment: environment) if key_descriptor.nil?
|
|
498
638
|
|
|
639
|
+
# ADR-103 WD13 / #382 — the effects slot is consulted FIRST, because it is the one that can force
|
|
640
|
+
# the analysis: a missing or differently-identified sidecar is a miss for effects consumers only,
|
|
641
|
+
# and the only way to re-collect is to re-analyze. When it hits (or collection is off) this returns
|
|
642
|
+
# nil and the diagnostics slot decides on its own, exactly as it did before effects existed.
|
|
643
|
+
analysis = serve_effect_collections(expansion, environment, key_descriptor, rbs_descriptor)
|
|
499
644
|
computed = false
|
|
500
645
|
diagnostics = @cache_store.fetch_or_validate(
|
|
501
646
|
producer_id: RunCacheKey::RUN_DIAGNOSTICS_PRODUCER_ID, key_descriptor: key_descriptor,
|
|
502
647
|
generation_cap: RunCacheKey::GENERATION_CAP
|
|
503
648
|
) do
|
|
504
649
|
computed = true
|
|
505
|
-
diags = assemble_run_diagnostics(expansion, environment: environment)
|
|
650
|
+
diags = analysis || assemble_run_diagnostics(expansion, environment: environment)
|
|
506
651
|
[diags, run_dependency_descriptor(expansion, rbs_descriptor)]
|
|
507
652
|
end
|
|
508
|
-
|
|
653
|
+
# An effects miss that ran the analysis is not a cache-served run even when the diagnostics slot
|
|
654
|
+
# then hit — the stats it would otherwise suppress were all gathered.
|
|
655
|
+
@run_served_from_cache = !computed && analysis.nil?
|
|
509
656
|
diagnostics
|
|
510
657
|
rescue StandardError
|
|
511
658
|
# The result cache must never break a run. If anything in the cache path fails, fall back to a
|
|
@@ -514,6 +661,150 @@ module Rigor
|
|
|
514
661
|
assemble_run_diagnostics(expansion)
|
|
515
662
|
end
|
|
516
663
|
|
|
664
|
+
# ADR-103 WD13 / issue #382 — the whole-run **effects sidecar**, the second of "one cache, two
|
|
665
|
+
# identities, one extra slot". Keyed by {Effects::Identity.descriptor} — the diagnostics key
|
|
666
|
+
# descriptor plus the vocabulary version, the catalogue identity and the `effects:` digest — and
|
|
667
|
+
# validated against the same post-run dependency descriptor the diagnostics slot records, so exactly
|
|
668
|
+
# the file set that invalidates diagnostics invalidates summaries.
|
|
669
|
+
#
|
|
670
|
+
# Returns the diagnostics of the analysis it had to run (a miss — the run has to re-collect, and the
|
|
671
|
+
# only way to collect is to analyze), or nil when it did not have to run one (a hit, or collection
|
|
672
|
+
# off). The diagnostics slot is never written, read, or invalidated from here.
|
|
673
|
+
def serve_effect_collections(expansion, environment, key_descriptor, rbs_descriptor)
|
|
674
|
+
return nil unless @record_effects
|
|
675
|
+
|
|
676
|
+
descriptor = effects_key_descriptor(key_descriptor)
|
|
677
|
+
cached = descriptor && peek_effect_collections(descriptor)
|
|
678
|
+
if cached
|
|
679
|
+
adopt_effect_collections(cached)
|
|
680
|
+
@effects_served_from_cache = true
|
|
681
|
+
return nil
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
analysis = assemble_run_diagnostics(expansion, environment: environment)
|
|
685
|
+
store_effect_collections(descriptor, expansion, rbs_descriptor)
|
|
686
|
+
analysis
|
|
687
|
+
end
|
|
688
|
+
|
|
689
|
+
def effects_key_descriptor(key_descriptor)
|
|
690
|
+
Effects::Identity.descriptor(base: key_descriptor, configuration: @configuration,
|
|
691
|
+
plugin_facts: effect_plugin_facts)
|
|
692
|
+
rescue StandardError
|
|
693
|
+
nil
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
# The read half. A miss, a stale dependency, a corrupt entry and a stored value of the wrong shape
|
|
697
|
+
# are one answer — nil, "collect it again" — because none of them can be told apart from the outside
|
|
698
|
+
# and all of them have the same remedy.
|
|
699
|
+
def peek_effect_collections(descriptor)
|
|
700
|
+
cached = @cache_store.peek_validated(
|
|
701
|
+
producer_id: RunCacheKey::RUN_EFFECTS_PRODUCER_ID, key_descriptor: descriptor
|
|
702
|
+
)
|
|
703
|
+
cached.is_a?(Hash) ? cached : nil
|
|
704
|
+
rescue StandardError
|
|
705
|
+
nil
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
# The write half, run only after a miss, so the block never recomputes anything: it hands over the
|
|
709
|
+
# collections the analysis just produced, validated against the same post-run dependency descriptor
|
|
710
|
+
# the diagnostics slot records. Fail-soft — a collection that will not Marshal (which nothing in
|
|
711
|
+
# {Effects::FileCollection} should be, and the fork pool already proves per file) costs the next run
|
|
712
|
+
# its warm start and nothing else.
|
|
713
|
+
def store_effect_collections(descriptor, expansion, rbs_descriptor)
|
|
714
|
+
return if descriptor.nil?
|
|
715
|
+
|
|
716
|
+
collections = effect_collections_by_path
|
|
717
|
+
@cache_store.fetch_or_validate(
|
|
718
|
+
producer_id: RunCacheKey::RUN_EFFECTS_PRODUCER_ID, key_descriptor: descriptor,
|
|
719
|
+
generation_cap: RunCacheKey::EFFECTS_GENERATION_CAP
|
|
720
|
+
) { [collections, run_dependency_descriptor(expansion, rbs_descriptor)] }
|
|
721
|
+
nil
|
|
722
|
+
rescue StandardError
|
|
723
|
+
nil
|
|
724
|
+
end
|
|
725
|
+
private :serve_effect_collections, :effects_key_descriptor,
|
|
726
|
+
:peek_effect_collections, :store_effect_collections
|
|
727
|
+
|
|
728
|
+
# ADR-103 WD8 / #383 — the envelope check. Nothing at all without an `effects:` block, and nothing
|
|
729
|
+
# under `effects.check: false`; with both, one walk of the project's own RBS for `%a{pure}` /
|
|
730
|
+
# `%a{rigor:v1:effect …}`, and only if that finds an envelope does anything else run (the discovery
|
|
731
|
+
# tables the `def` positions come from are forced from inside the pass, lazily, for that reason).
|
|
732
|
+
#
|
|
733
|
+
# The environment is the one the cacheable path already resolved when there is one, so a warm run
|
|
734
|
+
# reads the envelopes off the loader it built anyway rather than building a second.
|
|
735
|
+
def effect_envelope_diagnostics(expansion)
|
|
736
|
+
return [] unless @record_effects && @configuration.effects_check?
|
|
737
|
+
|
|
738
|
+
EffectEnvelopePass.new(
|
|
739
|
+
configuration: @configuration,
|
|
740
|
+
rbs_loader: envelope_rbs_loader(expansion),
|
|
741
|
+
effect_table: effect_table,
|
|
742
|
+
discovery: -> { envelope_discovery_tables(expansion) },
|
|
743
|
+
sources: @in_memory_sources,
|
|
744
|
+
unit_sources: effect_sources,
|
|
745
|
+
# ADR-103 WD1 / #386 — the nominal relation `effect.liskov-widened` reads, from the collector's
|
|
746
|
+
# own as-written superclass table, so the Liskov check and the closed-world proven lane resolve
|
|
747
|
+
# the same ancestry. Lazy: only a project that declared an envelope pays the merge.
|
|
748
|
+
ancestry: -> { effect_collection.superclasses },
|
|
749
|
+
apply_tolerated: !@no_tolerated_effects,
|
|
750
|
+
# #387 — the same compiled plugin tables the collection window scanned under, so an envelope may
|
|
751
|
+
# name a label a plugin opened and the unknown-label check agrees with the scan.
|
|
752
|
+
plugin_facts: effect_plugin_facts
|
|
753
|
+
).diagnostics
|
|
754
|
+
end
|
|
755
|
+
|
|
756
|
+
# The residual takes the virtual RBS the run ALREADY resolved — never `envelope_rbs_loader`, which
|
|
757
|
+
# builds one on demand. This runs on the effects-off path, where an environment build is a cost
|
|
758
|
+
# the project did not ask for; the `signature_paths:` `.rbs` stratum is always read, and the
|
|
759
|
+
# rbs-inline stratum rides whatever the run happened to have.
|
|
760
|
+
#
|
|
761
|
+
# #441 — two sources, because two shapes of run resolve an environment and only one of them keeps
|
|
762
|
+
# it: `@run_environment` is assigned solely by the ADR-45 result-cacheable path, while every
|
|
763
|
+
# ANALYZING run (`--no-cache`, `--workers N`, `--incremental`, an editor buffer) builds one inside
|
|
764
|
+
# {PoolCoordinator} and lets it go. The snapshot is that run's carrier, and taking it second
|
|
765
|
+
# preserves the cacheable path's byte-identical behaviour.
|
|
766
|
+
def effect_annotation_residual_diagnostics
|
|
767
|
+
EffectAnnotationResidualPass.new(
|
|
768
|
+
configuration: @configuration,
|
|
769
|
+
virtual_rbs: @run_environment&.rbs_loader&.virtual_rbs || @snapshots.effect_annotation_carrier
|
|
770
|
+
).diagnostics
|
|
771
|
+
end
|
|
772
|
+
|
|
773
|
+
def envelope_rbs_loader(expansion)
|
|
774
|
+
environment = @run_environment ||
|
|
775
|
+
@pool_coordinator.resolve_sequential_environment(source_files: target_files(expansion))
|
|
776
|
+
environment&.rbs_loader
|
|
777
|
+
rescue StandardError
|
|
778
|
+
nil
|
|
779
|
+
end
|
|
780
|
+
|
|
781
|
+
def envelope_discovery_tables(expansion)
|
|
782
|
+
ensure_project_discovery(expansion)
|
|
783
|
+
[@project_discovered_def_sources, @project_discovered_singleton_def_sources,
|
|
784
|
+
@project_discovered_class_sources]
|
|
785
|
+
end
|
|
786
|
+
|
|
787
|
+
private :effect_envelope_diagnostics, :effect_annotation_residual_diagnostics,
|
|
788
|
+
:envelope_rbs_loader, :envelope_discovery_tables
|
|
789
|
+
|
|
790
|
+
# ADR-103 WD13 — fail-soft at the run level too: a propagator that raises leaves the table empty and
|
|
791
|
+
# the run untouched. `Propagator.propagate` already swallows its own failures; this guards the merge.
|
|
792
|
+
def close_effect_graph
|
|
793
|
+
return unless @record_effects
|
|
794
|
+
|
|
795
|
+
@effect_table = Effects::Propagator.propagate(effect_collection, discharge: effect_discharge)
|
|
796
|
+
rescue StandardError
|
|
797
|
+
@effect_table = Effects::EffectTable.empty
|
|
798
|
+
end
|
|
799
|
+
|
|
800
|
+
# ADR-103 WD14 / #385 — the `effects.tolerated:` policy the propagator's second (undischarged) lane
|
|
801
|
+
# is computed under. Built from the configuration and never from the audit flag: BOTH lanes are in
|
|
802
|
+
# the table, and `--no-tolerated-effects` picks the other one at judgment time, so one fixpoint
|
|
803
|
+
# serves an audit run and an ordinary one alike.
|
|
804
|
+
def effect_discharge
|
|
805
|
+
@effect_discharge ||= Effects::Discharge.new(@configuration.effects_tolerated)
|
|
806
|
+
end
|
|
807
|
+
|
|
517
808
|
def assemble_run_diagnostics(expansion, environment: nil)
|
|
518
809
|
# Force the deferred cross-file discovery pre-pass on the analysis (miss) path. Memoised, so the
|
|
519
810
|
# eager force in `#run` (recording / subset modes) makes this a no-op. A warm cache HIT never calls
|
|
@@ -526,6 +817,11 @@ module Rigor
|
|
|
526
817
|
# assembles). Gate off → no-op, and `environment` is left untouched so its lazy build timing is
|
|
527
818
|
# unchanged. Returns the resolved environment so the sequential dispatch reuses it (no double build).
|
|
528
819
|
environment = seed_parameter_inference(expansion, environment)
|
|
820
|
+
# Issue #352 — the `pre_eval:` constant publication pre-pass. Same placement rationale as the line
|
|
821
|
+
# above: it runs on the parent BEFORE the pool split so every worker sees the same frozen table, and
|
|
822
|
+
# only on the analysis (miss) path. No `pre_eval:` entry → no-op, and `environment` passes through
|
|
823
|
+
# untouched so its lazy build timing is unchanged.
|
|
824
|
+
environment = seed_pre_eval_constants(expansion, environment)
|
|
529
825
|
diagnostics = @diagnostic_aggregator.pre_file_diagnostics(expansion)
|
|
530
826
|
# ADR-46 — record which project files this run actually analyzed (the `analyze_only` subset, or
|
|
531
827
|
# all of them). The incremental orchestrator serves every analyzed-but-not-affected file from the
|
|
@@ -533,6 +829,11 @@ module Rigor
|
|
|
533
829
|
targets = target_files(expansion)
|
|
534
830
|
@analyzed_files = targets
|
|
535
831
|
diagnostics += @pool_coordinator.analyze_files(targets, environment: environment)
|
|
832
|
+
# ADR-103 WD12 — the effect fixpoint, in the post-pool aggregation slot beside the conformance
|
|
833
|
+
# results. Graph-only over a finite lattice, so it is a plain worklist to a true fixpoint; it
|
|
834
|
+
# contributes NO diagnostics and its result leaves through `#effect_table`, never through the
|
|
835
|
+
# stream. Skipped entirely when collection did not run.
|
|
836
|
+
close_effect_graph
|
|
536
837
|
diagnostics += @diagnostic_aggregator.rbs_quarantined_signature_diagnostics
|
|
537
838
|
diagnostics += @diagnostic_aggregator.rbs_environment_build_failed_diagnostics
|
|
538
839
|
diagnostics += @diagnostic_aggregator.rbs_synthesized_namespace_diagnostics
|
|
@@ -572,6 +873,34 @@ module Rigor
|
|
|
572
873
|
environment
|
|
573
874
|
end
|
|
574
875
|
|
|
876
|
+
# Issue #352 / ADR-17 — the `pre_eval:` CONSTANT publication pre-pass, the twin of the slice-2 patched-
|
|
877
|
+
# METHOD registry the eager pre-passes already build. Walks each listed file's constant writes with the
|
|
878
|
+
# per-file pre-pass ({Inference::ScopeIndexer.build_in_source_constants}) under a project-seeded scope,
|
|
879
|
+
# widens each result to its erased class, and populates `@project_pre_eval_constants` — which
|
|
880
|
+
# `project_scope_seed_tables` then seeds onto every per-file scope, so `TIMEOUT = 30` in a listed file
|
|
881
|
+
# reads as `Integer` instead of `Dynamic[top]` in its consumers. The widening + multi-file conflict rules
|
|
882
|
+
# live in {Inference::PreEvalConstants}.
|
|
883
|
+
#
|
|
884
|
+
# Runs here rather than in {ProjectPrePasses#run} because typing a constant's rvalue needs the RBS
|
|
885
|
+
# environment, which the eager pre-passes feed rather than consume. Fails soft — a collector error must
|
|
886
|
+
# never break a run, so the table stays empty and the run proceeds exactly as it does today.
|
|
887
|
+
def seed_pre_eval_constants(expansion, environment)
|
|
888
|
+
paths = @configuration.pre_eval.select { |path| File.file?(path) }
|
|
889
|
+
return environment if paths.empty?
|
|
890
|
+
|
|
891
|
+
environment ||= @pool_coordinator.resolve_sequential_environment(source_files: expansion.fetch(:files))
|
|
892
|
+
@project_pre_eval_constants = Inference::PreEvalConstants.collect(
|
|
893
|
+
paths: paths, target_ruby: @configuration.target_ruby, buffer: @buffer,
|
|
894
|
+
scope_builder: lambda { |path|
|
|
895
|
+
seed_project_scope(Scope.empty(environment: environment, source_path: path))
|
|
896
|
+
}
|
|
897
|
+
)
|
|
898
|
+
environment
|
|
899
|
+
rescue StandardError
|
|
900
|
+
@project_pre_eval_constants = {}.freeze
|
|
901
|
+
environment
|
|
902
|
+
end
|
|
903
|
+
|
|
575
904
|
# A cache hit skipped the analysis, so the per-run stats (wall split, RBS-class counts, …) were never
|
|
576
905
|
# gathered — report none rather than the stale snapshot defaults.
|
|
577
906
|
def stats_for_run(wall_started_at:, expansion:)
|
|
@@ -592,6 +921,11 @@ module Rigor
|
|
|
592
921
|
# share the full run's result key (`run_key_descriptor` keys on the whole expansion, not the
|
|
593
922
|
# subset), so serving one as the other would manufacture a wrong result. Both instead use the
|
|
594
923
|
# store for the RBS-env + plugin-producer tiers, where the incremental win actually lives.
|
|
924
|
+
# ADR-103 WD13 / issue #382 — a COLLECTING run is no longer excluded. It was, for the same reason a
|
|
925
|
+
# `record_dependencies` run is (a cache-served run collects nothing, so `#effect_table` would come
|
|
926
|
+
# back empty from a warm hit), and the sidecar slot is what lifts it: the collections ride their own
|
|
927
|
+
# entry under their own identity, so a warm run restores them and re-runs only the fixpoint. A run
|
|
928
|
+
# with collection OFF never reads or writes that slot and is byte-identical here.
|
|
595
929
|
def run_result_cacheable?
|
|
596
930
|
!@cache_store.nil? && !@cache_store.read_only? &&
|
|
597
931
|
@buffer.nil? && @prebuilt.nil? && !pool_mode? &&
|
|
@@ -615,8 +949,16 @@ module Rigor
|
|
|
615
949
|
# Files the run actually depended on, collected AFTER it ran: every analyzed file, every RBS `sig`
|
|
616
950
|
# file (`rbs_descriptor.files`), and every file each plugin read (complete post-run, so reads made
|
|
617
951
|
# mid-analysis are included). Re-digested on the next run by {Descriptor#fresh?}.
|
|
952
|
+
# Memoised because a collecting run asks twice — once to validate the effects sidecar it just wrote,
|
|
953
|
+
# once for the diagnostics entry — and this is a whole-project stat + digest walk. Both asks happen
|
|
954
|
+
# after the same analysis, so they are answering about the same world; a run with collection off asks
|
|
955
|
+
# once and the memo is inert.
|
|
618
956
|
def run_dependency_descriptor(expansion, rbs_descriptor)
|
|
619
|
-
|
|
957
|
+
@run_dependency_descriptor ||= build_run_dependency_descriptor(expansion, rbs_descriptor)
|
|
958
|
+
end
|
|
959
|
+
|
|
960
|
+
def build_run_dependency_descriptor(expansion, rbs_descriptor)
|
|
961
|
+
entries = analyzed_file_entries(expansion) + pre_eval_file_entries + rbs_descriptor.files
|
|
620
962
|
@plugin_registry.plugins.each do |plugin|
|
|
621
963
|
# Read the boundary WITHOUT triggering its lazy `@io_boundary ||=` initializer: plugin instances
|
|
622
964
|
# are frozen after the run, and a plugin that never built a boundary read no files through it,
|
|
@@ -636,6 +978,21 @@ module Rigor
|
|
|
636
978
|
end
|
|
637
979
|
end
|
|
638
980
|
|
|
981
|
+
# Issue #352 — a `pre_eval:` file is a real input to the run's diagnostics: its `def`s populate the
|
|
982
|
+
# ADR-17 patched-method registry and (since #352) its constants populate the project seed. ADR-17 WD5
|
|
983
|
+
# permits listing a file under `pre_eval:` and NOT under `paths:`, and such a file never appears in the
|
|
984
|
+
# expansion — so without this entry, editing one would leave the run-result cache serving diagnostics
|
|
985
|
+
# computed against the previous version. The common case (a `lib/core_ext/` file that is also under
|
|
986
|
+
# `paths:`) contributes a duplicate entry, which the descriptor's per-path validation absorbs.
|
|
987
|
+
def pre_eval_file_entries
|
|
988
|
+
@configuration.pre_eval.filter_map do |path|
|
|
989
|
+
physical = @buffer ? @buffer.resolve(path) : path
|
|
990
|
+
next unless File.file?(physical)
|
|
991
|
+
|
|
992
|
+
Cache::Descriptor::FileEntry.stat(path: physical, digest: Cache::FileDigest.hexdigest(physical))
|
|
993
|
+
end
|
|
994
|
+
end
|
|
995
|
+
|
|
639
996
|
# Runs every project-wide pre-pass (`load_plugins` +
|
|
640
997
|
# `plugin#prepare` + dependency-source builder +
|
|
641
998
|
# synthetic-method scanner + project-patched scanner)
|
|
@@ -798,6 +1155,13 @@ module Rigor
|
|
|
798
1155
|
|
|
799
1156
|
private
|
|
800
1157
|
|
|
1158
|
+
# The run's per-file effect collections in sorted path order — the sequential half merged with the
|
|
1159
|
+
# pool's, exactly as {#file_dependencies} reconciles its two halves. Sorted, because the fold below
|
|
1160
|
+
# it must not depend on pool-completion order.
|
|
1161
|
+
def effect_collections
|
|
1162
|
+
effect_collections_by_path.sort_by { |path, _| path.to_s }.map(&:last)
|
|
1163
|
+
end
|
|
1164
|
+
|
|
801
1165
|
# Editor mode § "Scope choice — option A". Under `buffer:` non-nil the per-file analysis emits
|
|
802
1166
|
# diagnostics ONLY for the buffer's logical path; the rest of `paths:` is consumed by the
|
|
803
1167
|
# project-wide pre-passes (synthetic methods, project-patched methods, plugin facts) but contributes
|
|
@@ -1106,10 +1470,7 @@ module Rigor
|
|
|
1106
1470
|
tables[:discovered_method_visibilities] = @project_discovered_method_visibilities
|
|
1107
1471
|
end
|
|
1108
1472
|
tables[:discovered_methods] = @project_discovered_methods unless @project_discovered_methods.empty?
|
|
1109
|
-
|
|
1110
|
-
# scope seeds inferred parameters identically to the sequential path. Empty (and absent) unless the
|
|
1111
|
-
# `parameter_inference:` gate ran the pre-pass.
|
|
1112
|
-
tables[:param_inferred_types] = @project_param_inferred_types unless @project_param_inferred_types.empty?
|
|
1473
|
+
seed_opt_in_pre_pass_tables(tables)
|
|
1113
1474
|
seed_member_layout_tables(tables)
|
|
1114
1475
|
# ADR-46 slice 1 — the class-declaration source map is read only by the ancestry accessors during
|
|
1115
1476
|
# dependency recording, so seed it only when recording is on; a normal run never carries it.
|
|
@@ -1126,6 +1487,23 @@ module Rigor
|
|
|
1126
1487
|
@discovery_seed ? @discovery_seed.dup : {}
|
|
1127
1488
|
end
|
|
1128
1489
|
|
|
1490
|
+
# Seeds the two OPT-IN pre-pass tables — the ones an ordinary run leaves empty, so they are absent from
|
|
1491
|
+
# the seed entirely unless the project asked for them. Both ride this seed so a pooled `WorkerSession`
|
|
1492
|
+
# scope resolves identically to the sequential path (ADR-15 sequential equivalence).
|
|
1493
|
+
#
|
|
1494
|
+
# - ADR-67 WD6a `param_inferred_types`: the call-site parameter-inference table, populated only when the
|
|
1495
|
+
# `parameter_inference:` gate ran the pre-pass.
|
|
1496
|
+
# - Issue #352 `in_source_constants`: the `pre_eval:` constant publication, populated only when the
|
|
1497
|
+
# project lists `pre_eval:` files that declare publishable constants.
|
|
1498
|
+
#
|
|
1499
|
+
# Extracted to keep {#project_scope_seed_tables} under the complexity budget.
|
|
1500
|
+
def seed_opt_in_pre_pass_tables(tables)
|
|
1501
|
+
tables[:param_inferred_types] = @project_param_inferred_types unless @project_param_inferred_types.empty?
|
|
1502
|
+
return if @project_pre_eval_constants.empty?
|
|
1503
|
+
|
|
1504
|
+
tables[:in_source_constants] = @project_pre_eval_constants
|
|
1505
|
+
end
|
|
1506
|
+
|
|
1129
1507
|
# ADR-46 — seed the instance + singleton `"path:line"` def-source tables (each only when non-empty).
|
|
1130
1508
|
# Extracted to keep {#project_scope_seed_tables} under the complexity budget. The singleton table (slice 4
|
|
1131
1509
|
# extension) rides the same seed so a pooled `WorkerSession` records singleton symbol edges identically.
|
|
@@ -1151,16 +1529,52 @@ module Rigor
|
|
|
1151
1529
|
# `DependencyRecorder.active? == false`. Recording is observational, so diagnostics are byte-identical
|
|
1152
1530
|
# either way.
|
|
1153
1531
|
def analyze_file(path, environment)
|
|
1154
|
-
return
|
|
1532
|
+
return analyze_with_effects(path, environment) unless @record_dependencies
|
|
1155
1533
|
|
|
1156
1534
|
diagnostics = nil
|
|
1157
1535
|
record = DependencyRecorder.record_for(path) do
|
|
1158
|
-
diagnostics =
|
|
1536
|
+
diagnostics = analyze_with_effects(path, environment)
|
|
1159
1537
|
end
|
|
1160
1538
|
@file_dependencies[path] = record
|
|
1161
1539
|
diagnostics
|
|
1162
1540
|
end
|
|
1163
1541
|
|
|
1542
|
+
# ADR-103 WD13 — the effect-collection window, in the same shape as the dependency one and nested
|
|
1543
|
+
# inside it so a recording incremental run collects both. Off by default: `effects_enabled?` false
|
|
1544
|
+
# calls the body directly, {Effects::Collector.active?} stays false, and the recorder's sites on the
|
|
1545
|
+
# dispatch path short-circuit on one integer read.
|
|
1546
|
+
def analyze_with_effects(path, environment)
|
|
1547
|
+
return analyze_file_body(path, environment) unless @record_effects
|
|
1548
|
+
|
|
1549
|
+
diagnostics = nil
|
|
1550
|
+
collection = Effects::Collector.collect_for(
|
|
1551
|
+
path, attribution: @effect_attribution, envelopes: effect_envelope_index(environment),
|
|
1552
|
+
plugin_facts: effect_plugin_facts
|
|
1553
|
+
) do
|
|
1554
|
+
diagnostics = analyze_file_body(path, environment)
|
|
1555
|
+
end
|
|
1556
|
+
@file_effects[path] = collection
|
|
1557
|
+
diagnostics
|
|
1558
|
+
end
|
|
1559
|
+
|
|
1560
|
+
# ADR-103 WD6 / #386 — the envelopes a call site may import as a `≤` bound, built once per process
|
|
1561
|
+
# over the first environment the analysis resolves and reused for every later file. A worker builds
|
|
1562
|
+
# its own from the same configuration and the same signature content ({WorkerSession}), so the two
|
|
1563
|
+
# agree without a channel to keep in sync.
|
|
1564
|
+
#
|
|
1565
|
+
# Memoised on the environment's identity rather than unconditionally: a run that resolves its
|
|
1566
|
+
# environment lazily hands `nil` to the first files, and an index built from `nil` would silently
|
|
1567
|
+
# pin the whole run to the strata a loader-less build can read.
|
|
1568
|
+
def effect_envelope_index(environment)
|
|
1569
|
+
return @effect_envelope_index if @effect_envelope_index && @effect_envelope_index_env.equal?(environment)
|
|
1570
|
+
|
|
1571
|
+
@effect_envelope_index_env = environment
|
|
1572
|
+
@effect_envelope_index = Effects::EnvelopeIndex.build(
|
|
1573
|
+
configuration: @configuration, environment: environment, plugin_facts: effect_plugin_facts
|
|
1574
|
+
)
|
|
1575
|
+
end
|
|
1576
|
+
private :effect_envelope_index
|
|
1577
|
+
|
|
1164
1578
|
def analyze_file_body(path, environment) # rubocop:disable Metrics/MethodLength
|
|
1165
1579
|
parse_result = parse_source(path)
|
|
1166
1580
|
unless parse_result.errors.empty?
|
|
@@ -1169,6 +1583,11 @@ module Rigor
|
|
|
1169
1583
|
return parse_diagnostics(path, parse_result)
|
|
1170
1584
|
end
|
|
1171
1585
|
|
|
1586
|
+
# ADR-103 WD13 — hand the effect collector the root the typer is about to walk, so its per-def
|
|
1587
|
+
# scan runs over exactly what was typed. Guarded inside the recorder rather than by `active?`: this
|
|
1588
|
+
# is a per-FILE site, so one `Thread.current` read is already nothing. The per-dispatch site in
|
|
1589
|
+
# `ExpressionTyper#call_type_for` is the one that pays for the integer fast path.
|
|
1590
|
+
Effects::Collector.record_root(parse_result.value)
|
|
1172
1591
|
scope = seed_project_scope(Scope.empty(environment: environment, source_path: path))
|
|
1173
1592
|
# ADR-24 slice 4a/4 — record unresolved implicit-self calls during the typing pass ONLY (not
|
|
1174
1593
|
# CheckRules, whose own `type_of` queries would otherwise re-trigger the choke-point).
|