rigortype 0.4.4 → 0.4.6
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 +5 -5
- data/docs/handbook/11-sig-gen.md +100 -18
- data/docs/manual/02-cli-reference.md +85 -16
- data/docs/manual/03-configuration.md +12 -9
- data/docs/manual/04-diagnostics.md +17 -6
- data/docs/manual/05-inspecting-types.md +2 -2
- data/docs/manual/06-baseline.md +1 -1
- data/docs/manual/07-plugins.md +2 -1
- data/docs/manual/08-skills.md +7 -0
- data/docs/manual/10-mcp-server.md +17 -2
- data/docs/manual/12-caching.md +8 -1
- data/docs/manual/18-removing-dead-code.md +18 -7
- data/docs/manual/plugins/rigor-actionmailer.md +30 -0
- data/docs/manual/plugins/rigor-actionpack.md +30 -0
- data/docs/manual/plugins/rigor-devise.md +15 -0
- data/docs/manual/plugins/rigor-rspec.md +102 -1
- data/lib/rigor/analysis/buffer_binding.rb +14 -0
- data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +30 -9
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +142 -15
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +115 -7
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +253 -0
- data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
- data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
- data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +3 -1
- data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +64 -4
- data/lib/rigor/analysis/check_rules.rb +319 -21
- data/lib/rigor/analysis/dependency_recorder.rb +11 -0
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/edit_replay.rb +67 -0
- data/lib/rigor/analysis/incremental.rb +86 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
- data/lib/rigor/analysis/incremental_session.rb +336 -46
- data/lib/rigor/analysis/path_expansion.rb +44 -1
- data/lib/rigor/analysis/reachability/graph.rb +90 -13
- data/lib/rigor/analysis/reachability/scan.rb +279 -39
- data/lib/rigor/analysis/require_relative_closure.rb +100 -0
- data/lib/rigor/analysis/rule_catalog.rb +22 -5
- data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
- data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +11 -3
- data/lib/rigor/analysis/runner/pool_coordinator.rb +82 -16
- data/lib/rigor/analysis/runner/project_pre_passes.rb +45 -3
- data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
- data/lib/rigor/analysis/runner.rb +222 -111
- data/lib/rigor/analysis/severity_stamp.rb +11 -0
- data/lib/rigor/analysis/worker_session.rb +24 -1
- data/lib/rigor/cache/descriptor.rb +40 -7
- data/lib/rigor/cache/engine_source.rb +3 -2
- data/lib/rigor/cache/incremental_snapshot.rb +49 -8
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +4 -3
- data/lib/rigor/cli/check_command.rb +93 -4
- data/lib/rigor/cli/command.rb +3 -1
- data/lib/rigor/cli/diff_command.rb +2 -1
- data/lib/rigor/cli/docs_command.rb +3 -2
- data/lib/rigor/cli/doctor_command.rb +1 -1
- data/lib/rigor/cli/explain_command.rb +3 -2
- data/lib/rigor/cli/lens_command.rb +5 -5
- data/lib/rigor/cli/mcp_command.rb +2 -1
- data/lib/rigor/cli/options.rb +20 -3
- data/lib/rigor/cli/plugin_command.rb +2 -1
- data/lib/rigor/cli/plugins_command.rb +7 -1
- data/lib/rigor/cli/plugins_renderer.rb +24 -2
- data/lib/rigor/cli/show_bleedingedge_command.rb +2 -1
- data/lib/rigor/cli/sig_gen_command.rb +73 -13
- data/lib/rigor/cli/skill_command.rb +2 -1
- data/lib/rigor/cli/skill_describe.rb +3 -2
- data/lib/rigor/cli/type_of_command.rb +2 -1
- data/lib/rigor/cli/type_of_project_scope.rb +27 -0
- data/lib/rigor/cli/unused_command.rb +10 -7
- data/lib/rigor/cli.rb +3 -2
- data/lib/rigor/config_audit.rb +72 -16
- data/lib/rigor/configuration/severity_profile.rb +20 -2
- data/lib/rigor/configuration.rb +43 -7
- data/lib/rigor/effects/signature_sources.rb +3 -1
- data/lib/rigor/environment/bundle_sig_discovery.rb +3 -2
- data/lib/rigor/environment/gated_signature_guard.rb +2 -1
- data/lib/rigor/environment/gated_signature_plan.rb +2 -1
- data/lib/rigor/environment/installed_gem_set.rb +2 -1
- data/lib/rigor/environment/missing_gem_constant_index.rb +4 -3
- data/lib/rigor/environment/rbs_loader.rb +136 -7
- data/lib/rigor/environment/required_features.rb +136 -29
- data/lib/rigor/environment/ruby_library_roots.rb +122 -0
- data/lib/rigor/environment.rb +4 -2
- data/lib/rigor/inference/block_call_timing.rb +7 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/callee_mutation_chain.rb +115 -0
- data/lib/rigor/inference/captured_locals.rb +44 -13
- data/lib/rigor/inference/clause_widening.rb +90 -0
- data/lib/rigor/inference/deep_erasure.rb +138 -0
- data/lib/rigor/inference/def_return_typer.rb +159 -9
- data/lib/rigor/inference/definer_resolution.rb +101 -20
- data/lib/rigor/inference/error_info.rb +17 -3
- data/lib/rigor/inference/expression_typer.rb +97 -18
- data/lib/rigor/inference/global_write_census.rb +36 -1
- data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
- data/lib/rigor/inference/ivar_writer_census.rb +195 -0
- data/lib/rigor/inference/loop_value_record.rb +53 -0
- data/lib/rigor/inference/macro_block_self_type.rb +20 -6
- data/lib/rigor/inference/match_rebinding/frame.rb +19 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +278 -42
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +53 -20
- data/lib/rigor/inference/method_dispatcher.rb +113 -19
- data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
- data/lib/rigor/inference/module_function_state.rb +149 -42
- data/lib/rigor/inference/mutation_widening.rb +5 -2
- data/lib/rigor/inference/narrowing.rb +116 -11
- data/lib/rigor/inference/object_mixins.rb +13 -1
- data/lib/rigor/inference/parameter_inference_collector.rb +4 -26
- data/lib/rigor/inference/project_patched_methods.rb +168 -4
- data/lib/rigor/inference/project_patched_mixins.rb +162 -0
- data/lib/rigor/inference/project_patched_scanner.rb +51 -12
- data/lib/rigor/inference/project_patched_visibility.rb +214 -0
- data/lib/rigor/inference/project_seed_tables.rb +121 -0
- data/lib/rigor/inference/project_yielder.rb +128 -0
- data/lib/rigor/inference/refined_dispatch/constructed_class.rb +113 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +41 -12
- data/lib/rigor/inference/reflective_ivar_write.rb +6 -6
- data/lib/rigor/inference/return_barrier.rb +16 -0
- data/lib/rigor/inference/scope_indexer/certainty.rb +9 -0
- data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +162 -0
- data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
- data/lib/rigor/inference/scope_indexer.rb +1144 -197
- data/lib/rigor/inference/self_call_ivar_writes.rb +29 -12
- data/lib/rigor/inference/singleton_external_ahead.rb +64 -0
- data/lib/rigor/inference/singleton_hook_decline.rb +7 -0
- data/lib/rigor/inference/statement_evaluator.rb +1088 -213
- data/lib/rigor/inference/synthetic_method_index.rb +23 -0
- data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +72 -12
- data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
- data/lib/rigor/language_server/buffer_table.rb +6 -0
- data/lib/rigor/language_server/diagnostic_publisher.rb +20 -6
- data/lib/rigor/language_server/project_context.rb +59 -3
- data/lib/rigor/language_server/server.rb +5 -3
- data/lib/rigor/lens/contract_reader.rb +45 -2
- data/lib/rigor/lens/document.rb +2 -1
- data/lib/rigor/lens/file_reader.rb +12 -2
- data/lib/rigor/lens/members.rb +16 -2
- data/lib/rigor/lens/run.rb +34 -7
- data/lib/rigor/mcp/server.rb +11 -3
- data/lib/rigor/plugin/base.rb +29 -13
- data/lib/rigor/plugin/io_boundary.rb +42 -6
- data/lib/rigor/plugin/isolation.rb +4 -3
- data/lib/rigor/plugin/manifest.rb +29 -3
- data/lib/rigor/plugin/registry.rb +38 -0
- data/lib/rigor/protection/discovery_seed.rb +64 -36
- data/lib/rigor/protection/mutation_cache.rb +8 -2
- data/lib/rigor/rooted_glob.rb +36 -0
- data/lib/rigor/scope/discovery_index.rb +5 -0
- data/lib/rigor/scope/resolution_chain/relevance.rb +14 -0
- data/lib/rigor/scope/resolution_chain.rb +40 -5
- data/lib/rigor/scope.rb +59 -7
- data/lib/rigor/sig_gen/alias_index.rb +2 -1
- data/lib/rigor/sig_gen/classification.rb +4 -1
- data/lib/rigor/sig_gen/generator.rb +465 -49
- data/lib/rigor/sig_gen/layout_index.rb +5 -2
- data/lib/rigor/sig_gen/observation_collector.rb +141 -12
- data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
- data/lib/rigor/sig_gen/renderer.rb +16 -155
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
- data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
- data/lib/rigor/sig_gen/write_reporter.rb +203 -0
- data/lib/rigor/sig_gen/write_result.rb +3 -1
- data/lib/rigor/sig_gen/writer.rb +5 -3
- data/lib/rigor/signature_path_audit.rb +4 -2
- data/lib/rigor/type/combinator.rb +4 -3
- data/lib/rigor/type/constant.rb +54 -3
- data/lib/rigor/type/hash_shape.rb +14 -9
- data/lib/rigor/type/struct_instance.rb +3 -2
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actioncable/lib/rigor/plugin/actioncable/channel_discoverer.rb +1 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +61 -44
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +20 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/controller_discoverer.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/render_locals.rb +6 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack/view_assigns.rb +1 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +13 -0
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/effects.rb +4 -3
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob/job_discoverer.rb +1 -1
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -2
- data/plugins/rigor-activerecord/lib/rigor/plugin/activerecord/model_discoverer.rb +1 -1
- data/plugins/rigor-activestorage/lib/rigor/plugin/activestorage/attachment_discoverer.rb +1 -1
- data/plugins/rigor-alba/lib/rigor/plugin/alba/resource_discoverer.rb +1 -1
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +7 -6
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +38 -9
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/authorization_scan.rb +1 -1
- data/plugins/rigor-pundit/lib/rigor/plugin/pundit/policy_discoverer.rb +1 -1
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_index.rb +7 -2
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n/locale_loader.rb +5 -5
- data/plugins/rigor-rails-i18n/lib/rigor/plugin/rails_i18n.rb +18 -17
- data/plugins/rigor-rails-routes/lib/rigor/plugin/rails_routes.rb +1 -1
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +4 -2
- data/plugins/rigor-sinatra/lib/rigor/plugin/sinatra.rb +4 -2
- data/plugins/rigor-sorbet/lib/rigor/plugin/sorbet.rb +2 -2
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer/serializer_discoverer.rb +2 -2
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
- data/sig/rigor/analysis/baseline.rbs +3 -2
- data/sig/rigor/effects/file_collection.rbs +5 -6
- data/sig/rigor/effects/registry.rbs +8 -6
- data/sig/rigor/effects/summary.rbs +6 -5
- data/sig/rigor/environment.rbs +5 -3
- data/sig/rigor/inference/optimistic_origin.rbs +11 -0
- data/sig/rigor/inference.rbs +6 -0
- data/sig/rigor/plugin/additional_initializer.rbs +5 -5
- data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
- data/sig/rigor/plugin/effect_attribution.rbs +25 -10
- data/sig/rigor/plugin/effect_edge.rbs +12 -5
- data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
- data/sig/rigor/plugin/io_boundary.rbs +5 -2
- data/sig/rigor/plugin/manifest.rbs +16 -18
- data/sig/rigor/plugin/protocol_contract.rbs +10 -9
- data/sig/rigor/plugin/registry.rbs +1 -1
- data/sig/rigor/rooted_glob.rbs +7 -0
- data/sig/rigor/scope.rbs +34 -52
- data/sig/rigor/type.rbs +0 -6
- data/skills/rigor-doctor/references/01-checks.md +23 -11
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
- metadata +32 -1
|
@@ -5,6 +5,7 @@ require_relative "incremental"
|
|
|
5
5
|
require_relative "incremental_run_slot"
|
|
6
6
|
require_relative "incremental_run_slot/write_guard"
|
|
7
7
|
require_relative "plugin_fact_fingerprint"
|
|
8
|
+
require_relative "run_disclosure_ledger"
|
|
8
9
|
require_relative "source_rbs_gate"
|
|
9
10
|
require_relative "../cache/file_digest"
|
|
10
11
|
require_relative "../cache/incremental_snapshot"
|
|
@@ -40,15 +41,19 @@ module Rigor
|
|
|
40
41
|
# unconditional snapshot rewrite.
|
|
41
42
|
# `rebaselined` (#1898) is true when the recheck found its served effect collections stale and answered with a
|
|
42
43
|
# full baseline instead; see {#rebaseline_recheck}.
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
# `stale_readers` (#1553) are the files the recheck re-analysed because a plugin read one of them made went
|
|
45
|
+
# stale ({#plugin_read_affected}).
|
|
46
|
+
Recheck = Data.define(:diagnostics, :changed, :added, :removed, :affected, :reused, :templates, :rebaselined,
|
|
47
|
+
:stale_readers) do
|
|
48
|
+
def initialize(rebaselined: false, stale_readers: Set.new, **rest)
|
|
45
49
|
super
|
|
46
50
|
end
|
|
47
51
|
|
|
48
|
-
# A recheck that changed, added, and removed nothing — the session state is
|
|
49
|
-
# snapshot that was restored, so persisting it again would only rewrite identical
|
|
52
|
+
# A recheck that changed, added, and removed nothing, and found no stale plugin read — the session state is
|
|
53
|
+
# byte-equivalent to the snapshot that was restored, so persisting it again would only rewrite identical
|
|
54
|
+
# bytes. A stale reader's refreshed rows differ (#1553): unsaved, every later run would re-analyse it.
|
|
50
55
|
def no_change?
|
|
51
|
-
changed.empty? && added.empty? && removed.empty?
|
|
56
|
+
changed.empty? && added.empty? && removed.empty? && stale_readers.empty?
|
|
52
57
|
end
|
|
53
58
|
end
|
|
54
59
|
|
|
@@ -99,6 +104,9 @@ module Rigor
|
|
|
99
104
|
@cache = {} # analyzed path => [Diagnostic]
|
|
100
105
|
@sources = {} # analyzed path => Set<source path it read from>
|
|
101
106
|
@digests = {} # analyzed path => ADR-87 packed stat-digest entry at last analysis
|
|
107
|
+
# Issue #1583 — discovered-not-analysed path => the same packed entry, for a run over path arguments that
|
|
108
|
+
# #684 widened to the configured `paths:`. Empty on every run over the configured paths.
|
|
109
|
+
@discovery_digests = {}
|
|
102
110
|
@analyzed = [] # the project files analyzed last round
|
|
103
111
|
@dependents = {} # inverted @sources (file-level)
|
|
104
112
|
# ADR-46 slice 4 — symbol-granularity tracking.
|
|
@@ -128,6 +136,13 @@ module Rigor
|
|
|
128
136
|
# and a missing invalidation edge is impossible by construction — the reason this is a table diff
|
|
129
137
|
# and not the caller→callee edge recording #204 first sketched.
|
|
130
138
|
@param_table = {}
|
|
139
|
+
# Issue #1553 — analysed path => the plugin reads ({Plugin::IoBoundary.attributing}) its last analysis made:
|
|
140
|
+
# the frozen `Cache::Descriptor::FileEntry` / `GlobEntry` rows, as each read observed its input. A recheck
|
|
141
|
+
# re-analyses every file one of whose rows no longer validates ({#plugin_read_affected}).
|
|
142
|
+
@plugin_reads = {}
|
|
143
|
+
# Issue #2020 — the run-scoped plugin rows each analysed file registered, and the reads that make one stale.
|
|
144
|
+
@plugin_disclosures = RunDisclosureLedger.new
|
|
145
|
+
@stale_disclosures = Set.new
|
|
131
146
|
reset_carried_state
|
|
132
147
|
# ADR-88 WD1 — the plugin fact-surface digest computed for THIS invocation (nil until a
|
|
133
148
|
# `#run_incremental` pass runs / a plugin-free project) and the reporting flags a caller (the CLI
|
|
@@ -151,6 +166,49 @@ module Rigor
|
|
|
151
166
|
current_files
|
|
152
167
|
end
|
|
153
168
|
|
|
169
|
+
# Issue #1583 (and #1560) — whether a file the run discovers but does not analyse was edited, added or
|
|
170
|
+
# removed since the snapshot. Such a file feeds the cross-file index the analysed files resolve against
|
|
171
|
+
# (a constant, a method, an ancestor), but no recorded edge or digest of the analysed set sees it move.
|
|
172
|
+
# A move re-baselines the session: discovery-only files are not tracked per symbol, so the closure
|
|
173
|
+
# cannot be narrowed, and the full analysis is over the argument set alone. A session over the
|
|
174
|
+
# configured `paths:` discovers nothing beyond what it analyses and pays nothing here.
|
|
175
|
+
def discovery_moved?
|
|
176
|
+
current = current_discovery_only_files
|
|
177
|
+
return true unless current.size == @discovery_digests.size
|
|
178
|
+
|
|
179
|
+
current.any? do |path|
|
|
180
|
+
entry = @discovery_digests[path]
|
|
181
|
+
entry.nil? || !Cache::FileDigest.stat_fresh?(path, entry)
|
|
182
|
+
end
|
|
183
|
+
rescue StandardError
|
|
184
|
+
true
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# A baseline's freshness entries: each analysed file's, and (issue #1583) each discovered-not-analysed file's.
|
|
188
|
+
def record_baseline_digests
|
|
189
|
+
@digests = @analyzed.to_h { |path| [path, pack_digest(path)] }
|
|
190
|
+
@discovery_digests = current_discovery_only_files.to_h { |path| [path, pack_digest(path)] }
|
|
191
|
+
forget_unbound_digests
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def restore_digests(payload)
|
|
195
|
+
@digests = payload.digests
|
|
196
|
+
@discovery_digests = payload.discovery_digests # Issue #1583 — `{}` in a payload built without it.
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Issue #1583 — the discovered-not-analysed files of a run over this session's paths
|
|
200
|
+
# ({Runner#discovery_only_file_set}). Asked of a runner WITHOUT the editor buffer: a buffered run does not
|
|
201
|
+
# widen, so a buffered session (editor mode option B, #146) would read an empty set against the snapshot's
|
|
202
|
+
# and re-baseline on every keystroke. Globbing without it keeps the comparison the one the snapshot's
|
|
203
|
+
# writer made, so a sibling edit still declines option B and an unchanged project still serves it.
|
|
204
|
+
def current_discovery_only_files
|
|
205
|
+
return [] if @paths.nil?
|
|
206
|
+
|
|
207
|
+
Runner.new(configuration: @configuration, cache_store: nil, environment: @environment)
|
|
208
|
+
.discovery_only_file_set(@paths)
|
|
209
|
+
end
|
|
210
|
+
private :discovery_moved?, :record_baseline_digests, :restore_digests, :current_discovery_only_files
|
|
211
|
+
|
|
154
212
|
# The per-file discovery bundles of the last run, keyed by path (ADR-85 WD2).
|
|
155
213
|
attr_reader :seed_bundles
|
|
156
214
|
|
|
@@ -215,9 +273,11 @@ module Rigor
|
|
|
215
273
|
@effect_collections = runner.effect_collections_by_path
|
|
216
274
|
@effects_identity = current_effects_identity
|
|
217
275
|
@effect_plugin_digest = effect_plugin_digest_of(runner)
|
|
276
|
+
@synthetic_surface_digest = synthetic_surface_digest_of(runner)
|
|
218
277
|
@cache = per_file(runner.per_file_diagnostics)
|
|
219
|
-
@
|
|
220
|
-
|
|
278
|
+
@plugin_reads = plugin_reads_of(runner, @analyzed)
|
|
279
|
+
@plugin_disclosures = RunDisclosureLedger.from(runner.analysis_disclosures.slice(*@analyzed))
|
|
280
|
+
record_baseline_digests
|
|
221
281
|
@run_level_rows = runner.run_level_rows
|
|
222
282
|
diagnostics
|
|
223
283
|
end
|
|
@@ -231,6 +291,8 @@ module Rigor
|
|
|
231
291
|
added = current - previous
|
|
232
292
|
removed = previous - current
|
|
233
293
|
changed = changed_paths(current & previous)
|
|
294
|
+
return rebaseline_recheck(changed, added, removed) if discovery_moved?
|
|
295
|
+
|
|
234
296
|
# ADR-67 WD6c lift — recompute the whole-project inferred-param table BEFORE deciding the closure,
|
|
235
297
|
# and diff it against the snapshot's copy: an entry that moved (because a caller's argument type
|
|
236
298
|
# changed, a caller appeared, or one vanished) invalidates the CALLEE's file and its symbol
|
|
@@ -238,21 +300,47 @@ module Rigor
|
|
|
238
300
|
fresh_params = fresh_param_table(current, changed, added, removed)
|
|
239
301
|
param_files, param_pairs = param_seed_invalidation(fresh_params)
|
|
240
302
|
affected = affected_closure(changed, added, removed, param_files, param_pairs)
|
|
303
|
+
affected, analyze_set, runner, merged, reused =
|
|
304
|
+
run_settled_closure(affected, [current, previous, removed], fresh_params)
|
|
305
|
+
if served_effects_stale?(runner, reused) || synthetic_surface_moved?(runner, reused)
|
|
306
|
+
return rebaseline_recheck(changed, added, removed)
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
absorb(runner, current, analyze_set, removed)
|
|
310
|
+
@param_table = fresh_params
|
|
311
|
+
recheck_result(merged, [changed, added, removed], affected: affected, reused: reused.to_set, runner: runner,
|
|
312
|
+
stale_readers: @stale_plugin_readers)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# {#recheck}'s narrowed run. Issue #2020 — a stale run-scoped key whose chosen refresher registered nothing (a
|
|
316
|
+
# parse error, a plugin that raised, an edit that took the notice out of its text) is decided by its other
|
|
317
|
+
# holders, so they join the closure and it runs again. Returns `[affected, analyze_set, runner, merged, reused]`.
|
|
318
|
+
def run_settled_closure(affected, (current, previous, removed), fresh_params)
|
|
319
|
+
forced, analyze_set = forced_analyze_set(affected, current)
|
|
320
|
+
runner, merged, reused = run_closure(analyze_set, current, previous, forced, fresh_params)
|
|
321
|
+
fallback = @plugin_disclosures.unresolved_holders(runner.analysis_disclosures, analyze_set, @stale_disclosures,
|
|
322
|
+
removed.to_set)
|
|
323
|
+
return [affected, analyze_set, runner, merged, reused] if fallback.empty?
|
|
324
|
+
|
|
325
|
+
affected = (affected | fallback).freeze
|
|
326
|
+
@stale_plugin_readers |= fallback
|
|
241
327
|
forced, analyze_set = forced_analyze_set(affected, current)
|
|
328
|
+
[affected, analyze_set, *run_closure(analyze_set, current, previous, forced, fresh_params)]
|
|
329
|
+
end
|
|
330
|
+
private :run_settled_closure
|
|
331
|
+
|
|
332
|
+
# One narrowed run of {#recheck}'s closure: the runner, its merged rows, and the files it served. The freshly
|
|
333
|
+
# collected table is handed to the runner so the run seeds from the SAME table the diff was decided on (and the
|
|
334
|
+
# collector runs once per recheck, not twice). The reused files' cached rows go in too, so the runner lists them
|
|
335
|
+
# in the full run's file order (#1524).
|
|
336
|
+
def run_closure(analyze_set, current, previous, forced, fresh_params)
|
|
242
337
|
reused = (current & previous) - analyze_set.to_a
|
|
243
|
-
# The freshly collected table is handed to the runner so the run seeds from the SAME table the diff
|
|
244
|
-
# was decided on (and the collector runs once per recheck, not twice). The reused files' cached rows
|
|
245
|
-
# go in too, so the runner lists them in the full run's file order (#1524).
|
|
246
338
|
runner = build_runner(analyze_only: analyze_set, record_dependencies: true,
|
|
247
339
|
param_inferred_types: fresh_params, **forced_target_options(forced),
|
|
248
340
|
restored_run_level_rows: @run_level_rows, **served_state(reused))
|
|
249
|
-
|
|
250
|
-
return rebaseline_recheck(changed, added, removed) if served_effects_stale?(runner, reused)
|
|
251
|
-
|
|
252
|
-
absorb(runner, current, analyze_set, removed)
|
|
253
|
-
@param_table = fresh_params
|
|
254
|
-
recheck_result(merged, [changed, added, removed], affected: affected, reused: reused.to_set, runner: runner)
|
|
341
|
+
[runner, run_runner(runner).diagnostics, reused]
|
|
255
342
|
end
|
|
343
|
+
private :run_closure
|
|
256
344
|
|
|
257
345
|
# Editor mode option B (#146) — a whole-project recheck with the editor's buffer substituted for one
|
|
258
346
|
# file, for the CLI's `--incremental --tmp-file=X --instead-of=Y`. Returns the {Recheck} when the
|
|
@@ -304,9 +392,10 @@ module Rigor
|
|
|
304
392
|
# records none at all), so no dependent set can bound who read it. An edit that moved it re-analyses the
|
|
305
393
|
# whole project — what a `sig/` edit already costs through the snapshot fingerprint.
|
|
306
394
|
def affected_closure(changed, added, removed, param_files = Set.new, param_pairs = Set.new)
|
|
395
|
+
@stale_plugin_readers = plugin_read_affected | disclosure_refreshers(removed)
|
|
307
396
|
return whole_project_closure(changed, added) if source_rbs_gate.moved?(@seed_bundles, changed, added, removed)
|
|
308
397
|
|
|
309
|
-
gated_closure(changed, added, removed, param_files, param_pairs)
|
|
398
|
+
(gated_closure(changed, added, removed, param_files, param_pairs) | @stale_plugin_readers).freeze
|
|
310
399
|
end
|
|
311
400
|
|
|
312
401
|
# {#affected_closure} for an edit whose synthesized RBS is byte-identical in every file it touched.
|
|
@@ -350,17 +439,47 @@ module Rigor
|
|
|
350
439
|
closure.freeze
|
|
351
440
|
end
|
|
352
441
|
|
|
442
|
+
# Issue #1553 — every analysed file whose last analysis made a plugin read that no longer validates: a file
|
|
443
|
+
# read through {Plugin::IoBoundary#read_file} or probed through `#file?` / `#directory?` was edited,
|
|
444
|
+
# removed, or created after a read that found it absent, or a `#list_directory` / `#glob` listing moved.
|
|
445
|
+
# The ADR-46 edges record type and declaration reads only, so none of these moves a reader into the
|
|
446
|
+
# closure, and its cached rows were computed from the input as it was. The reader's own text is unchanged,
|
|
447
|
+
# so it joins the closure alone: a plugin read made while analysing a dependent is credited to that
|
|
448
|
+
# dependent. Each distinct row is validated once however many files made it (one `File.stat` for a
|
|
449
|
+
# content row whose stat tuple did not move, one re-glob for a listing row).
|
|
450
|
+
def plugin_read_affected
|
|
451
|
+
verdicts = {}
|
|
452
|
+
@plugin_reads.each_with_object(Set.new) do |(path, rows), stale|
|
|
453
|
+
next unless rows.any? { |row| !verdicts.fetch(row) { verdicts[row] = plugin_read_fresh?(row) } }
|
|
454
|
+
|
|
455
|
+
stale << path
|
|
456
|
+
end
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
def plugin_read_fresh?(row)
|
|
460
|
+
if row.is_a?(Cache::Descriptor::GlobEntry)
|
|
461
|
+
Cache::Descriptor::GlobEntry.fresh?(row)
|
|
462
|
+
else
|
|
463
|
+
Cache::Descriptor.new(files: [row]).fresh?
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
# Issue #1553 — `runner`'s per-file plugin reads for `paths`, each file's rows de-duplicated and frozen.
|
|
468
|
+
def plugin_reads_of(runner, paths)
|
|
469
|
+
runner.analysis_reads.slice(*paths).transform_values { |rows| rows.uniq.freeze }
|
|
470
|
+
end
|
|
471
|
+
|
|
353
472
|
# Issue #1906 — a file that rewrites a constant's singleton names without declaring it
|
|
354
|
-
# (`D.singleton_class.alias_method :own, :keep`) contributes opaque singleton
|
|
355
|
-
# but a reader's class edge names only the files that declared the constant
|
|
356
|
-
# class-declaration diff does not move while the constant stays in the file
|
|
357
|
-
# call becoming a `class D` reopening). So a declaration-unstable edit to such a
|
|
358
|
-
# segment>` for each constant that, before the edit, both sat in the file's
|
|
359
|
-
# singleton envelope entry or an unpositioned singleton side the file recorded (a
|
|
360
|
-
# side unpositioned, and becoming literal reads it again). A file that only
|
|
361
|
-
# (`ActiveRecord::Base.include(X)`) records neither and fires nothing. A constant the
|
|
362
|
-
# rewrite needs no key: the class-declaration diff fires if it is new to the file, and the
|
|
363
|
-
# names the file if the file declared it.
|
|
473
|
+
# (`D.singleton_class.alias_method :own, :keep`, or, since #1941, `def D.own`) contributes opaque singleton
|
|
474
|
+
# envelopes to the constant's bucket, but a reader's class edge names only the files that declared the constant
|
|
475
|
+
# when the reader last ran, and the class-declaration diff does not move while the constant stays in the file
|
|
476
|
+
# (`:own` becoming `:keep`, or the call becoming a `class D` reopening). So a declaration-unstable edit to such a
|
|
477
|
+
# file fires `class:<last segment>` for each constant that, before the edit, both sat in the file's
|
|
478
|
+
# singleton-source set and held a singleton envelope entry or an unpositioned singleton side the file recorded (a
|
|
479
|
+
# computed name there leaves the side unpositioned, and becoming literal reads it again). A file that only
|
|
480
|
+
# includes into a constant (`ActiveRecord::Base.include(X)`) records neither and fires nothing. A constant the
|
|
481
|
+
# edit makes the file rewrite needs no key: the class-declaration diff fires if it is new to the file, and the
|
|
482
|
+
# reader's class edge names the file if the file declared it.
|
|
364
483
|
def singleton_rewrite_affected(unstable)
|
|
365
484
|
names = unstable.flat_map { |path| singleton_rewritten_constants(@seed_bundles[path]) }.uniq
|
|
366
485
|
return Set.new if names.empty?
|
|
@@ -383,8 +502,8 @@ module Rigor
|
|
|
383
502
|
# exempts such a write is a fact of the whole program — an alias, or a `write` / `to_str` / `to_int` / hatch
|
|
384
503
|
# defined in any spelling on any receiver, a rewritten ancestor, a mixin — which no name-keyed edge can
|
|
385
504
|
# follow, so a stale report would outlive the edit that exempts it. Such files are rare, so this costs little.
|
|
386
|
-
# A file the census silenced is
|
|
387
|
-
#
|
|
505
|
+
# A file the census silenced is re-checked through the `global_census:` keys its rule recorded
|
|
506
|
+
# ({#global_census_affected}, issue #2158).
|
|
388
507
|
def global_write_reporters
|
|
389
508
|
@cache.each_with_object(Set.new) do |(path, diagnostics), reporters|
|
|
390
509
|
reporters << path if diagnostics.any? { |diagnostic| diagnostic.rule.to_s.start_with?("global.") }
|
|
@@ -532,6 +651,32 @@ module Rigor
|
|
|
532
651
|
run_runner(runner).diagnostics
|
|
533
652
|
end
|
|
534
653
|
|
|
654
|
+
# Issue #1967 — the edit replay of `--verify-incremental-edits`: a warm recheck of this session's baseline with
|
|
655
|
+
# `buffer` standing in for one file, the way editor mode option B (#146) rechecks an unsaved edit. The baseline is
|
|
656
|
+
# handed over as a Marshal copy of the snapshot a `--incremental` run would save, through a fresh session that
|
|
657
|
+
# never persists, so this session's own state is untouched and each replay starts from the same baseline. Unlike
|
|
658
|
+
# {#reanalyze_subset}, whose tree is the baseline's, the edit moves what the served files' rows were computed
|
|
659
|
+
# from, so a dependent the recheck fails to re-analyse is served stale and the comparison sees it. Returns the
|
|
660
|
+
# {Recheck}, or nil when the recheck declined as option B does (the plugin fact surface moved, an unverified
|
|
661
|
+
# source-RBS gate, a re-baseline), where the answer would be a full analysis and there is nothing to compare.
|
|
662
|
+
def replay_edit(buffer)
|
|
663
|
+
adopt_plugin_fact_fingerprint if @plugin_fact_reusable.nil?
|
|
664
|
+
replay = IncrementalSession.new(
|
|
665
|
+
configuration: @configuration, paths: @paths, environment: @environment, cache_store: @cache_store,
|
|
666
|
+
plugin_requirer: @plugin_requirer, buffer: buffer, explain: @explain
|
|
667
|
+
)
|
|
668
|
+
replay.run_buffer_recheck(snapshot: ReplaySnapshot.new(blob: Marshal.dump(to_payload)), fingerprint: :replay)
|
|
669
|
+
end
|
|
670
|
+
|
|
671
|
+
# The in-memory stand-in for {Cache::IncrementalSnapshot} {#replay_edit} hands its replay session: every load is a
|
|
672
|
+
# fresh copy, as a load from disk is.
|
|
673
|
+
ReplaySnapshot = Data.define(:blob) do
|
|
674
|
+
def load(fingerprint:) # rubocop:disable Lint/UnusedMethodArgument
|
|
675
|
+
Marshal.load(blob) # rubocop:disable Security/MarshalLoad
|
|
676
|
+
end
|
|
677
|
+
end
|
|
678
|
+
private_constant :ReplaySnapshot
|
|
679
|
+
|
|
535
680
|
# Cross-process incremental run (the `--incremental` flag's engine). With a disk `snapshot` whose
|
|
536
681
|
# `fingerprint` matches, restore the prior per-file state and `#recheck` (re-analyze only the changed
|
|
537
682
|
# closure, serve the rest from the restored cache); otherwise run a full `#baseline`. Either way,
|
|
@@ -669,7 +814,7 @@ module Rigor
|
|
|
669
814
|
# the gate could not see. {#run_incremental} re-analyses the project and {#run_buffer_recheck} declines,
|
|
670
815
|
# the way each treats a moved plugin fact surface. A run that changed nothing is exempt: under an
|
|
671
816
|
# untrusted gate every earlier edit already re-analysed the whole project, so the snapshot it serves is
|
|
672
|
-
# whole, and a null run stays one.
|
|
817
|
+
# whole, and a null run stays one. A recheck that found a stale plugin read (#1553) is a change.
|
|
673
818
|
def source_rbs_unverified?(result)
|
|
674
819
|
source_rbs_gate.untrusted? && !result.no_change? && !result.reused.empty?
|
|
675
820
|
end
|
|
@@ -889,7 +1034,7 @@ module Rigor
|
|
|
889
1034
|
@analyzed = payload.analyzed
|
|
890
1035
|
@cache = payload.cache
|
|
891
1036
|
@sources = payload.sources
|
|
892
|
-
|
|
1037
|
+
restore_digests(payload)
|
|
893
1038
|
# ADR-85 WD2 — restore the per-file discovery bundles if present (absent in a pre-#85 snapshot → empty,
|
|
894
1039
|
# so the recheck's discovery re-walks every file: a cold-quality index, always sound). The SCHEMA bump
|
|
895
1040
|
# makes a genuinely stale-shaped bundle unreadable rather than mis-folded.
|
|
@@ -903,6 +1048,7 @@ module Rigor
|
|
|
903
1048
|
restore_structural_declarations(payload)
|
|
904
1049
|
@return_summaries = payload.return_summaries || {}
|
|
905
1050
|
@param_table = payload.param_table || {} # ADR-67 WD6c lift — the seeds the cache was built under.
|
|
1051
|
+
restore_plugin_records(payload)
|
|
906
1052
|
# Issues #796 / #794 — absent (nil) in a snapshot written before schema 22 is impossible (the SCHEMA
|
|
907
1053
|
# gate rejects one), so nil here means the writing run genuinely had nothing to say.
|
|
908
1054
|
@run_level_rows = payload.run_level_rows
|
|
@@ -918,6 +1064,14 @@ module Rigor
|
|
|
918
1064
|
@negative_dependents = Incremental.invert(@missing)
|
|
919
1065
|
end
|
|
920
1066
|
|
|
1067
|
+
# Issue #1553 — the plugin reads each file's last analysis made, and (#2020) the run-scoped plugin rows it
|
|
1068
|
+
# registered; `{}` in a payload built without them. Extracted to keep {#restore} under its ABC budget.
|
|
1069
|
+
def restore_plugin_records(payload)
|
|
1070
|
+
@plugin_reads = payload.plugin_reads
|
|
1071
|
+
@plugin_disclosures = RunDisclosureLedger.load(payload.plugin_disclosures)
|
|
1072
|
+
@synthetic_surface_digest = payload.synthetic_surface_digest # the plugin-built ADR-16 index (PR #2286 M1)
|
|
1073
|
+
end
|
|
1074
|
+
|
|
921
1075
|
# ADR-46 slice 3 / issue #644 — the structural tier's negative-edge state: the per-consumer missed
|
|
922
1076
|
# lookups plus the two per-file DECLARATION sets (`class_decls`, `constant_decls`) whose diff says what
|
|
923
1077
|
# APPEARED in an edit. Absent in an older snapshot → empty, which only loses the appeared-symbol
|
|
@@ -942,7 +1096,11 @@ module Rigor
|
|
|
942
1096
|
effect_collections: marshal_safe_effect_collections,
|
|
943
1097
|
effects_identity: @effects_identity,
|
|
944
1098
|
run_level_rows: @run_level_rows,
|
|
945
|
-
effect_plugin_digest: @effect_plugin_digest
|
|
1099
|
+
effect_plugin_digest: @effect_plugin_digest,
|
|
1100
|
+
synthetic_surface_digest: @synthetic_surface_digest,
|
|
1101
|
+
plugin_reads: @plugin_reads,
|
|
1102
|
+
plugin_disclosures: @plugin_disclosures.to_h,
|
|
1103
|
+
discovery_digests: @discovery_digests
|
|
946
1104
|
)
|
|
947
1105
|
end
|
|
948
1106
|
|
|
@@ -1054,15 +1212,20 @@ module Rigor
|
|
|
1054
1212
|
whole_project = current.all? { |path| analyze_set.include?(path) }
|
|
1055
1213
|
@seed_bundles = source_rbs_gate.stamp(runner.seed_bundles, whole_project: whole_project)
|
|
1056
1214
|
fresh_by_file = per_file(runner.per_file_diagnostics)
|
|
1215
|
+
# Issue #1553 — a re-analysed file's reads are this run's: a read it no longer makes stops guarding it.
|
|
1216
|
+
fresh_reads = plugin_reads_of(runner, analyze_set)
|
|
1057
1217
|
analyze_set.each do |path|
|
|
1058
1218
|
@cache[path] = fresh_by_file[path] || []
|
|
1059
1219
|
@digests[path] = pack_digest(path)
|
|
1220
|
+
fresh_reads.key?(path) ? @plugin_reads[path] = fresh_reads[path] : @plugin_reads.delete(path)
|
|
1060
1221
|
end
|
|
1222
|
+
absorb_disclosures(runner, analyze_set)
|
|
1061
1223
|
forget_unbound_digests
|
|
1062
1224
|
absorb_dependency_graph(runner)
|
|
1063
1225
|
# Issues #796 / #794 — the recheck's own snapshots already carry the replayed rows folded together
|
|
1064
1226
|
# with anything its closure demanded for itself, so this is the same union the next run replays.
|
|
1065
1227
|
@run_level_rows = runner.run_level_rows
|
|
1228
|
+
@synthetic_surface_digest = synthetic_surface_digest_of(runner)
|
|
1066
1229
|
refresh_return_summaries(runner, analyze_set)
|
|
1067
1230
|
refresh_effect_collections(runner, analyze_set)
|
|
1068
1231
|
end
|
|
@@ -1105,6 +1268,8 @@ module Rigor
|
|
|
1105
1268
|
@cache.delete(path)
|
|
1106
1269
|
@digests.delete(path)
|
|
1107
1270
|
@sources.delete(path)
|
|
1271
|
+
@plugin_reads.delete(path)
|
|
1272
|
+
@plugin_disclosures.forget(path) # Unpinned hygiene: a removed file is never served, so never replayed.
|
|
1108
1273
|
@symbol_sources.delete(path)
|
|
1109
1274
|
@ancestry_sources.delete(path)
|
|
1110
1275
|
@missing.delete(path)
|
|
@@ -1176,7 +1341,8 @@ module Rigor
|
|
|
1176
1341
|
# top-level def, `method:C#m` otherwise), then unions the recorded negative-dependents of those keys.
|
|
1177
1342
|
# Issue #1120 — plus the consumers of any refinement name the edit moved ({#refinement_affected}). Issue #1367 —
|
|
1178
1343
|
# and every file whose last result holds a `global.*` diagnostic ({#global_write_reporters}). Issue #1459 — and
|
|
1179
|
-
# the consumers of any method name a file started or stopped defining ({#defined_name_affected}).
|
|
1344
|
+
# the consumers of any method name a file started or stopped defining ({#defined_name_affected}). Issue #2134 —
|
|
1345
|
+
# and the consumers of any visibility slot a file moved ({#visibility_name_affected}).
|
|
1180
1346
|
def negative_affected(changed, removed, new_fingerprints, new_class_decls, new_constant_decls, summary = nil)
|
|
1181
1347
|
appeared_methods = Incremental.appeared_symbols(changed, @symbol_fingerprints, new_fingerprints)
|
|
1182
1348
|
# Issue #639 — a class DISAPPEARING satisfies the `class:` kind too, now that a resolved bare
|
|
@@ -1193,8 +1359,61 @@ module Rigor
|
|
|
1193
1359
|
keys = appeared_methods.map { |symbol| negative_key_for(symbol) }
|
|
1194
1360
|
keys.concat(class_move_keys(moved_classes))
|
|
1195
1361
|
keys.concat(moved_constants.map { |name| "constant:#{name.split('::').last}" })
|
|
1196
|
-
Incremental.negative_closure(keys, @negative_dependents) |
|
|
1197
|
-
|
|
1362
|
+
Incremental.negative_closure(keys, @negative_dependents) | table_diff_affected(changed + removed, summary) |
|
|
1363
|
+
global_write_reporters
|
|
1364
|
+
end
|
|
1365
|
+
|
|
1366
|
+
# The consumers of the name-keyed edges whose producers diff a per-file table between each file's seed bundle
|
|
1367
|
+
# and the scan.
|
|
1368
|
+
def table_diff_affected(paths, summary)
|
|
1369
|
+
refinement_affected(paths, summary) | defined_name_affected(paths, summary) |
|
|
1370
|
+
visibility_name_affected(paths, summary) | mark_affected(paths, summary) |
|
|
1371
|
+
global_census_affected(paths, summary) | ivar_set_affected(paths, summary)
|
|
1372
|
+
end
|
|
1373
|
+
|
|
1374
|
+
# Issue #1833 — the consumers whose answer read a class mark that moved in this edit. A reader of a class's
|
|
1375
|
+
# envelope bucket records `mark:<last segment>` (`Scope#parameter_envelopes_of`), whichever way it answered, so
|
|
1376
|
+
# a file that adds or drops only a mark (`x.singleton_class.prepend(Comparable)`, `o.extend(Helper)` in a
|
|
1377
|
+
# method body, a `refine` block's refined mark) re-checks it although the file declares nothing it read. A
|
|
1378
|
+
# reader that scans every bucket for an object-extended module (`SourceArity#object_extension_may_shadow?`)
|
|
1379
|
+
# records `mark:*`, which fires when an object-extended mark moves. The before-state is each touched file's
|
|
1380
|
+
# seed bundle, the after-state the scan's merged table.
|
|
1381
|
+
#
|
|
1382
|
+
# Issue #1661 — and `mixin:<qualified name>` for a class or module whose unpositioned mixin edges moved
|
|
1383
|
+
# ({Incremental.changed_unpositioned_owners}): an external chain entry records it for each name it can denote
|
|
1384
|
+
# (`ResolutionChain#record_hooks`), so a new file that taints `Comparable` with `include(*MODS)` re-checks the
|
|
1385
|
+
# readers of a chain through it. It is not `mark:`, which a plain reopening of a module moves (the bucket's
|
|
1386
|
+
# module mark) and which would re-check every reader of `Kernel` on any `module Kernel` reopening.
|
|
1387
|
+
def mark_affected(paths, summary)
|
|
1388
|
+
def_index = summary && summary[:def_index]
|
|
1389
|
+
before = paths.map { |path| @seed_bundles.dig(path, :parameter_envelopes) }
|
|
1390
|
+
moved = Incremental.changed_class_marks(before, [def_index && def_index[:parameter_envelopes]])
|
|
1391
|
+
tainted = Incremental.changed_unpositioned_owners(
|
|
1392
|
+
paths.map { |path| @seed_bundles.dig(path, :unpositioned_mixins) },
|
|
1393
|
+
[def_index && def_index[:unpositioned_mixins]]
|
|
1394
|
+
)
|
|
1395
|
+
return Set.new if moved.empty? && tainted.empty?
|
|
1396
|
+
|
|
1397
|
+
keys = moved.map { |(class_name, _mark)| "mark:#{class_name.split('::').last}" }.uniq
|
|
1398
|
+
keys.concat(mixin_keys(tainted))
|
|
1399
|
+
extended = Scope::DiscoveryIndex::ENVELOPE_OBJECT_EXTENDED_MARK
|
|
1400
|
+
keys << "mark:*" if moved.any? { |(_class_name, mark)| mark == extended }
|
|
1401
|
+
Incremental.negative_closure(keys, @negative_dependents)
|
|
1402
|
+
end
|
|
1403
|
+
|
|
1404
|
+
# Issue #1661 — the `mixin:` keys an owner of a moved unpositioned edge fires. The table keys a receiver-form
|
|
1405
|
+
# owner by the spelling written (`class File; Constants.include(*MODS); end` is `"Constants"`), not the constant
|
|
1406
|
+
# Ruby resolves it to, while the chain files the qualified candidate (`mixin:File::Constants`). The table does
|
|
1407
|
+
# not carry the nesting, and every lexical-prefix candidate of the owner ends in `::<owner>`, so the owner fires
|
|
1408
|
+
# its own key and every recorded `mixin:` key ending in `::<owner>`. A relative spelling over-fires to the
|
|
1409
|
+
# readers of every module sharing its trailing segments; a qualified one fires only its suffix matches.
|
|
1410
|
+
def mixin_keys(owners)
|
|
1411
|
+
keys = owners.map { |name| "mixin:#{name}" }
|
|
1412
|
+
suffixes = owners.map { |name| "::#{name}" }
|
|
1413
|
+
@negative_dependents.each_key do |key|
|
|
1414
|
+
keys << key if key.start_with?("mixin:") && suffixes.any? { |suffix| key.end_with?(suffix) }
|
|
1415
|
+
end
|
|
1416
|
+
keys.uniq
|
|
1198
1417
|
end
|
|
1199
1418
|
|
|
1200
1419
|
# The negative keys a class-declaration diff fires: `class:<last segment>` for a class name, and (issue #1820)
|
|
@@ -1238,6 +1457,53 @@ module Rigor
|
|
|
1238
1457
|
Incremental.negative_closure(names.map { |name| "defines:#{name}" }, @negative_dependents)
|
|
1239
1458
|
end
|
|
1240
1459
|
|
|
1460
|
+
# Issue #2158 — the consumers whose `global.*` write rule read a census entry that a file in `paths` started or
|
|
1461
|
+
# stopped writing. The before-state is each file's seed bundle, the after-state the scan's per-file census. The
|
|
1462
|
+
# write the census silenced reports nothing, so {#global_write_reporters} does not reach it.
|
|
1463
|
+
def global_census_affected(paths, summary)
|
|
1464
|
+
after = (summary && summary[:global_write_census]) || {}
|
|
1465
|
+
before = paths.to_h { |path| [path, @seed_bundles.dig(path, :global_write_census)] }
|
|
1466
|
+
keys = Incremental.changed_global_census_keys(paths, before, after)
|
|
1467
|
+
return Set.new if keys.empty?
|
|
1468
|
+
|
|
1469
|
+
Incremental.negative_closure(keys, @negative_dependents)
|
|
1470
|
+
end
|
|
1471
|
+
|
|
1472
|
+
# Issue #1154 — the consumers whose declared-parameter ivar credit read an ivar writer entry a file in `paths`
|
|
1473
|
+
# started or stopped recording ({GlobalWriteCensus.ivar_key}: `ivar_set:@x`, `ivar_set_class:K`,
|
|
1474
|
+
# `own_write:K#@x`). The before-state is each file's seed bundle, the after-state the scan's per-file census. The
|
|
1475
|
+
# write sits in a method body, so neither the declaration signature nor the symbol fingerprints move with it.
|
|
1476
|
+
def ivar_set_affected(paths, summary)
|
|
1477
|
+
after = (summary && summary[:global_write_census]) || {}
|
|
1478
|
+
keys = Set.new
|
|
1479
|
+
paths.each do |path|
|
|
1480
|
+
census = @seed_bundles.dig(path, :global_write_census) || Inference::GlobalWriteCensus::EMPTY
|
|
1481
|
+
before = Inference::GlobalWriteCensus.ivar_set_entries(census)
|
|
1482
|
+
now = Inference::GlobalWriteCensus.ivar_set_entries(after[path] || Inference::GlobalWriteCensus::EMPTY)
|
|
1483
|
+
(before ^ now).each { |entry| keys << Inference::GlobalWriteCensus.ivar_key(entry) }
|
|
1484
|
+
end
|
|
1485
|
+
return Set.new if keys.empty?
|
|
1486
|
+
|
|
1487
|
+
Incremental.negative_closure(keys.to_a, @negative_dependents)
|
|
1488
|
+
end
|
|
1489
|
+
|
|
1490
|
+
# Issue #2134 — the consumers whose answer read a class's visibility slot for a method, or its contest, for each
|
|
1491
|
+
# slot whose entries a file in `paths` added, dropped or changed (`Scope#discovered_method_visibility`). The
|
|
1492
|
+
# before-state is each file's seed bundle, the after-state the scan's per-file entries. A `private :foo` in a
|
|
1493
|
+
# reopening or a `class_eval` block moves no `def`, so neither the symbol fingerprints nor the defined-name diff
|
|
1494
|
+
# sees it.
|
|
1495
|
+
def visibility_name_affected(paths, summary)
|
|
1496
|
+
after = (summary && summary[:visibility_entries]) || {}
|
|
1497
|
+
before = paths.to_h do |path|
|
|
1498
|
+
bundle = @seed_bundles[path]
|
|
1499
|
+
[path, bundle && Inference::ScopeIndexer.visibility_entries(bundle)]
|
|
1500
|
+
end
|
|
1501
|
+
slots = Incremental.changed_visibility_slots(paths, before, after)
|
|
1502
|
+
return Set.new if slots.empty?
|
|
1503
|
+
|
|
1504
|
+
Incremental.negative_closure(slots.map { |slot| "visibility:#{slot}" }, @negative_dependents)
|
|
1505
|
+
end
|
|
1506
|
+
|
|
1241
1507
|
# The qualified class/module names declared in the pre-parsed `index` (shared with
|
|
1242
1508
|
# {#symbol_fingerprints_from_index}, so the changed set is parsed once). `{ path => Set<class name> }`.
|
|
1243
1509
|
def class_declarations_from_index(index)
|
|
@@ -1400,7 +1666,22 @@ module Rigor
|
|
|
1400
1666
|
# and (#1898) their effect collections, so the run closes its effect graph over the whole project.
|
|
1401
1667
|
def served_state(reused)
|
|
1402
1668
|
{ served_per_file_diagnostics: @cache.slice(*reused),
|
|
1403
|
-
served_effect_collections: @effect_collections.slice(*reused)
|
|
1669
|
+
served_effect_collections: @effect_collections.slice(*reused),
|
|
1670
|
+
served_run_disclosures: @plugin_disclosures.served(reused, @stale_disclosures) }
|
|
1671
|
+
end
|
|
1672
|
+
|
|
1673
|
+
# Issue #2020 — folds the run's run-scoped registrations into the ledger, refreshing the keys this recheck found
|
|
1674
|
+
# stale. Extracted to keep {#absorb} under its ABC budget.
|
|
1675
|
+
def absorb_disclosures(runner, analyze_set)
|
|
1676
|
+
@plugin_disclosures.absorb(runner.analysis_disclosures, analyze_set, @stale_disclosures)
|
|
1677
|
+
@stale_disclosures = Set.new
|
|
1678
|
+
end
|
|
1679
|
+
|
|
1680
|
+
# Issue #2020 — the run-scoped plugin rows whose reads went stale ({RunDisclosureLedger}): one registering file
|
|
1681
|
+
# each joins the closure, and their replays are withheld until {#absorb} refreshes them from that file.
|
|
1682
|
+
def disclosure_refreshers(removed)
|
|
1683
|
+
@stale_disclosures = @plugin_disclosures.stale_identities { |row| plugin_read_fresh?(row) }
|
|
1684
|
+
@plugin_disclosures.refreshers(@stale_disclosures, removed.to_set)
|
|
1404
1685
|
end
|
|
1405
1686
|
|
|
1406
1687
|
# Issue #1898 — the plugin effect-fact digest a run's collections were stamped under. A plugin writes its
|
|
@@ -1419,6 +1700,21 @@ module Rigor
|
|
|
1419
1700
|
effect_plugin_digest_of(runner) != @effect_plugin_digest
|
|
1420
1701
|
end
|
|
1421
1702
|
|
|
1703
|
+
# PR #2286 review M1 — whether the ADR-16 synthetic-method index `runner` built differs from the one the
|
|
1704
|
+
# served files were analysed under. The index comes from class-body DSL calls (`devise :confirmable`,
|
|
1705
|
+
# a Tier C `attribute`), which no declaration signature records and no dependency edge names, so a file
|
|
1706
|
+
# that dispatched through a synthesised member is not in the closure of the edit that removed it. The
|
|
1707
|
+
# comparison waits for the run because the index is built by its pre-pass; a move re-analyses the project.
|
|
1708
|
+
def synthetic_surface_moved?(runner, reused)
|
|
1709
|
+
return false if reused.empty?
|
|
1710
|
+
|
|
1711
|
+
synthetic_surface_digest_of(runner) != @synthetic_surface_digest
|
|
1712
|
+
end
|
|
1713
|
+
|
|
1714
|
+
def synthetic_surface_digest_of(runner)
|
|
1715
|
+
runner.synthetic_method_index&.surface_digest
|
|
1716
|
+
end
|
|
1717
|
+
|
|
1422
1718
|
# The answer {#recheck} gives when {#served_effects_stale?}: a full baseline, reported as a recheck that
|
|
1423
1719
|
# re-analysed every file. {#run_incremental} reads `rebaselined` so it neither runs a second baseline
|
|
1424
1720
|
# nor reports the run as warm.
|
|
@@ -1429,9 +1725,10 @@ module Rigor
|
|
|
1429
1725
|
end
|
|
1430
1726
|
|
|
1431
1727
|
# A {Recheck} over `deltas` (`[changed, added, removed]`), counting `runner`'s template units.
|
|
1432
|
-
def recheck_result(diagnostics, deltas, affected:, reused:, runner:, rebaselined: false)
|
|
1728
|
+
def recheck_result(diagnostics, deltas, affected:, reused:, runner:, rebaselined: false, stale_readers: Set.new)
|
|
1433
1729
|
changed, added, removed = deltas.map(&:to_set)
|
|
1434
1730
|
Recheck.new(diagnostics: diagnostics, changed: changed, added: added, removed: removed, affected: affected,
|
|
1731
|
+
stale_readers: stale_readers,
|
|
1435
1732
|
reused: reused, templates: runner.template_unit_paths.size, rebaselined: rebaselined)
|
|
1436
1733
|
end
|
|
1437
1734
|
|
|
@@ -1502,19 +1799,12 @@ module Rigor
|
|
|
1502
1799
|
def rebind_buffer(files)
|
|
1503
1800
|
return if @buffer.nil? || files.include?(@buffer.logical_path)
|
|
1504
1801
|
|
|
1505
|
-
target = real_path(@buffer.logical_path)
|
|
1506
|
-
return if target.nil?
|
|
1802
|
+
target = BufferBinding.real_path(@buffer.logical_path)
|
|
1507
1803
|
|
|
1508
|
-
match = files.find { |path| real_path(path) == target }
|
|
1804
|
+
match = files.find { |path| BufferBinding.real_path(path) == target }
|
|
1509
1805
|
@buffer = BufferBinding.new(logical_path: match, physical_path: @buffer.physical_path) if match
|
|
1510
1806
|
end
|
|
1511
1807
|
|
|
1512
|
-
def real_path(path)
|
|
1513
|
-
File.realpath(path)
|
|
1514
|
-
rescue StandardError
|
|
1515
|
-
nil
|
|
1516
|
-
end
|
|
1517
|
-
|
|
1518
1808
|
# True when `path`'s recorded stat entry proves it unchanged since the last analysis. Any stat / parse
|
|
1519
1809
|
# failure (missing entry, unreadable / vanished file) reads as NOT fresh (→ re-analyse), preserving the
|
|
1520
1810
|
# prior `digest(path) != recorded` "changed" semantics for a file that cannot be validated.
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../rooted_glob"
|
|
4
|
+
require_relative "diagnostic"
|
|
5
|
+
|
|
3
6
|
module Rigor
|
|
4
7
|
module Analysis
|
|
5
8
|
# ADR-87 WD4 — project file-set expansion shared by the {Analysis::Runner} (miss path) and the
|
|
@@ -8,8 +11,13 @@ module Rigor
|
|
|
8
11
|
# is taken verbatim; anything else contributes no files here (the Runner layers its own path-error
|
|
9
12
|
# diagnostics and buffer / in-memory handling on top — none of which add or remove files for an ordinary
|
|
10
13
|
# on-disk check, so the two file sets agree).
|
|
14
|
+
#
|
|
15
|
+
# #1991 — also the one place a path that names neither a directory nor a `.rb` file is worded and
|
|
16
|
+
# severity-graded, so `rigor check` and `rigor sig-gen` report the same path error the same way.
|
|
11
17
|
module PathExpansion
|
|
12
18
|
RUBY_GLOB = "**/*.rb"
|
|
19
|
+
NO_SUCH_PATH = "no such file or directory"
|
|
20
|
+
NOT_A_RUBY_FILE = "not a Ruby file (expected `.rb` or a directory)"
|
|
13
21
|
|
|
14
22
|
module_function
|
|
15
23
|
|
|
@@ -26,8 +34,43 @@ module Rigor
|
|
|
26
34
|
end
|
|
27
35
|
end
|
|
28
36
|
|
|
37
|
+
# The `[path, message]` pairs for each of `paths` that names neither a directory nor a `.rb` file,
|
|
38
|
+
# worded as `rigor check` words them.
|
|
39
|
+
def unusable(paths)
|
|
40
|
+
Array(paths).each_with_object([]) do |path, bad|
|
|
41
|
+
next if File.directory?(path) || (File.file?(path) && path.end_with?(".rb"))
|
|
42
|
+
|
|
43
|
+
bad << [path, unusable_message(path)]
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def unusable_message(path)
|
|
48
|
+
File.exist?(path) ? NOT_A_RUBY_FILE : NO_SUCH_PATH
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# The error diagnostics for `bad` (from {unusable}). A bad path among valid ones is a warning that it was
|
|
52
|
+
# skipped; a bad path that leaves nothing to analyse is an error, so a lone typo is not masked.
|
|
53
|
+
def diagnostics(bad, any_files:)
|
|
54
|
+
severity = any_files ? :warning : :error
|
|
55
|
+
suffix = any_files ? " (skipped)" : ""
|
|
56
|
+
bad.map do |path, message|
|
|
57
|
+
Diagnostic.new(path: path, line: 1, column: 1, message: "#{message}#{suffix}", severity: severity)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
29
61
|
def directory_files(dir, exclude_patterns, base: Dir.pwd)
|
|
30
|
-
reject_excluded(
|
|
62
|
+
reject_excluded(RootedGlob.glob(dir, RUBY_GLOB), exclude_patterns, base: base)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# The configured project files, expanded as `rigor check` expands its configured paths: a directory globs,
|
|
66
|
+
# any other entry (a file, or a path that does not exist) is kept verbatim and left to the reader to skip.
|
|
67
|
+
# The file list a cross-file discovery seed is folded over ({Protection::DiscoverySeed}).
|
|
68
|
+
def configured_files(configuration)
|
|
69
|
+
Array(configuration.paths).flat_map do |path|
|
|
70
|
+
next [path] unless File.directory?(path)
|
|
71
|
+
|
|
72
|
+
directory_files(path, configuration.exclude_patterns, base: configuration.exclude_base)
|
|
73
|
+
end
|
|
31
74
|
end
|
|
32
75
|
|
|
33
76
|
# `exclude_patterns` are matched with `File.fnmatch?` WITHOUT `FNM_PATHNAME`, so `**`/`*` span separators
|