rigortype 0.3.2 → 0.3.4
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 +843 -0
- data/data/effects/registry.yml +67 -0
- data/docs/handbook/07-rbs-and-extended.md +106 -5
- data/docs/manual/02-cli-reference.md +311 -1
- data/docs/manual/03-configuration.md +84 -1
- data/docs/manual/04-diagnostics.md +14 -2
- data/docs/manual/16-rbs-extended-annotations.md +77 -0
- data/docs/manual/18-removing-dead-code.md +326 -0
- data/docs/manual/README.md +5 -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/always_truthy_condition_collector.rb +8 -0
- data/lib/rigor/analysis/check_rules/declaration_sourced_guard.rb +53 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +28 -2
- data/lib/rigor/analysis/check_rules/rule_walk.rb +7 -0
- data/lib/rigor/analysis/check_rules.rb +62 -22
- 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 +163 -1
- data/lib/rigor/analysis/run_cache_key.rb +16 -0
- data/lib/rigor/analysis/runner/declaration_position.rb +54 -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 +68 -10
- data/lib/rigor/analysis/runner.rb +420 -10
- data/lib/rigor/analysis/severity_stamp.rb +2 -1
- data/lib/rigor/analysis/worker_session.rb +58 -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 +131 -0
- data/lib/rigor/cli/effects_diff_renderer.rb +123 -0
- data/lib/rigor/cli/effects_explain_renderer.rb +68 -0
- data/lib/rigor/cli/effects_renderer.rb +54 -0
- data/lib/rigor/cli/effects_report.rb +53 -0
- data/lib/rigor/cli/effects_snapshot_command.rb +256 -0
- data/lib/rigor/cli/unused_command.rb +288 -0
- data/lib/rigor/cli.rb +16 -0
- data/lib/rigor/configuration/severity_profile.rb +23 -0
- data/lib/rigor/configuration.rb +296 -14
- data/lib/rigor/effects/attribution.rb +76 -0
- data/lib/rigor/effects/catalog.rb +275 -0
- data/lib/rigor/effects/collector.rb +182 -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 +93 -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 +160 -0
- data/lib/rigor/effects/framework_units.rb +167 -0
- data/lib/rigor/effects/identity.rb +104 -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 +335 -0
- data/lib/rigor/effects/propagator.rb +275 -0
- data/lib/rigor/effects/registry.rb +193 -0
- data/lib/rigor/effects/scanner.rb +290 -0
- data/lib/rigor/effects/signature_sources.rb +74 -0
- data/lib/rigor/effects/snapshot.rb +380 -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 +38 -0
- data/lib/rigor/effects/unit_scan.rb +572 -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/anonymous_meta_class.rb +81 -0
- data/lib/rigor/inference/expression_typer.rb +74 -17
- data/lib/rigor/inference/fork_map.rb +45 -11
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +147 -17
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +97 -2
- data/lib/rigor/inference/method_dispatcher.rb +41 -10
- data/lib/rigor/inference/optimistic_origin.rb +68 -3
- data/lib/rigor/inference/pre_eval_constants.rb +189 -0
- data/lib/rigor/inference/scope_indexer.rb +134 -31
- data/lib/rigor/inference/singleton_object_constant.rb +63 -0
- data/lib/rigor/inference/statement_evaluator.rb +25 -12
- data/lib/rigor/plugin/base.rb +24 -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 +103 -3
- data/lib/rigor/plugin/node_rule_walk.rb +6 -0
- data/lib/rigor/plugin/registry.rb +48 -0
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/protection/closure_kill_oracle.rb +20 -1
- data/lib/rigor/rbs_extended/envelope_scanner.rb +160 -0
- data/lib/rigor/rbs_extended.rb +168 -0
- data/lib/rigor/reflection.rb +131 -30
- data/lib/rigor/scope.rb +62 -2
- data/lib/rigor/source/node_walker.rb +12 -0
- data/lib/rigor/type/anonymous_class_name.rb +40 -0
- data/lib/rigor/type/nominal.rb +5 -2
- data/lib/rigor/type/singleton.rb +4 -1
- 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 +385 -28
- 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-sidekiq/lib/rigor/plugin/sidekiq/schedule_scan.rb +122 -0
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +56 -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/scope.rbs +4 -0
- data/sig/rigor.rbs +21 -1
- data/skills/rigor-unused-adjudicate/SKILL.md +90 -0
- metadata +83 -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,90 @@ 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(@plugin_registry, superclasses: table)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# The merged per-file collections behind {#effect_table} — the *direct* summaries, before the graph
|
|
115
|
+
# closure. #381's snapshot records these, because a diff over direct summaries stays attributable to
|
|
116
|
+
# the pull request's own lines.
|
|
117
|
+
def effect_collection
|
|
118
|
+
Effects::FileCollection.merge_all(effect_collections)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Issue #382 — the same collections keyed by the path that produced each, which is the form the two
|
|
122
|
+
# caches persist: `{ "lib/a.rb" => FileCollection, … }`. The merged {#effect_collection} cannot be
|
|
123
|
+
# persisted per file (merging is where the path is deliberately dropped), and per file is what an
|
|
124
|
+
# ADR-46 recheck needs — it re-collects the changed closure and serves the rest from the snapshot.
|
|
125
|
+
def effect_collections_by_path
|
|
126
|
+
pooled = @pool_coordinator.collected_effects
|
|
127
|
+
pooled.empty? ? @file_effects.dup : @file_effects.merge(pooled)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Issue #382 — adopt persisted collections as this run's own and close the graph over them. The
|
|
131
|
+
# warm-hit half of the whole-run effects slot: the analysis did not run, so `#assemble_run_diagnostics`
|
|
132
|
+
# never reached {#close_effect_graph} and the fixpoint is run here instead. The fixpoint is ALWAYS
|
|
133
|
+
# re-run rather than persisted — it is the cheap half, and a stored table would have to be invalidated
|
|
134
|
+
# by every input a summary has.
|
|
135
|
+
def adopt_effect_collections(collections)
|
|
136
|
+
@file_effects = collections
|
|
137
|
+
close_effect_graph
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Issue #382 — whether this run's effect collections came from the whole-run effects slot rather than
|
|
141
|
+
# from a fresh collection pass. Read by the specs and by nothing in the engine; `#effect_table` reads
|
|
142
|
+
# the same either way, which is the property the slot exists to provide.
|
|
143
|
+
def effects_served_from_cache?
|
|
144
|
+
@effects_served_from_cache
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Which file each effect unit was defined in — `{ "Class#m" => [path, …] }`, sorted, one entry per
|
|
148
|
+
# file that contributes a `def` to the key (a reopening spans several).
|
|
149
|
+
#
|
|
150
|
+
# The merged {#effect_collection} cannot answer this: merging drops the per-file path, deliberately,
|
|
151
|
+
# because a summary is line- and file-free by design. The snapshot's `reach:` table needs it anyway —
|
|
152
|
+
# its entry points are named by *file* globs (`effects.snapshot.reach:`, the `unused --entry-point`
|
|
153
|
+
# syntax), so the key has to be traced back to the file it was written in.
|
|
154
|
+
def effect_sources
|
|
155
|
+
effect_collections.each_with_object({}) do |collection, out|
|
|
156
|
+
path = collection.path
|
|
157
|
+
next if path.nil?
|
|
158
|
+
|
|
159
|
+
collection.summaries.each_key { |key| (out[key] ||= []) << path }
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
71
163
|
# @param configuration [Rigor::Configuration]
|
|
72
164
|
# @param explain [Boolean] surface fail-soft fallback events as `:info` diagnostics.
|
|
73
165
|
# @param cache_store [Rigor::Cache::Store, nil] the persistent cache the runner exposes to producers
|
|
@@ -102,13 +194,16 @@ module Rigor
|
|
|
102
194
|
# discovery tables": {Protection::DiagnosticOracle} threads the table set Tier 2's site filter already
|
|
103
195
|
# judges anchors against, so a site admitted because a sibling-file class resolved is also a site the
|
|
104
196
|
# oracle can kill at. nil (the default) leaves the prebuilt/LSP contract byte-identical.
|
|
197
|
+
# @param no_tolerated_effects [Boolean] ADR-103 WD1 / #385 — `rigor check --no-tolerated-effects`.
|
|
198
|
+
# Judges effect envelopes as if `effects.tolerated:` were empty. A judgment-time switch only: the
|
|
199
|
+
# run, its collection and its cache identity are unchanged.
|
|
105
200
|
def initialize(configuration:, explain: false, # rubocop:disable Metrics/ParameterLists,Metrics/AbcSize,Metrics/MethodLength
|
|
106
201
|
cache_store: Cache::Store.new(root: DEFAULT_CACHE_ROOT),
|
|
107
202
|
plugin_requirer: nil, workers: 0, collect_stats: true,
|
|
108
203
|
buffer: nil, prebuilt: nil, environment: nil,
|
|
109
204
|
record_dependencies: false, record_self_calls: false, analyze_only: nil,
|
|
110
205
|
seed_bundles: nil, collect_seed_bundles: false, param_inferred_types: nil,
|
|
111
|
-
discovery_seed: nil)
|
|
206
|
+
discovery_seed: nil, no_tolerated_effects: false)
|
|
112
207
|
@configuration = configuration
|
|
113
208
|
@explain = explain
|
|
114
209
|
@cache_store = enforce_read_only_cache(cache_store, buffer)
|
|
@@ -128,6 +223,30 @@ module Rigor
|
|
|
128
223
|
# diagnostics are byte-identical.
|
|
129
224
|
@record_self_calls = record_self_calls
|
|
130
225
|
@unresolved_self_calls = {}
|
|
226
|
+
# ADR-103 WD13 — effect collection. Derived from the configuration, never from a flag: the
|
|
227
|
+
# `effects:` block (or an implicit one, which is how `rigor effects` opts in) is the ONLY switch.
|
|
228
|
+
# Observational — diagnostics are byte-identical whichever way it resolves.
|
|
229
|
+
@record_effects = configuration.effects_enabled?
|
|
230
|
+
# ADR-103 WD6 / #385 — the project's `effects.attribution:` table, built once and carried on the
|
|
231
|
+
# collection window so a fork-pool worker scans under the same claims the parent does.
|
|
232
|
+
@effect_attribution = Effects::Attribution.build(configuration.effects_attribution)
|
|
233
|
+
# ADR-103 WD6 / #386 — the call-site envelope index, built lazily off the run's environment
|
|
234
|
+
# (`#effect_envelope_index`) because the strata it reads include the built RBS one.
|
|
235
|
+
@effect_envelope_index = nil
|
|
236
|
+
@effect_envelope_index_env = nil
|
|
237
|
+
# #387 — the compiled plugin effect tables, memoised on the ancestry table they were built from
|
|
238
|
+
# (see `#effect_plugin_facts`).
|
|
239
|
+
@effect_plugin_facts = nil
|
|
240
|
+
@effect_plugin_facts_ancestry = nil
|
|
241
|
+
# ADR-103 WD1 invariant 3 / #385 — `--no-tolerated-effects`, the audit switch. It changes the
|
|
242
|
+
# JUDGMENT only: collection, the propagated table and the cache identity are all untouched, so an
|
|
243
|
+
# audit run and an ordinary one share a cache entry and differ solely in which lane the envelope
|
|
244
|
+
# check reads.
|
|
245
|
+
@no_tolerated_effects = no_tolerated_effects
|
|
246
|
+
@file_effects = {}
|
|
247
|
+
@effect_table = nil
|
|
248
|
+
@effects_served_from_cache = false
|
|
249
|
+
@run_dependency_descriptor = nil
|
|
131
250
|
# Memoised activation decision for the `call.self-undefined-method` rule (nil = not yet computed).
|
|
132
251
|
# See `self_undefined_rule_active?`.
|
|
133
252
|
@self_undefined_rule_active = nil
|
|
@@ -189,6 +308,11 @@ module Rigor
|
|
|
189
308
|
# scope (sequential + fork-worker) through `project_scope_seed_tables`. Empty by default, so the gate-off
|
|
190
309
|
# run carries no table and is byte-identical.
|
|
191
310
|
@project_param_inferred_types = {}.freeze
|
|
311
|
+
# Issue #352 / ADR-17 — the `pre_eval:` constant publication table, populated by
|
|
312
|
+
# `seed_pre_eval_constants` in `assemble_run_diagnostics` and seeded onto every per-file scope
|
|
313
|
+
# (sequential + fork-worker) through `project_scope_seed_tables`. Empty unless the project lists
|
|
314
|
+
# `pre_eval:` files that declare publishable constants, so a project without one is byte-identical.
|
|
315
|
+
@project_pre_eval_constants = {}.freeze
|
|
192
316
|
# ADR-84 WD2 — per-run identity token for the user-method return memo's bucket (see
|
|
193
317
|
# Scope::DiscoveryIndex#run_generation). Minted fresh in `run_analysis` so the memo never serves an
|
|
194
318
|
# entry across a run boundary (LSP re-check, ADR-62 warm loop); nil until the first run so
|
|
@@ -232,6 +356,9 @@ module Rigor
|
|
|
232
356
|
@snapshots.reset_for_run
|
|
233
357
|
# Per-run reset of the deferred-discovery memo (see `#ensure_project_discovery`).
|
|
234
358
|
@project_discovery_done = false
|
|
359
|
+
# Per-run reset of the environment the cacheable path resolves, reused by the envelope pass so a
|
|
360
|
+
# run never builds two.
|
|
361
|
+
@run_environment = nil
|
|
235
362
|
# ADR-84 WD2 — roll the return-memo bucket: a fresh frozen token per run makes every per-file scope
|
|
236
363
|
# of THIS run share one memo bucket while entries from any earlier run in this process (stale after
|
|
237
364
|
# an edit) become unreachable.
|
|
@@ -249,6 +376,16 @@ module Rigor
|
|
|
249
376
|
ensure_project_discovery(expansion) if force_eager_discovery?
|
|
250
377
|
|
|
251
378
|
diagnostics = compute_run_diagnostics(expansion)
|
|
379
|
+
# ADR-103 WD12 / #383 — `effect.envelope-exceeded`, recomputed every run from the (possibly
|
|
380
|
+
# cached) effect table and never stored. It sits OUTSIDE `compute_run_diagnostics` on purpose:
|
|
381
|
+
# the `effects:` block is absent from the diagnostics cache identity, so a finding written into
|
|
382
|
+
# that entry would outlive the configuration that produced it. See {EffectEnvelopePass}.
|
|
383
|
+
diagnostics += effect_envelope_diagnostics(expansion)
|
|
384
|
+
# ADR-103 WD13 commitment 1 / #384 — the mirror image: a project that wrote effect annotations
|
|
385
|
+
# and NO `effects:` block. Same placement rationale (the block is absent from the diagnostics
|
|
386
|
+
# cache identity, so a residual stored there would outlive the edit that answers it), and the
|
|
387
|
+
# opposite gate, so exactly one of the two ever runs.
|
|
388
|
+
diagnostics += effect_annotation_residual_diagnostics
|
|
252
389
|
|
|
253
390
|
Result.new(
|
|
254
391
|
diagnostics: @diagnostic_aggregator.apply_severity_profile(diagnostics),
|
|
@@ -493,19 +630,27 @@ module Rigor
|
|
|
493
630
|
else
|
|
494
631
|
Cache::Descriptor.new
|
|
495
632
|
end
|
|
633
|
+
@run_environment = environment
|
|
496
634
|
key_descriptor = run_key_descriptor(expansion, rbs_descriptor)
|
|
497
635
|
return assemble_run_diagnostics(expansion, environment: environment) if key_descriptor.nil?
|
|
498
636
|
|
|
637
|
+
# ADR-103 WD13 / #382 — the effects slot is consulted FIRST, because it is the one that can force
|
|
638
|
+
# the analysis: a missing or differently-identified sidecar is a miss for effects consumers only,
|
|
639
|
+
# and the only way to re-collect is to re-analyze. When it hits (or collection is off) this returns
|
|
640
|
+
# nil and the diagnostics slot decides on its own, exactly as it did before effects existed.
|
|
641
|
+
analysis = serve_effect_collections(expansion, environment, key_descriptor, rbs_descriptor)
|
|
499
642
|
computed = false
|
|
500
643
|
diagnostics = @cache_store.fetch_or_validate(
|
|
501
644
|
producer_id: RunCacheKey::RUN_DIAGNOSTICS_PRODUCER_ID, key_descriptor: key_descriptor,
|
|
502
645
|
generation_cap: RunCacheKey::GENERATION_CAP
|
|
503
646
|
) do
|
|
504
647
|
computed = true
|
|
505
|
-
diags = assemble_run_diagnostics(expansion, environment: environment)
|
|
648
|
+
diags = analysis || assemble_run_diagnostics(expansion, environment: environment)
|
|
506
649
|
[diags, run_dependency_descriptor(expansion, rbs_descriptor)]
|
|
507
650
|
end
|
|
508
|
-
|
|
651
|
+
# An effects miss that ran the analysis is not a cache-served run even when the diagnostics slot
|
|
652
|
+
# then hit — the stats it would otherwise suppress were all gathered.
|
|
653
|
+
@run_served_from_cache = !computed && analysis.nil?
|
|
509
654
|
diagnostics
|
|
510
655
|
rescue StandardError
|
|
511
656
|
# The result cache must never break a run. If anything in the cache path fails, fall back to a
|
|
@@ -514,6 +659,143 @@ module Rigor
|
|
|
514
659
|
assemble_run_diagnostics(expansion)
|
|
515
660
|
end
|
|
516
661
|
|
|
662
|
+
# ADR-103 WD13 / issue #382 — the whole-run **effects sidecar**, the second of "one cache, two
|
|
663
|
+
# identities, one extra slot". Keyed by {Effects::Identity.descriptor} — the diagnostics key
|
|
664
|
+
# descriptor plus the vocabulary version, the catalogue identity and the `effects:` digest — and
|
|
665
|
+
# validated against the same post-run dependency descriptor the diagnostics slot records, so exactly
|
|
666
|
+
# the file set that invalidates diagnostics invalidates summaries.
|
|
667
|
+
#
|
|
668
|
+
# Returns the diagnostics of the analysis it had to run (a miss — the run has to re-collect, and the
|
|
669
|
+
# only way to collect is to analyze), or nil when it did not have to run one (a hit, or collection
|
|
670
|
+
# off). The diagnostics slot is never written, read, or invalidated from here.
|
|
671
|
+
def serve_effect_collections(expansion, environment, key_descriptor, rbs_descriptor)
|
|
672
|
+
return nil unless @record_effects
|
|
673
|
+
|
|
674
|
+
descriptor = effects_key_descriptor(key_descriptor)
|
|
675
|
+
cached = descriptor && peek_effect_collections(descriptor)
|
|
676
|
+
if cached
|
|
677
|
+
adopt_effect_collections(cached)
|
|
678
|
+
@effects_served_from_cache = true
|
|
679
|
+
return nil
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
analysis = assemble_run_diagnostics(expansion, environment: environment)
|
|
683
|
+
store_effect_collections(descriptor, expansion, rbs_descriptor)
|
|
684
|
+
analysis
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
def effects_key_descriptor(key_descriptor)
|
|
688
|
+
Effects::Identity.descriptor(base: key_descriptor, configuration: @configuration,
|
|
689
|
+
plugin_facts: effect_plugin_facts)
|
|
690
|
+
rescue StandardError
|
|
691
|
+
nil
|
|
692
|
+
end
|
|
693
|
+
|
|
694
|
+
# The read half. A miss, a stale dependency, a corrupt entry and a stored value of the wrong shape
|
|
695
|
+
# are one answer — nil, "collect it again" — because none of them can be told apart from the outside
|
|
696
|
+
# and all of them have the same remedy.
|
|
697
|
+
def peek_effect_collections(descriptor)
|
|
698
|
+
cached = @cache_store.peek_validated(
|
|
699
|
+
producer_id: RunCacheKey::RUN_EFFECTS_PRODUCER_ID, key_descriptor: descriptor
|
|
700
|
+
)
|
|
701
|
+
cached.is_a?(Hash) ? cached : nil
|
|
702
|
+
rescue StandardError
|
|
703
|
+
nil
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
# The write half, run only after a miss, so the block never recomputes anything: it hands over the
|
|
707
|
+
# collections the analysis just produced, validated against the same post-run dependency descriptor
|
|
708
|
+
# the diagnostics slot records. Fail-soft — a collection that will not Marshal (which nothing in
|
|
709
|
+
# {Effects::FileCollection} should be, and the fork pool already proves per file) costs the next run
|
|
710
|
+
# its warm start and nothing else.
|
|
711
|
+
def store_effect_collections(descriptor, expansion, rbs_descriptor)
|
|
712
|
+
return if descriptor.nil?
|
|
713
|
+
|
|
714
|
+
collections = effect_collections_by_path
|
|
715
|
+
@cache_store.fetch_or_validate(
|
|
716
|
+
producer_id: RunCacheKey::RUN_EFFECTS_PRODUCER_ID, key_descriptor: descriptor,
|
|
717
|
+
generation_cap: RunCacheKey::EFFECTS_GENERATION_CAP
|
|
718
|
+
) { [collections, run_dependency_descriptor(expansion, rbs_descriptor)] }
|
|
719
|
+
nil
|
|
720
|
+
rescue StandardError
|
|
721
|
+
nil
|
|
722
|
+
end
|
|
723
|
+
private :serve_effect_collections, :effects_key_descriptor,
|
|
724
|
+
:peek_effect_collections, :store_effect_collections
|
|
725
|
+
|
|
726
|
+
# ADR-103 WD8 / #383 — the envelope check. Nothing at all without an `effects:` block, and nothing
|
|
727
|
+
# under `effects.check: false`; with both, one walk of the project's own RBS for `%a{pure}` /
|
|
728
|
+
# `%a{rigor:v1:effect …}`, and only if that finds an envelope does anything else run (the discovery
|
|
729
|
+
# tables the `def` positions come from are forced from inside the pass, lazily, for that reason).
|
|
730
|
+
#
|
|
731
|
+
# The environment is the one the cacheable path already resolved when there is one, so a warm run
|
|
732
|
+
# reads the envelopes off the loader it built anyway rather than building a second.
|
|
733
|
+
def effect_envelope_diagnostics(expansion)
|
|
734
|
+
return [] unless @record_effects && @configuration.effects_check?
|
|
735
|
+
|
|
736
|
+
EffectEnvelopePass.new(
|
|
737
|
+
configuration: @configuration,
|
|
738
|
+
rbs_loader: envelope_rbs_loader(expansion),
|
|
739
|
+
effect_table: effect_table,
|
|
740
|
+
discovery: -> { envelope_discovery_tables(expansion) },
|
|
741
|
+
sources: @in_memory_sources,
|
|
742
|
+
unit_sources: effect_sources,
|
|
743
|
+
# ADR-103 WD1 / #386 — the nominal relation `effect.liskov-widened` reads, from the collector's
|
|
744
|
+
# own as-written superclass table, so the Liskov check and the closed-world proven lane resolve
|
|
745
|
+
# the same ancestry. Lazy: only a project that declared an envelope pays the merge.
|
|
746
|
+
ancestry: -> { effect_collection.superclasses },
|
|
747
|
+
apply_tolerated: !@no_tolerated_effects,
|
|
748
|
+
# #387 — the same compiled plugin tables the collection window scanned under, so an envelope may
|
|
749
|
+
# name a label a plugin opened and the unknown-label check agrees with the scan.
|
|
750
|
+
plugin_facts: effect_plugin_facts
|
|
751
|
+
).diagnostics
|
|
752
|
+
end
|
|
753
|
+
|
|
754
|
+
# The residual takes the loader the run ALREADY resolved — never `envelope_rbs_loader`, which
|
|
755
|
+
# builds one on demand. This runs on the effects-off path, where an environment build is a cost
|
|
756
|
+
# the project did not ask for; the `signature_paths:` `.rbs` stratum is always read, and the
|
|
757
|
+
# rbs-inline stratum rides whatever the run happened to have.
|
|
758
|
+
def effect_annotation_residual_diagnostics
|
|
759
|
+
EffectAnnotationResidualPass.new(
|
|
760
|
+
configuration: @configuration, rbs_loader: @run_environment&.rbs_loader
|
|
761
|
+
).diagnostics
|
|
762
|
+
end
|
|
763
|
+
|
|
764
|
+
def envelope_rbs_loader(expansion)
|
|
765
|
+
environment = @run_environment ||
|
|
766
|
+
@pool_coordinator.resolve_sequential_environment(source_files: target_files(expansion))
|
|
767
|
+
environment&.rbs_loader
|
|
768
|
+
rescue StandardError
|
|
769
|
+
nil
|
|
770
|
+
end
|
|
771
|
+
|
|
772
|
+
def envelope_discovery_tables(expansion)
|
|
773
|
+
ensure_project_discovery(expansion)
|
|
774
|
+
[@project_discovered_def_sources, @project_discovered_singleton_def_sources,
|
|
775
|
+
@project_discovered_class_sources]
|
|
776
|
+
end
|
|
777
|
+
|
|
778
|
+
private :effect_envelope_diagnostics, :effect_annotation_residual_diagnostics,
|
|
779
|
+
:envelope_rbs_loader, :envelope_discovery_tables
|
|
780
|
+
|
|
781
|
+
# ADR-103 WD13 — fail-soft at the run level too: a propagator that raises leaves the table empty and
|
|
782
|
+
# the run untouched. `Propagator.propagate` already swallows its own failures; this guards the merge.
|
|
783
|
+
def close_effect_graph
|
|
784
|
+
return unless @record_effects
|
|
785
|
+
|
|
786
|
+
@effect_table = Effects::Propagator.propagate(effect_collection, discharge: effect_discharge)
|
|
787
|
+
rescue StandardError
|
|
788
|
+
@effect_table = Effects::EffectTable.empty
|
|
789
|
+
end
|
|
790
|
+
|
|
791
|
+
# ADR-103 WD14 / #385 — the `effects.tolerated:` policy the propagator's second (undischarged) lane
|
|
792
|
+
# is computed under. Built from the configuration and never from the audit flag: BOTH lanes are in
|
|
793
|
+
# the table, and `--no-tolerated-effects` picks the other one at judgment time, so one fixpoint
|
|
794
|
+
# serves an audit run and an ordinary one alike.
|
|
795
|
+
def effect_discharge
|
|
796
|
+
@effect_discharge ||= Effects::Discharge.new(@configuration.effects_tolerated)
|
|
797
|
+
end
|
|
798
|
+
|
|
517
799
|
def assemble_run_diagnostics(expansion, environment: nil)
|
|
518
800
|
# Force the deferred cross-file discovery pre-pass on the analysis (miss) path. Memoised, so the
|
|
519
801
|
# eager force in `#run` (recording / subset modes) makes this a no-op. A warm cache HIT never calls
|
|
@@ -526,6 +808,11 @@ module Rigor
|
|
|
526
808
|
# assembles). Gate off → no-op, and `environment` is left untouched so its lazy build timing is
|
|
527
809
|
# unchanged. Returns the resolved environment so the sequential dispatch reuses it (no double build).
|
|
528
810
|
environment = seed_parameter_inference(expansion, environment)
|
|
811
|
+
# Issue #352 — the `pre_eval:` constant publication pre-pass. Same placement rationale as the line
|
|
812
|
+
# above: it runs on the parent BEFORE the pool split so every worker sees the same frozen table, and
|
|
813
|
+
# only on the analysis (miss) path. No `pre_eval:` entry → no-op, and `environment` passes through
|
|
814
|
+
# untouched so its lazy build timing is unchanged.
|
|
815
|
+
environment = seed_pre_eval_constants(expansion, environment)
|
|
529
816
|
diagnostics = @diagnostic_aggregator.pre_file_diagnostics(expansion)
|
|
530
817
|
# ADR-46 — record which project files this run actually analyzed (the `analyze_only` subset, or
|
|
531
818
|
# all of them). The incremental orchestrator serves every analyzed-but-not-affected file from the
|
|
@@ -533,6 +820,11 @@ module Rigor
|
|
|
533
820
|
targets = target_files(expansion)
|
|
534
821
|
@analyzed_files = targets
|
|
535
822
|
diagnostics += @pool_coordinator.analyze_files(targets, environment: environment)
|
|
823
|
+
# ADR-103 WD12 — the effect fixpoint, in the post-pool aggregation slot beside the conformance
|
|
824
|
+
# results. Graph-only over a finite lattice, so it is a plain worklist to a true fixpoint; it
|
|
825
|
+
# contributes NO diagnostics and its result leaves through `#effect_table`, never through the
|
|
826
|
+
# stream. Skipped entirely when collection did not run.
|
|
827
|
+
close_effect_graph
|
|
536
828
|
diagnostics += @diagnostic_aggregator.rbs_quarantined_signature_diagnostics
|
|
537
829
|
diagnostics += @diagnostic_aggregator.rbs_environment_build_failed_diagnostics
|
|
538
830
|
diagnostics += @diagnostic_aggregator.rbs_synthesized_namespace_diagnostics
|
|
@@ -572,6 +864,34 @@ module Rigor
|
|
|
572
864
|
environment
|
|
573
865
|
end
|
|
574
866
|
|
|
867
|
+
# Issue #352 / ADR-17 — the `pre_eval:` CONSTANT publication pre-pass, the twin of the slice-2 patched-
|
|
868
|
+
# METHOD registry the eager pre-passes already build. Walks each listed file's constant writes with the
|
|
869
|
+
# per-file pre-pass ({Inference::ScopeIndexer.build_in_source_constants}) under a project-seeded scope,
|
|
870
|
+
# widens each result to its erased class, and populates `@project_pre_eval_constants` — which
|
|
871
|
+
# `project_scope_seed_tables` then seeds onto every per-file scope, so `TIMEOUT = 30` in a listed file
|
|
872
|
+
# reads as `Integer` instead of `Dynamic[top]` in its consumers. The widening + multi-file conflict rules
|
|
873
|
+
# live in {Inference::PreEvalConstants}.
|
|
874
|
+
#
|
|
875
|
+
# Runs here rather than in {ProjectPrePasses#run} because typing a constant's rvalue needs the RBS
|
|
876
|
+
# environment, which the eager pre-passes feed rather than consume. Fails soft — a collector error must
|
|
877
|
+
# never break a run, so the table stays empty and the run proceeds exactly as it does today.
|
|
878
|
+
def seed_pre_eval_constants(expansion, environment)
|
|
879
|
+
paths = @configuration.pre_eval.select { |path| File.file?(path) }
|
|
880
|
+
return environment if paths.empty?
|
|
881
|
+
|
|
882
|
+
environment ||= @pool_coordinator.resolve_sequential_environment(source_files: expansion.fetch(:files))
|
|
883
|
+
@project_pre_eval_constants = Inference::PreEvalConstants.collect(
|
|
884
|
+
paths: paths, target_ruby: @configuration.target_ruby, buffer: @buffer,
|
|
885
|
+
scope_builder: lambda { |path|
|
|
886
|
+
seed_project_scope(Scope.empty(environment: environment, source_path: path))
|
|
887
|
+
}
|
|
888
|
+
)
|
|
889
|
+
environment
|
|
890
|
+
rescue StandardError
|
|
891
|
+
@project_pre_eval_constants = {}.freeze
|
|
892
|
+
environment
|
|
893
|
+
end
|
|
894
|
+
|
|
575
895
|
# A cache hit skipped the analysis, so the per-run stats (wall split, RBS-class counts, …) were never
|
|
576
896
|
# gathered — report none rather than the stale snapshot defaults.
|
|
577
897
|
def stats_for_run(wall_started_at:, expansion:)
|
|
@@ -592,6 +912,11 @@ module Rigor
|
|
|
592
912
|
# share the full run's result key (`run_key_descriptor` keys on the whole expansion, not the
|
|
593
913
|
# subset), so serving one as the other would manufacture a wrong result. Both instead use the
|
|
594
914
|
# store for the RBS-env + plugin-producer tiers, where the incremental win actually lives.
|
|
915
|
+
# ADR-103 WD13 / issue #382 — a COLLECTING run is no longer excluded. It was, for the same reason a
|
|
916
|
+
# `record_dependencies` run is (a cache-served run collects nothing, so `#effect_table` would come
|
|
917
|
+
# back empty from a warm hit), and the sidecar slot is what lifts it: the collections ride their own
|
|
918
|
+
# entry under their own identity, so a warm run restores them and re-runs only the fixpoint. A run
|
|
919
|
+
# with collection OFF never reads or writes that slot and is byte-identical here.
|
|
595
920
|
def run_result_cacheable?
|
|
596
921
|
!@cache_store.nil? && !@cache_store.read_only? &&
|
|
597
922
|
@buffer.nil? && @prebuilt.nil? && !pool_mode? &&
|
|
@@ -615,8 +940,16 @@ module Rigor
|
|
|
615
940
|
# Files the run actually depended on, collected AFTER it ran: every analyzed file, every RBS `sig`
|
|
616
941
|
# file (`rbs_descriptor.files`), and every file each plugin read (complete post-run, so reads made
|
|
617
942
|
# mid-analysis are included). Re-digested on the next run by {Descriptor#fresh?}.
|
|
943
|
+
# Memoised because a collecting run asks twice — once to validate the effects sidecar it just wrote,
|
|
944
|
+
# once for the diagnostics entry — and this is a whole-project stat + digest walk. Both asks happen
|
|
945
|
+
# after the same analysis, so they are answering about the same world; a run with collection off asks
|
|
946
|
+
# once and the memo is inert.
|
|
618
947
|
def run_dependency_descriptor(expansion, rbs_descriptor)
|
|
619
|
-
|
|
948
|
+
@run_dependency_descriptor ||= build_run_dependency_descriptor(expansion, rbs_descriptor)
|
|
949
|
+
end
|
|
950
|
+
|
|
951
|
+
def build_run_dependency_descriptor(expansion, rbs_descriptor)
|
|
952
|
+
entries = analyzed_file_entries(expansion) + pre_eval_file_entries + rbs_descriptor.files
|
|
620
953
|
@plugin_registry.plugins.each do |plugin|
|
|
621
954
|
# Read the boundary WITHOUT triggering its lazy `@io_boundary ||=` initializer: plugin instances
|
|
622
955
|
# are frozen after the run, and a plugin that never built a boundary read no files through it,
|
|
@@ -636,6 +969,21 @@ module Rigor
|
|
|
636
969
|
end
|
|
637
970
|
end
|
|
638
971
|
|
|
972
|
+
# Issue #352 — a `pre_eval:` file is a real input to the run's diagnostics: its `def`s populate the
|
|
973
|
+
# ADR-17 patched-method registry and (since #352) its constants populate the project seed. ADR-17 WD5
|
|
974
|
+
# permits listing a file under `pre_eval:` and NOT under `paths:`, and such a file never appears in the
|
|
975
|
+
# expansion — so without this entry, editing one would leave the run-result cache serving diagnostics
|
|
976
|
+
# computed against the previous version. The common case (a `lib/core_ext/` file that is also under
|
|
977
|
+
# `paths:`) contributes a duplicate entry, which the descriptor's per-path validation absorbs.
|
|
978
|
+
def pre_eval_file_entries
|
|
979
|
+
@configuration.pre_eval.filter_map do |path|
|
|
980
|
+
physical = @buffer ? @buffer.resolve(path) : path
|
|
981
|
+
next unless File.file?(physical)
|
|
982
|
+
|
|
983
|
+
Cache::Descriptor::FileEntry.stat(path: physical, digest: Cache::FileDigest.hexdigest(physical))
|
|
984
|
+
end
|
|
985
|
+
end
|
|
986
|
+
|
|
639
987
|
# Runs every project-wide pre-pass (`load_plugins` +
|
|
640
988
|
# `plugin#prepare` + dependency-source builder +
|
|
641
989
|
# synthetic-method scanner + project-patched scanner)
|
|
@@ -798,6 +1146,13 @@ module Rigor
|
|
|
798
1146
|
|
|
799
1147
|
private
|
|
800
1148
|
|
|
1149
|
+
# The run's per-file effect collections in sorted path order — the sequential half merged with the
|
|
1150
|
+
# pool's, exactly as {#file_dependencies} reconciles its two halves. Sorted, because the fold below
|
|
1151
|
+
# it must not depend on pool-completion order.
|
|
1152
|
+
def effect_collections
|
|
1153
|
+
effect_collections_by_path.sort_by { |path, _| path.to_s }.map(&:last)
|
|
1154
|
+
end
|
|
1155
|
+
|
|
801
1156
|
# Editor mode § "Scope choice — option A". Under `buffer:` non-nil the per-file analysis emits
|
|
802
1157
|
# diagnostics ONLY for the buffer's logical path; the rest of `paths:` is consumed by the
|
|
803
1158
|
# project-wide pre-passes (synthetic methods, project-patched methods, plugin facts) but contributes
|
|
@@ -1106,10 +1461,7 @@ module Rigor
|
|
|
1106
1461
|
tables[:discovered_method_visibilities] = @project_discovered_method_visibilities
|
|
1107
1462
|
end
|
|
1108
1463
|
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?
|
|
1464
|
+
seed_opt_in_pre_pass_tables(tables)
|
|
1113
1465
|
seed_member_layout_tables(tables)
|
|
1114
1466
|
# ADR-46 slice 1 — the class-declaration source map is read only by the ancestry accessors during
|
|
1115
1467
|
# dependency recording, so seed it only when recording is on; a normal run never carries it.
|
|
@@ -1126,6 +1478,23 @@ module Rigor
|
|
|
1126
1478
|
@discovery_seed ? @discovery_seed.dup : {}
|
|
1127
1479
|
end
|
|
1128
1480
|
|
|
1481
|
+
# Seeds the two OPT-IN pre-pass tables — the ones an ordinary run leaves empty, so they are absent from
|
|
1482
|
+
# the seed entirely unless the project asked for them. Both ride this seed so a pooled `WorkerSession`
|
|
1483
|
+
# scope resolves identically to the sequential path (ADR-15 sequential equivalence).
|
|
1484
|
+
#
|
|
1485
|
+
# - ADR-67 WD6a `param_inferred_types`: the call-site parameter-inference table, populated only when the
|
|
1486
|
+
# `parameter_inference:` gate ran the pre-pass.
|
|
1487
|
+
# - Issue #352 `in_source_constants`: the `pre_eval:` constant publication, populated only when the
|
|
1488
|
+
# project lists `pre_eval:` files that declare publishable constants.
|
|
1489
|
+
#
|
|
1490
|
+
# Extracted to keep {#project_scope_seed_tables} under the complexity budget.
|
|
1491
|
+
def seed_opt_in_pre_pass_tables(tables)
|
|
1492
|
+
tables[:param_inferred_types] = @project_param_inferred_types unless @project_param_inferred_types.empty?
|
|
1493
|
+
return if @project_pre_eval_constants.empty?
|
|
1494
|
+
|
|
1495
|
+
tables[:in_source_constants] = @project_pre_eval_constants
|
|
1496
|
+
end
|
|
1497
|
+
|
|
1129
1498
|
# ADR-46 — seed the instance + singleton `"path:line"` def-source tables (each only when non-empty).
|
|
1130
1499
|
# Extracted to keep {#project_scope_seed_tables} under the complexity budget. The singleton table (slice 4
|
|
1131
1500
|
# extension) rides the same seed so a pooled `WorkerSession` records singleton symbol edges identically.
|
|
@@ -1151,16 +1520,52 @@ module Rigor
|
|
|
1151
1520
|
# `DependencyRecorder.active? == false`. Recording is observational, so diagnostics are byte-identical
|
|
1152
1521
|
# either way.
|
|
1153
1522
|
def analyze_file(path, environment)
|
|
1154
|
-
return
|
|
1523
|
+
return analyze_with_effects(path, environment) unless @record_dependencies
|
|
1155
1524
|
|
|
1156
1525
|
diagnostics = nil
|
|
1157
1526
|
record = DependencyRecorder.record_for(path) do
|
|
1158
|
-
diagnostics =
|
|
1527
|
+
diagnostics = analyze_with_effects(path, environment)
|
|
1159
1528
|
end
|
|
1160
1529
|
@file_dependencies[path] = record
|
|
1161
1530
|
diagnostics
|
|
1162
1531
|
end
|
|
1163
1532
|
|
|
1533
|
+
# ADR-103 WD13 — the effect-collection window, in the same shape as the dependency one and nested
|
|
1534
|
+
# inside it so a recording incremental run collects both. Off by default: `effects_enabled?` false
|
|
1535
|
+
# calls the body directly, {Effects::Collector.active?} stays false, and the recorder's sites on the
|
|
1536
|
+
# dispatch path short-circuit on one integer read.
|
|
1537
|
+
def analyze_with_effects(path, environment)
|
|
1538
|
+
return analyze_file_body(path, environment) unless @record_effects
|
|
1539
|
+
|
|
1540
|
+
diagnostics = nil
|
|
1541
|
+
collection = Effects::Collector.collect_for(
|
|
1542
|
+
path, attribution: @effect_attribution, envelopes: effect_envelope_index(environment),
|
|
1543
|
+
plugin_facts: effect_plugin_facts
|
|
1544
|
+
) do
|
|
1545
|
+
diagnostics = analyze_file_body(path, environment)
|
|
1546
|
+
end
|
|
1547
|
+
@file_effects[path] = collection
|
|
1548
|
+
diagnostics
|
|
1549
|
+
end
|
|
1550
|
+
|
|
1551
|
+
# ADR-103 WD6 / #386 — the envelopes a call site may import as a `≤` bound, built once per process
|
|
1552
|
+
# over the first environment the analysis resolves and reused for every later file. A worker builds
|
|
1553
|
+
# its own from the same configuration and the same signature content ({WorkerSession}), so the two
|
|
1554
|
+
# agree without a channel to keep in sync.
|
|
1555
|
+
#
|
|
1556
|
+
# Memoised on the environment's identity rather than unconditionally: a run that resolves its
|
|
1557
|
+
# environment lazily hands `nil` to the first files, and an index built from `nil` would silently
|
|
1558
|
+
# pin the whole run to the strata a loader-less build can read.
|
|
1559
|
+
def effect_envelope_index(environment)
|
|
1560
|
+
return @effect_envelope_index if @effect_envelope_index && @effect_envelope_index_env.equal?(environment)
|
|
1561
|
+
|
|
1562
|
+
@effect_envelope_index_env = environment
|
|
1563
|
+
@effect_envelope_index = Effects::EnvelopeIndex.build(
|
|
1564
|
+
configuration: @configuration, environment: environment, plugin_facts: effect_plugin_facts
|
|
1565
|
+
)
|
|
1566
|
+
end
|
|
1567
|
+
private :effect_envelope_index
|
|
1568
|
+
|
|
1164
1569
|
def analyze_file_body(path, environment) # rubocop:disable Metrics/MethodLength
|
|
1165
1570
|
parse_result = parse_source(path)
|
|
1166
1571
|
unless parse_result.errors.empty?
|
|
@@ -1169,6 +1574,11 @@ module Rigor
|
|
|
1169
1574
|
return parse_diagnostics(path, parse_result)
|
|
1170
1575
|
end
|
|
1171
1576
|
|
|
1577
|
+
# ADR-103 WD13 — hand the effect collector the root the typer is about to walk, so its per-def
|
|
1578
|
+
# scan runs over exactly what was typed. Guarded inside the recorder rather than by `active?`: this
|
|
1579
|
+
# is a per-FILE site, so one `Thread.current` read is already nothing. The per-dispatch site in
|
|
1580
|
+
# `ExpressionTyper#call_type_for` is the one that pays for the integer fast path.
|
|
1581
|
+
Effects::Collector.record_root(parse_result.value)
|
|
1172
1582
|
scope = seed_project_scope(Scope.empty(environment: environment, source_path: path))
|
|
1173
1583
|
# ADR-24 slice 4a/4 — record unresolved implicit-self calls during the typing pass ONLY (not
|
|
1174
1584
|
# CheckRules, whose own `type_of` queries would otherwise re-trigger the choke-point).
|
|
@@ -35,7 +35,8 @@ module Rigor
|
|
|
35
35
|
Diagnostic.new(
|
|
36
36
|
path: diagnostic.path, line: diagnostic.line, column: diagnostic.column,
|
|
37
37
|
message: diagnostic.message, severity: resolved, rule: diagnostic.rule,
|
|
38
|
-
source_family: diagnostic.source_family
|
|
38
|
+
source_family: diagnostic.source_family, receiver_type: diagnostic.receiver_type,
|
|
39
|
+
method_name: diagnostic.method_name, project_definition_site: diagnostic.project_definition_site
|
|
39
40
|
)
|
|
40
41
|
end
|
|
41
42
|
end
|