rigortype 0.4.3 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +5 -5
- data/data/core_overlay/ostruct.rbs +40 -0
- data/docs/handbook/11-sig-gen.md +104 -15
- data/docs/manual/02-cli-reference.md +43 -12
- data/docs/manual/03-configuration.md +20 -7
- data/docs/manual/04-diagnostics.md +56 -3
- data/docs/manual/05-inspecting-types.md +2 -2
- data/docs/manual/06-baseline.md +1 -1
- data/docs/manual/07-plugins.md +3 -2
- data/docs/manual/08-skills.md +7 -0
- data/docs/manual/10-mcp-server.md +40 -4
- data/docs/manual/18-removing-dead-code.md +23 -2
- data/docs/manual/plugins/rigor-rails-routes.md +2 -2
- data/docs/manual/plugins/rigor-rspec.md +101 -1
- data/docs/manual/plugins/rigor-shoulda-matchers.md +4 -1
- data/exe/rigor +12 -17
- data/lib/rigor/analysis/check_rules/clause_coverage.rb +134 -0
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +111 -7
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +190 -0
- data/lib/rigor/analysis/check_rules/rule_ids.rb +60 -1
- data/lib/rigor/analysis/check_rules/source_arity.rb +32 -3
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +13 -4
- data/lib/rigor/analysis/check_rules.rb +274 -51
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +2 -1
- data/lib/rigor/analysis/incremental.rb +61 -0
- data/lib/rigor/analysis/incremental_run_slot/write_guard.rb +36 -18
- data/lib/rigor/analysis/incremental_session.rb +270 -28
- data/lib/rigor/analysis/path_expansion.rb +3 -1
- data/lib/rigor/analysis/reachability/graph.rb +111 -14
- data/lib/rigor/analysis/reachability/scan.rb +305 -23
- data/lib/rigor/analysis/rule_catalog.rb +22 -4
- data/lib/rigor/analysis/run_cache_key.rb +31 -7
- data/lib/rigor/analysis/run_cache_probe.rb +5 -3
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +48 -12
- data/lib/rigor/analysis/runner/pool_coordinator.rb +179 -31
- data/lib/rigor/analysis/runner/project_pre_passes.rb +29 -4
- data/lib/rigor/analysis/runner.rb +102 -72
- data/lib/rigor/analysis/severity_stamp.rb +45 -1
- data/lib/rigor/analysis/worker_session.rb +69 -2
- data/lib/rigor/cache/descriptor.rb +79 -12
- data/lib/rigor/cache/engine_source.rb +3 -2
- data/lib/rigor/cache/incremental_snapshot.rb +39 -5
- data/lib/rigor/cache/rbs_descriptor.rb +2 -1
- data/lib/rigor/cache/store.rb +4 -3
- data/lib/rigor/cli/command.rb +3 -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/mcp_command.rb +1 -1
- data/lib/rigor/cli/plugin_command.rb +2 -1
- data/lib/rigor/cli/plugins_command.rb +2 -1
- data/lib/rigor/cli/sig_gen_command.rb +32 -11
- data/lib/rigor/cli/skill_command.rb +2 -1
- data/lib/rigor/cli/skill_describe.rb +3 -2
- data/lib/rigor/cli/unused_command.rb +10 -7
- data/lib/rigor/cli.rb +3 -2
- data/lib/rigor/config_audit.rb +163 -22
- data/lib/rigor/configuration/dependencies.rb +19 -11
- data/lib/rigor/configuration/severity_profile.rb +20 -2
- data/lib/rigor/configuration.rb +29 -3
- 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 +67 -11
- data/lib/rigor/inference/block_call_timing.rb +12 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +2 -1
- data/lib/rigor/inference/captured_locals.rb +67 -13
- data/lib/rigor/inference/clause_widening.rb +90 -0
- data/lib/rigor/inference/definer_resolution.rb +101 -25
- data/lib/rigor/inference/expression_typer.rb +94 -9
- data/lib/rigor/inference/guard_rebinding.rb +7 -2
- data/lib/rigor/inference/intervening_ivar_reset.rb +303 -0
- data/lib/rigor/inference/last_line/self_evidence.rb +1 -1
- data/lib/rigor/inference/macro_block_self_type.rb +20 -6
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +170 -35
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +50 -2
- data/lib/rigor/inference/method_dispatcher.rb +67 -3
- data/lib/rigor/inference/module_function_state.rb +213 -51
- data/lib/rigor/inference/narrowing.rb +235 -75
- 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 +120 -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 +116 -0
- data/lib/rigor/inference/repeated_or_writes.rb +414 -1
- data/lib/rigor/inference/return_barrier.rb +16 -0
- data/lib/rigor/inference/scope_indexer.rb +1061 -122
- data/lib/rigor/inference/self_call_ivar_writes.rb +346 -0
- 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 +512 -150
- data/lib/rigor/inference/unthreaded_rebinds.rb +3 -1
- data/lib/rigor/lens/anchor.rb +3 -3
- data/lib/rigor/lens/contract_reader.rb +60 -2
- data/lib/rigor/lens/document.rb +4 -3
- data/lib/rigor/lens/file_reader.rb +452 -73
- data/lib/rigor/lens/members.rb +81 -17
- data/lib/rigor/lens/run.rb +73 -4
- data/lib/rigor/lens.rb +2 -1
- data/lib/rigor/mcp/server.rb +63 -22
- data/lib/rigor/plugin/base.rb +3 -2
- data/lib/rigor/plugin/box_probe.rb +154 -7
- data/lib/rigor/plugin/io_boundary.rb +42 -6
- data/lib/rigor/plugin/isolation.rb +4 -3
- data/lib/rigor/project_environment.rb +6 -3
- data/lib/rigor/protection/discovery_seed.rb +9 -36
- data/lib/rigor/protection/mutation_cache.rb +8 -2
- data/lib/rigor/rooted_glob.rb +27 -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 +56 -8
- 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 +167 -22
- data/lib/rigor/sig_gen/layout_index.rb +40 -1
- data/lib/rigor/sig_gen/method_candidate.rb +8 -0
- data/lib/rigor/sig_gen/observation_collector.rb +137 -11
- data/lib/rigor/sig_gen/path_mapper.rb +18 -2
- data/lib/rigor/sig_gen/renderer.rb +13 -155
- data/lib/rigor/sig_gen/skip_reason_catalog.rb +11 -0
- 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 +50 -35
- data/lib/rigor/signature_path_audit.rb +4 -2
- data/lib/rigor/type/constant.rb +10 -1
- 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 +1 -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-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-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-dry-schema/lib/rigor/plugin/dry_schema.rb +5 -1
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +1 -1
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
- data/plugins/rigor-factorybot/lib/rigor/plugin/factorybot/factory_discoverer.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +57 -6
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/target_detector.rb +1 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi.rb +1 -1
- data/plugins/rigor-graphql/lib/rigor/plugin/graphql.rb +5 -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 +155 -12
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +10 -8
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq/worker_discoverer.rb +1 -1
- 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/sig/rigor/plugin/io_boundary.rbs +5 -2
- data/sig/rigor/rooted_glob.rbs +7 -0
- data/sig/rigor/scope.rbs +4 -3
- data/skills/rigor-doctor/references/01-checks.md +23 -11
- data/skills/rigor-type-oracle/references/01-oracle-commands.md +2 -1
- metadata +16 -1
|
@@ -7,6 +7,7 @@ require_relative "../incremental_run_slot"
|
|
|
7
7
|
require_relative "../dependency_source_inference/gem_resolver"
|
|
8
8
|
require_relative "../../cache/file_digest"
|
|
9
9
|
require_relative "../../cache/incremental_snapshot"
|
|
10
|
+
require_relative "../../rooted_glob"
|
|
10
11
|
|
|
11
12
|
module Rigor
|
|
12
13
|
module Analysis
|
|
@@ -66,8 +67,8 @@ module Rigor
|
|
|
66
67
|
# opted-in gems' resolved versions, the key's one input that is neither a file nor the configuration (issue
|
|
67
68
|
# #1814), are read at the mark and watched the same way.
|
|
68
69
|
#
|
|
69
|
-
# The mark also records
|
|
70
|
-
# analysis roots, the `pre_eval:` entries and the signature roots.
|
|
70
|
+
# The mark also records what each path a presence row can name is ({Cache::Descriptor::FileEntry.kind_of}):
|
|
71
|
+
# the lockfiles, the analysis roots, the `pre_eval:` entries and the signature roots.
|
|
71
72
|
def self.start(configuration:, roots:, cache_root:, fingerprint:)
|
|
72
73
|
return nil if Gem.win_platform?
|
|
73
74
|
|
|
@@ -79,7 +80,7 @@ module Rigor
|
|
|
79
80
|
lockfiles = lockfile_paths(configuration)
|
|
80
81
|
named = Array(roots) + configuration.pre_eval + (configuration.signature_paths || ["sig"])
|
|
81
82
|
presence = (lockfiles + named.map { |path| File.absolute_path(path.to_s) }).to_h do |path|
|
|
82
|
-
[path,
|
|
83
|
+
[path, Cache::Descriptor::FileEntry.kind_of(path)]
|
|
83
84
|
end
|
|
84
85
|
rows = dependency_rows(configuration)
|
|
85
86
|
return nil if rows.nil?
|
|
@@ -174,11 +175,12 @@ module Rigor
|
|
|
174
175
|
# - no lockfile the key or the fingerprint digests was written, created or removed, and no opted-in gem
|
|
175
176
|
# resolves to another version than at the mark (issue #1814);
|
|
176
177
|
# - no file `rows` or `pinned` names changed. A content row's file changed if its change time moved. An
|
|
177
|
-
# existence row asks only whether its path is there,
|
|
178
|
-
# went,
|
|
179
|
-
#
|
|
180
|
-
#
|
|
181
|
-
#
|
|
178
|
+
# existence row asks only whether its path is there, and a `:kind` row what it is (#1977), so a path the
|
|
179
|
+
# mark recorded changed if it came or went, or for a kind row turned from a directory into a file or back;
|
|
180
|
+
# an editor's lock file created and removed beside it, which moves its directory's change time, does not
|
|
181
|
+
# count. That needs no clock, so it holds on any filesystem; it cannot see a path that comes and goes
|
|
182
|
+
# again within the run. Any other presence row falls back to change times: its own when present, its
|
|
183
|
+
# nearest existing ancestor's when absent;
|
|
182
184
|
# - no directory a glob row lists gained or lost an entry, nor, for a stat-mode glob, did any file it
|
|
183
185
|
# matches change.
|
|
184
186
|
#
|
|
@@ -231,15 +233,20 @@ module Rigor
|
|
|
231
233
|
ns.nil? || device != @device || ns < @started_ns
|
|
232
234
|
end
|
|
233
235
|
|
|
234
|
-
#
|
|
235
|
-
# is there, the second whether it changed), so the row that comes first must not hide the
|
|
236
|
+
# A presence row and a content row for one path are different questions (the first asks only whether it
|
|
237
|
+
# is there, or what it is, the second whether it changed), so the row that comes first must not hide the
|
|
238
|
+
# other; nor may an existence row hide a kind row.
|
|
239
|
+
PRESENCE_COMPARATORS = %i[exists kind].freeze
|
|
240
|
+
private_constant :PRESENCE_COMPARATORS
|
|
241
|
+
|
|
236
242
|
def files_unchanged?(entries, strict:)
|
|
237
243
|
seen = Set.new
|
|
238
244
|
entries.none? do |entry|
|
|
239
|
-
|
|
245
|
+
presence = PRESENCE_COMPARATORS.include?(entry.comparator) ? entry.comparator : nil
|
|
246
|
+
next false unless seen.add?([entry.path, presence])
|
|
240
247
|
|
|
241
|
-
if
|
|
242
|
-
|
|
248
|
+
if presence
|
|
249
|
+
presence_changed?(entry.path, presence, strict: strict)
|
|
243
250
|
else
|
|
244
251
|
changed?(entry.path, strict: strict)
|
|
245
252
|
end
|
|
@@ -248,8 +255,8 @@ module Rigor
|
|
|
248
255
|
|
|
249
256
|
def globs_unchanged?(entries, strict:)
|
|
250
257
|
entries.none? do |entry|
|
|
251
|
-
listed =
|
|
252
|
-
matched = entry.mode == :stat ?
|
|
258
|
+
listed = RootedGlob.glob(entry.root, File.join(File.dirname(entry.pattern), ""))
|
|
259
|
+
matched = entry.mode == :stat ? RootedGlob.glob(entry.root, entry.pattern) : []
|
|
253
260
|
(listed + matched).any? { |path| changed?(path, strict: strict) }
|
|
254
261
|
end
|
|
255
262
|
end
|
|
@@ -257,14 +264,25 @@ module Rigor
|
|
|
257
264
|
# A lockfile is read for its bytes: one absent at the mark must still be absent, one present must still be
|
|
258
265
|
# there with its change time before the mark.
|
|
259
266
|
def lockfile_changed?(path)
|
|
260
|
-
return File.exist?(path) unless
|
|
267
|
+
return File.exist?(path) unless present_at_mark?(path)
|
|
261
268
|
|
|
262
269
|
changed?(path, strict: true, missing: true)
|
|
263
270
|
end
|
|
264
271
|
|
|
265
|
-
def
|
|
272
|
+
def present_at_mark?(path)
|
|
273
|
+
kind = @presence[path]
|
|
274
|
+
!kind.nil? && kind != Cache::Descriptor::FileEntry::KIND_ABSENT
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# `comparator` is `:exists` or `:kind`: whether the path came or went, or whether it is no longer the kind
|
|
278
|
+
# of thing the mark recorded.
|
|
279
|
+
def presence_changed?(path, comparator, strict:)
|
|
266
280
|
absolute = File.absolute_path(path)
|
|
267
|
-
|
|
281
|
+
if @presence.key?(absolute)
|
|
282
|
+
return File.exist?(absolute) != present_at_mark?(absolute) if comparator == :exists
|
|
283
|
+
|
|
284
|
+
return Cache::Descriptor::FileEntry.kind_of(absolute) != @presence[absolute]
|
|
285
|
+
end
|
|
268
286
|
|
|
269
287
|
nearest = absolute
|
|
270
288
|
nearest = File.dirname(nearest) until File.exist?(nearest) || File.dirname(nearest) == nearest
|
|
@@ -38,11 +38,21 @@ module Rigor
|
|
|
38
38
|
# gate) can report what was re-analyzed versus served from cache. `added` / `removed` carry the
|
|
39
39
|
# structural delta so {#run_incremental} (ADR-87 WD3) can recognise a zero-change recheck and skip the
|
|
40
40
|
# unconditional snapshot rewrite.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
# `rebaselined` (#1898) is true when the recheck found its served effect collections stale and answered with a
|
|
42
|
+
# full baseline instead; see {#rebaseline_recheck}.
|
|
43
|
+
# `stale_readers` (#1553) are the files the recheck re-analysed because a plugin read one of them made went
|
|
44
|
+
# stale ({#plugin_read_affected}).
|
|
45
|
+
Recheck = Data.define(:diagnostics, :changed, :added, :removed, :affected, :reused, :templates, :rebaselined,
|
|
46
|
+
:stale_readers) do
|
|
47
|
+
def initialize(rebaselined: false, stale_readers: Set.new, **rest)
|
|
48
|
+
super
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# A recheck that changed, added, and removed nothing, and found no stale plugin read — the session state is
|
|
52
|
+
# byte-equivalent to the snapshot that was restored, so persisting it again would only rewrite identical
|
|
53
|
+
# bytes. A stale reader's refreshed rows differ (#1553): unsaved, every later run would re-analyse it.
|
|
44
54
|
def no_change?
|
|
45
|
-
changed.empty? && added.empty? && removed.empty?
|
|
55
|
+
changed.empty? && added.empty? && removed.empty? && stale_readers.empty?
|
|
46
56
|
end
|
|
47
57
|
end
|
|
48
58
|
|
|
@@ -93,6 +103,9 @@ module Rigor
|
|
|
93
103
|
@cache = {} # analyzed path => [Diagnostic]
|
|
94
104
|
@sources = {} # analyzed path => Set<source path it read from>
|
|
95
105
|
@digests = {} # analyzed path => ADR-87 packed stat-digest entry at last analysis
|
|
106
|
+
# Issue #1583 — discovered-not-analysed path => the same packed entry, for a run over path arguments that
|
|
107
|
+
# #684 widened to the configured `paths:`. Empty on every run over the configured paths.
|
|
108
|
+
@discovery_digests = {}
|
|
96
109
|
@analyzed = [] # the project files analyzed last round
|
|
97
110
|
@dependents = {} # inverted @sources (file-level)
|
|
98
111
|
# ADR-46 slice 4 — symbol-granularity tracking.
|
|
@@ -122,6 +135,10 @@ module Rigor
|
|
|
122
135
|
# and a missing invalidation edge is impossible by construction — the reason this is a table diff
|
|
123
136
|
# and not the caller→callee edge recording #204 first sketched.
|
|
124
137
|
@param_table = {}
|
|
138
|
+
# Issue #1553 — analysed path => the plugin reads ({Plugin::IoBoundary.attributing}) its last analysis made:
|
|
139
|
+
# the frozen `Cache::Descriptor::FileEntry` / `GlobEntry` rows, as each read observed its input. A recheck
|
|
140
|
+
# re-analyses every file one of whose rows no longer validates ({#plugin_read_affected}).
|
|
141
|
+
@plugin_reads = {}
|
|
125
142
|
reset_carried_state
|
|
126
143
|
# ADR-88 WD1 — the plugin fact-surface digest computed for THIS invocation (nil until a
|
|
127
144
|
# `#run_incremental` pass runs / a plugin-free project) and the reporting flags a caller (the CLI
|
|
@@ -145,6 +162,49 @@ module Rigor
|
|
|
145
162
|
current_files
|
|
146
163
|
end
|
|
147
164
|
|
|
165
|
+
# Issue #1583 (and #1560) — whether a file the run discovers but does not analyse was edited, added or
|
|
166
|
+
# removed since the snapshot. Such a file feeds the cross-file index the analysed files resolve against
|
|
167
|
+
# (a constant, a method, an ancestor), but no recorded edge or digest of the analysed set sees it move.
|
|
168
|
+
# A move re-baselines the session: discovery-only files are not tracked per symbol, so the closure
|
|
169
|
+
# cannot be narrowed, and the full analysis is over the argument set alone. A session over the
|
|
170
|
+
# configured `paths:` discovers nothing beyond what it analyses and pays nothing here.
|
|
171
|
+
def discovery_moved?
|
|
172
|
+
current = current_discovery_only_files
|
|
173
|
+
return true unless current.size == @discovery_digests.size
|
|
174
|
+
|
|
175
|
+
current.any? do |path|
|
|
176
|
+
entry = @discovery_digests[path]
|
|
177
|
+
entry.nil? || !Cache::FileDigest.stat_fresh?(path, entry)
|
|
178
|
+
end
|
|
179
|
+
rescue StandardError
|
|
180
|
+
true
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# A baseline's freshness entries: each analysed file's, and (issue #1583) each discovered-not-analysed file's.
|
|
184
|
+
def record_baseline_digests
|
|
185
|
+
@digests = @analyzed.to_h { |path| [path, pack_digest(path)] }
|
|
186
|
+
@discovery_digests = current_discovery_only_files.to_h { |path| [path, pack_digest(path)] }
|
|
187
|
+
forget_unbound_digests
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def restore_digests(payload)
|
|
191
|
+
@digests = payload.digests
|
|
192
|
+
@discovery_digests = payload.discovery_digests # Issue #1583 — `{}` in a payload built without it.
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Issue #1583 — the discovered-not-analysed files of a run over this session's paths
|
|
196
|
+
# ({Runner#discovery_only_file_set}). Asked of a runner WITHOUT the editor buffer: a buffered run does not
|
|
197
|
+
# widen, so a buffered session (editor mode option B, #146) would read an empty set against the snapshot's
|
|
198
|
+
# and re-baseline on every keystroke. Globbing without it keeps the comparison the one the snapshot's
|
|
199
|
+
# writer made, so a sibling edit still declines option B and an unchanged project still serves it.
|
|
200
|
+
def current_discovery_only_files
|
|
201
|
+
return [] if @paths.nil?
|
|
202
|
+
|
|
203
|
+
Runner.new(configuration: @configuration, cache_store: nil, environment: @environment)
|
|
204
|
+
.discovery_only_file_set(@paths)
|
|
205
|
+
end
|
|
206
|
+
private :discovery_moved?, :record_baseline_digests, :restore_digests, :current_discovery_only_files
|
|
207
|
+
|
|
148
208
|
# The per-file discovery bundles of the last run, keyed by path (ADR-85 WD2).
|
|
149
209
|
attr_reader :seed_bundles
|
|
150
210
|
|
|
@@ -208,9 +268,10 @@ module Rigor
|
|
|
208
268
|
# ADR-103 WD13 / #382 — a baseline collects every file, so its collections are the whole world.
|
|
209
269
|
@effect_collections = runner.effect_collections_by_path
|
|
210
270
|
@effects_identity = current_effects_identity
|
|
271
|
+
@effect_plugin_digest = effect_plugin_digest_of(runner)
|
|
211
272
|
@cache = per_file(runner.per_file_diagnostics)
|
|
212
|
-
@
|
|
213
|
-
|
|
273
|
+
@plugin_reads = plugin_reads_of(runner, @analyzed)
|
|
274
|
+
record_baseline_digests
|
|
214
275
|
@run_level_rows = runner.run_level_rows
|
|
215
276
|
diagnostics
|
|
216
277
|
end
|
|
@@ -224,6 +285,8 @@ module Rigor
|
|
|
224
285
|
added = current - previous
|
|
225
286
|
removed = previous - current
|
|
226
287
|
changed = changed_paths(current & previous)
|
|
288
|
+
return rebaseline_recheck(changed, added, removed) if discovery_moved?
|
|
289
|
+
|
|
227
290
|
# ADR-67 WD6c lift — recompute the whole-project inferred-param table BEFORE deciding the closure,
|
|
228
291
|
# and diff it against the snapshot's copy: an entry that moved (because a caller's argument type
|
|
229
292
|
# changed, a caller appeared, or one vanished) invalidates the CALLEE's file and its symbol
|
|
@@ -238,15 +301,14 @@ module Rigor
|
|
|
238
301
|
# go in too, so the runner lists them in the full run's file order (#1524).
|
|
239
302
|
runner = build_runner(analyze_only: analyze_set, record_dependencies: true,
|
|
240
303
|
param_inferred_types: fresh_params, **forced_target_options(forced),
|
|
241
|
-
restored_run_level_rows: @run_level_rows,
|
|
242
|
-
served_per_file_diagnostics: @cache.slice(*reused))
|
|
304
|
+
restored_run_level_rows: @run_level_rows, **served_state(reused))
|
|
243
305
|
merged = run_runner(runner).diagnostics
|
|
244
|
-
|
|
306
|
+
return rebaseline_recheck(changed, added, removed) if served_effects_stale?(runner, reused)
|
|
307
|
+
|
|
245
308
|
absorb(runner, current, analyze_set, removed)
|
|
246
309
|
@param_table = fresh_params
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
templates: runner.template_unit_paths.size)
|
|
310
|
+
recheck_result(merged, [changed, added, removed], affected: affected, reused: reused.to_set, runner: runner,
|
|
311
|
+
stale_readers: @stale_plugin_readers)
|
|
250
312
|
end
|
|
251
313
|
|
|
252
314
|
# Editor mode option B (#146) — a whole-project recheck with the editor's buffer substituted for one
|
|
@@ -271,6 +333,9 @@ module Rigor
|
|
|
271
333
|
# latency this mode exists to avoid, so decline and let the caller drop to single-file scope.
|
|
272
334
|
break nil unless @plugin_fact_reusable.reusable_against?(restored.plugin_fact_digest)
|
|
273
335
|
break nil if source_rbs_unverified?(result)
|
|
336
|
+
# #1898 — a recheck that re-baselined over stale plugin effect facts ran its baseline with the buffer set,
|
|
337
|
+
# which analyses the buffer alone, so it is not a whole-project answer. Decline, as above.
|
|
338
|
+
break nil if result.rebaselined
|
|
274
339
|
|
|
275
340
|
result
|
|
276
341
|
end
|
|
@@ -296,9 +361,10 @@ module Rigor
|
|
|
296
361
|
# records none at all), so no dependent set can bound who read it. An edit that moved it re-analyses the
|
|
297
362
|
# whole project — what a `sig/` edit already costs through the snapshot fingerprint.
|
|
298
363
|
def affected_closure(changed, added, removed, param_files = Set.new, param_pairs = Set.new)
|
|
364
|
+
@stale_plugin_readers = plugin_read_affected
|
|
299
365
|
return whole_project_closure(changed, added) if source_rbs_gate.moved?(@seed_bundles, changed, added, removed)
|
|
300
366
|
|
|
301
|
-
gated_closure(changed, added, removed, param_files, param_pairs)
|
|
367
|
+
(gated_closure(changed, added, removed, param_files, param_pairs) | @stale_plugin_readers).freeze
|
|
302
368
|
end
|
|
303
369
|
|
|
304
370
|
# {#affected_closure} for an edit whose synthesized RBS is byte-identical in every file it touched.
|
|
@@ -342,6 +408,65 @@ module Rigor
|
|
|
342
408
|
closure.freeze
|
|
343
409
|
end
|
|
344
410
|
|
|
411
|
+
# Issue #1553 — every analysed file whose last analysis made a plugin read that no longer validates: a file
|
|
412
|
+
# read through {Plugin::IoBoundary#read_file} or probed through `#file?` / `#directory?` was edited,
|
|
413
|
+
# removed, or created after a read that found it absent, or a `#list_directory` / `#glob` listing moved.
|
|
414
|
+
# The ADR-46 edges record type and declaration reads only, so none of these moves a reader into the
|
|
415
|
+
# closure, and its cached rows were computed from the input as it was. The reader's own text is unchanged,
|
|
416
|
+
# so it joins the closure alone: a plugin read made while analysing a dependent is credited to that
|
|
417
|
+
# dependent. Each distinct row is validated once however many files made it (one `File.stat` for a
|
|
418
|
+
# content row whose stat tuple did not move, one re-glob for a listing row).
|
|
419
|
+
def plugin_read_affected
|
|
420
|
+
verdicts = {}
|
|
421
|
+
@plugin_reads.each_with_object(Set.new) do |(path, rows), stale|
|
|
422
|
+
next unless rows.any? { |row| !verdicts.fetch(row) { verdicts[row] = plugin_read_fresh?(row) } }
|
|
423
|
+
|
|
424
|
+
stale << path
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def plugin_read_fresh?(row)
|
|
429
|
+
if row.is_a?(Cache::Descriptor::GlobEntry)
|
|
430
|
+
Cache::Descriptor::GlobEntry.fresh?(row)
|
|
431
|
+
else
|
|
432
|
+
Cache::Descriptor.new(files: [row]).fresh?
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
# Issue #1553 — `runner`'s per-file plugin reads for `paths`, each file's rows de-duplicated and frozen.
|
|
437
|
+
def plugin_reads_of(runner, paths)
|
|
438
|
+
runner.analysis_reads.slice(*paths).transform_values { |rows| rows.uniq.freeze }
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
# Issue #1906 — a file that rewrites a constant's singleton names without declaring it
|
|
442
|
+
# (`D.singleton_class.alias_method :own, :keep`, or, since #1941, `def D.own`) contributes opaque singleton
|
|
443
|
+
# envelopes to the constant's bucket, but a reader's class edge names only the files that declared the constant
|
|
444
|
+
# when the reader last ran, and the class-declaration diff does not move while the constant stays in the file
|
|
445
|
+
# (`:own` becoming `:keep`, or the call becoming a `class D` reopening). So a declaration-unstable edit to such a
|
|
446
|
+
# file fires `class:<last segment>` for each constant that, before the edit, both sat in the file's
|
|
447
|
+
# singleton-source set and held a singleton envelope entry or an unpositioned singleton side the file recorded (a
|
|
448
|
+
# computed name there leaves the side unpositioned, and becoming literal reads it again). A file that only
|
|
449
|
+
# includes into a constant (`ActiveRecord::Base.include(X)`) records neither and fires nothing. A constant the
|
|
450
|
+
# edit makes the file rewrite needs no key: the class-declaration diff fires if it is new to the file, and the
|
|
451
|
+
# reader's class edge names the file if the file declared it.
|
|
452
|
+
def singleton_rewrite_affected(unstable)
|
|
453
|
+
names = unstable.flat_map { |path| singleton_rewritten_constants(@seed_bundles[path]) }.uniq
|
|
454
|
+
return Set.new if names.empty?
|
|
455
|
+
|
|
456
|
+
Incremental.negative_closure(names.map { |name| "class:#{name.split('::').last}" }, @negative_dependents)
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
def singleton_rewritten_constants(bundle)
|
|
460
|
+
return [] if bundle.nil?
|
|
461
|
+
|
|
462
|
+
envelopes = bundle[:parameter_envelopes] || {}
|
|
463
|
+
unpositioned = bundle[:unpositioned_mixins] || {}
|
|
464
|
+
(bundle[:singleton_class_source_names] || []).select do |name|
|
|
465
|
+
(envelopes[name] || {}).each_key.any? { |key| key.is_a?(Array) && key.first == :singleton } ||
|
|
466
|
+
unpositioned.dig(name, :extend)
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
|
|
345
470
|
# Issue #1367 — every file whose last result holds a `global.*` diagnostic re-checks on any recheck. What
|
|
346
471
|
# exempts such a write is a fact of the whole program — an alias, or a `write` / `to_str` / `to_int` / hatch
|
|
347
472
|
# defined in any spelling on any receiver, a rewritten ancestor, a mixin — which no name-keyed edge can
|
|
@@ -365,13 +490,15 @@ module Rigor
|
|
|
365
490
|
|
|
366
491
|
# The dependents contributed by the declaration-unstable changed files and the behaviourally-unstable
|
|
367
492
|
# symbol pairs: the ADR-46 slice-4 symbol-granular fan-out when either is present (ancestry deps of the
|
|
368
|
-
# unstable files + symbol deps of the changed pairs), else the coarse file-level fan-out
|
|
493
|
+
# unstable files + symbol deps of the changed pairs), else the coarse file-level fan-out; plus the readers of
|
|
494
|
+
# the constants an unstable file rewrote from outside their bodies ({#singleton_rewrite_affected}).
|
|
369
495
|
def dependents_base(unstable, symbol_pairs)
|
|
370
|
-
if symbol_pairs.any? || unstable.any? { |f| @ancestry_dependents[f] }
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
496
|
+
base = if symbol_pairs.any? || unstable.any? { |f| @ancestry_dependents[f] }
|
|
497
|
+
Incremental.affected_with_symbols(unstable, symbol_pairs, @symbol_dependents, @ancestry_dependents)
|
|
498
|
+
else
|
|
499
|
+
Incremental.affected(unstable, @dependents)
|
|
500
|
+
end
|
|
501
|
+
base | singleton_rewrite_affected(unstable)
|
|
375
502
|
end
|
|
376
503
|
|
|
377
504
|
# ADR-67 WD6c lift — the fresh whole-project inferred-param table for this recheck ({} when the gate
|
|
@@ -466,7 +593,7 @@ module Rigor
|
|
|
466
593
|
def whole_project_closure(changed, added)
|
|
467
594
|
(@analyzed.to_set | changed | added).freeze
|
|
468
595
|
end
|
|
469
|
-
private :gated_closure, :whole_project_closure
|
|
596
|
+
private :gated_closure, :whole_project_closure, :singleton_rewrite_affected, :singleton_rewritten_constants
|
|
470
597
|
|
|
471
598
|
# The current project file set (cheap directory expansion, no analysis), used to detect files added /
|
|
472
599
|
# removed since the last run. Also where an editor buffer's binding is re-spelled onto the set, since
|
|
@@ -489,8 +616,7 @@ module Rigor
|
|
|
489
616
|
# ADR-67 WD6c lift — seed the subset run from the baseline's own table so the verification engine
|
|
490
617
|
# exercises the exact seeds the served cache entries were computed under (and skips a re-collect).
|
|
491
618
|
runner = build_runner(analyze_only: affected, param_inferred_types: @param_table,
|
|
492
|
-
restored_run_level_rows: @run_level_rows,
|
|
493
|
-
served_per_file_diagnostics: @cache.slice(*reused))
|
|
619
|
+
restored_run_level_rows: @run_level_rows, **served_state(reused))
|
|
494
620
|
run_runner(runner).diagnostics
|
|
495
621
|
end
|
|
496
622
|
|
|
@@ -534,7 +660,7 @@ module Rigor
|
|
|
534
660
|
result = recheck
|
|
535
661
|
adopt_plugin_fact_fingerprint
|
|
536
662
|
fact_reusable = @plugin_fact_reusable.reusable_against?(restored.plugin_fact_digest)
|
|
537
|
-
if fact_reusable && effects_reusable && !source_rbs_unverified?(result)
|
|
663
|
+
if fact_reusable && effects_reusable && !result.rebaselined && !source_rbs_unverified?(result)
|
|
538
664
|
diagnostics = keep_recheck(result)
|
|
539
665
|
warm = true
|
|
540
666
|
# ADR-87 WD3 — a warm recheck that changed nothing leaves the session state byte-equivalent to the
|
|
@@ -549,7 +675,8 @@ module Rigor
|
|
|
549
675
|
# digest (from the recheck runner) is unchanged by the re-analysis, so it is kept for the save.
|
|
550
676
|
# Only a genuine fact-surface reason sets the reporting flag the CLI banner reads.
|
|
551
677
|
@fact_surface_invalidated = true unless fact_reusable
|
|
552
|
-
|
|
678
|
+
# #1898 — a recheck that already re-baselined over stale plugin effect facts is that re-analysis.
|
|
679
|
+
diagnostics = result.rebaselined ? result.diagnostics : baseline
|
|
553
680
|
warm = false
|
|
554
681
|
skip_save = false
|
|
555
682
|
end
|
|
@@ -630,7 +757,7 @@ module Rigor
|
|
|
630
757
|
# the gate could not see. {#run_incremental} re-analyses the project and {#run_buffer_recheck} declines,
|
|
631
758
|
# the way each treats a moved plugin fact surface. A run that changed nothing is exempt: under an
|
|
632
759
|
# untrusted gate every earlier edit already re-analysed the whole project, so the snapshot it serves is
|
|
633
|
-
# whole, and a null run stays one.
|
|
760
|
+
# whole, and a null run stays one. A recheck that found a stale plugin read (#1553) is a change.
|
|
634
761
|
def source_rbs_unverified?(result)
|
|
635
762
|
source_rbs_gate.untrusted? && !result.no_change? && !result.reused.empty?
|
|
636
763
|
end
|
|
@@ -850,7 +977,7 @@ module Rigor
|
|
|
850
977
|
@analyzed = payload.analyzed
|
|
851
978
|
@cache = payload.cache
|
|
852
979
|
@sources = payload.sources
|
|
853
|
-
|
|
980
|
+
restore_digests(payload)
|
|
854
981
|
# ADR-85 WD2 — restore the per-file discovery bundles if present (absent in a pre-#85 snapshot → empty,
|
|
855
982
|
# so the recheck's discovery re-walks every file: a cold-quality index, always sound). The SCHEMA bump
|
|
856
983
|
# makes a genuinely stale-shaped bundle unreadable rather than mis-folded.
|
|
@@ -864,6 +991,7 @@ module Rigor
|
|
|
864
991
|
restore_structural_declarations(payload)
|
|
865
992
|
@return_summaries = payload.return_summaries || {}
|
|
866
993
|
@param_table = payload.param_table || {} # ADR-67 WD6c lift — the seeds the cache was built under.
|
|
994
|
+
@plugin_reads = payload.plugin_reads # Issue #1553 — `{}` in a payload built without it.
|
|
867
995
|
# Issues #796 / #794 — absent (nil) in a snapshot written before schema 22 is impossible (the SCHEMA
|
|
868
996
|
# gate rejects one), so nil here means the writing run genuinely had nothing to say.
|
|
869
997
|
@run_level_rows = payload.run_level_rows
|
|
@@ -873,6 +1001,7 @@ module Rigor
|
|
|
873
1001
|
# a partial re-collection cannot be closed into a whole-project fixpoint.
|
|
874
1002
|
@effects_identity = payload.effects_identity
|
|
875
1003
|
@effect_collections = effect_collections_reusable?(payload) ? payload.effect_collections.dup : {}
|
|
1004
|
+
@effect_plugin_digest = payload.effect_plugin_digest
|
|
876
1005
|
@symbol_dependents = Incremental.invert_symbols(@symbol_sources)
|
|
877
1006
|
@ancestry_dependents = Incremental.invert(@ancestry_sources)
|
|
878
1007
|
@negative_dependents = Incremental.invert(@missing)
|
|
@@ -901,7 +1030,10 @@ module Rigor
|
|
|
901
1030
|
param_table: marshal_safe_param_table,
|
|
902
1031
|
effect_collections: marshal_safe_effect_collections,
|
|
903
1032
|
effects_identity: @effects_identity,
|
|
904
|
-
run_level_rows: @run_level_rows
|
|
1033
|
+
run_level_rows: @run_level_rows,
|
|
1034
|
+
effect_plugin_digest: @effect_plugin_digest,
|
|
1035
|
+
plugin_reads: @plugin_reads,
|
|
1036
|
+
discovery_digests: @discovery_digests
|
|
905
1037
|
)
|
|
906
1038
|
end
|
|
907
1039
|
|
|
@@ -913,6 +1045,7 @@ module Rigor
|
|
|
913
1045
|
def reset_effect_state
|
|
914
1046
|
@effect_collections = {}
|
|
915
1047
|
@effects_identity = nil
|
|
1048
|
+
@effect_plugin_digest = nil
|
|
916
1049
|
end
|
|
917
1050
|
|
|
918
1051
|
# The state this session carries ACROSS processes in the snapshot rather than deriving each run: the
|
|
@@ -1003,6 +1136,7 @@ module Rigor
|
|
|
1003
1136
|
# multiple edits: the analyzed set gets fresh diagnostics + digests + dependency edges, removed files
|
|
1004
1137
|
# are evicted from every map, and the analyzed-file list advances to `current`.
|
|
1005
1138
|
def absorb(runner, current, analyze_set, removed)
|
|
1139
|
+
@last_runner = runner # ADR-88 WD1 — the post-hoc fact-surface fingerprint reads this prepared registry.
|
|
1006
1140
|
removed.each { |path| forget(path) }
|
|
1007
1141
|
@analyzed = current
|
|
1008
1142
|
# ADR-85 WD2 — the recheck's discovery folded the restored bundles and refreshed them (changed files
|
|
@@ -1011,9 +1145,12 @@ module Rigor
|
|
|
1011
1145
|
whole_project = current.all? { |path| analyze_set.include?(path) }
|
|
1012
1146
|
@seed_bundles = source_rbs_gate.stamp(runner.seed_bundles, whole_project: whole_project)
|
|
1013
1147
|
fresh_by_file = per_file(runner.per_file_diagnostics)
|
|
1148
|
+
# Issue #1553 — a re-analysed file's reads are this run's: a read it no longer makes stops guarding it.
|
|
1149
|
+
fresh_reads = plugin_reads_of(runner, analyze_set)
|
|
1014
1150
|
analyze_set.each do |path|
|
|
1015
1151
|
@cache[path] = fresh_by_file[path] || []
|
|
1016
1152
|
@digests[path] = pack_digest(path)
|
|
1153
|
+
fresh_reads.key?(path) ? @plugin_reads[path] = fresh_reads[path] : @plugin_reads.delete(path)
|
|
1017
1154
|
end
|
|
1018
1155
|
forget_unbound_digests
|
|
1019
1156
|
absorb_dependency_graph(runner)
|
|
@@ -1036,6 +1173,7 @@ module Rigor
|
|
|
1036
1173
|
@effect_collections.reject! { |path, _| analyzed.include?(path) }
|
|
1037
1174
|
@effect_collections.merge!(runner.effect_collections_by_path)
|
|
1038
1175
|
@effects_identity = current_effects_identity
|
|
1176
|
+
@effect_plugin_digest = effect_plugin_digest_of(runner)
|
|
1039
1177
|
end
|
|
1040
1178
|
|
|
1041
1179
|
# ADR-89 WD2 — replace the behavioural summaries of every re-analyzed file with THIS run's harvest: drop
|
|
@@ -1061,6 +1199,7 @@ module Rigor
|
|
|
1061
1199
|
@cache.delete(path)
|
|
1062
1200
|
@digests.delete(path)
|
|
1063
1201
|
@sources.delete(path)
|
|
1202
|
+
@plugin_reads.delete(path)
|
|
1064
1203
|
@symbol_sources.delete(path)
|
|
1065
1204
|
@ancestry_sources.delete(path)
|
|
1066
1205
|
@missing.delete(path)
|
|
@@ -1131,7 +1270,8 @@ module Rigor
|
|
|
1131
1270
|
# appeared `"ClassName#method"` to the negative-dependency key it would satisfy (`toplevel:foo` for a
|
|
1132
1271
|
# top-level def, `method:C#m` otherwise), then unions the recorded negative-dependents of those keys.
|
|
1133
1272
|
# Issue #1120 — plus the consumers of any refinement name the edit moved ({#refinement_affected}). Issue #1367 —
|
|
1134
|
-
# and every file whose last result holds a `global.*` diagnostic ({#global_write_reporters}).
|
|
1273
|
+
# and every file whose last result holds a `global.*` diagnostic ({#global_write_reporters}). Issue #1459 — and
|
|
1274
|
+
# the consumers of any method name a file started or stopped defining ({#defined_name_affected}).
|
|
1135
1275
|
def negative_affected(changed, removed, new_fingerprints, new_class_decls, new_constant_decls, summary = nil)
|
|
1136
1276
|
appeared_methods = Incremental.appeared_symbols(changed, @symbol_fingerprints, new_fingerprints)
|
|
1137
1277
|
# Issue #639 — a class DISAPPEARING satisfies the `class:` kind too, now that a resolved bare
|
|
@@ -1149,9 +1289,55 @@ module Rigor
|
|
|
1149
1289
|
keys.concat(class_move_keys(moved_classes))
|
|
1150
1290
|
keys.concat(moved_constants.map { |name| "constant:#{name.split('::').last}" })
|
|
1151
1291
|
Incremental.negative_closure(keys, @negative_dependents) | refinement_affected(changed + removed, summary) |
|
|
1292
|
+
defined_name_affected(changed + removed, summary) | mark_affected(changed + removed, summary) |
|
|
1152
1293
|
global_write_reporters
|
|
1153
1294
|
end
|
|
1154
1295
|
|
|
1296
|
+
# Issue #1833 — the consumers whose answer read a class mark that moved in this edit. A reader of a class's
|
|
1297
|
+
# envelope bucket records `mark:<last segment>` (`Scope#parameter_envelopes_of`), whichever way it answered, so
|
|
1298
|
+
# a file that adds or drops only a mark (`x.singleton_class.prepend(Comparable)`, `o.extend(Helper)` in a
|
|
1299
|
+
# method body, a `refine` block's refined mark) re-checks it although the file declares nothing it read. A
|
|
1300
|
+
# reader that scans every bucket for an object-extended module (`SourceArity#object_extension_may_shadow?`)
|
|
1301
|
+
# records `mark:*`, which fires when an object-extended mark moves. The before-state is each touched file's
|
|
1302
|
+
# seed bundle, the after-state the scan's merged table.
|
|
1303
|
+
#
|
|
1304
|
+
# Issue #1661 — and `mixin:<qualified name>` for a class or module whose unpositioned mixin edges moved
|
|
1305
|
+
# ({Incremental.changed_unpositioned_owners}): an external chain entry records it for each name it can denote
|
|
1306
|
+
# (`ResolutionChain#record_hooks`), so a new file that taints `Comparable` with `include(*MODS)` re-checks the
|
|
1307
|
+
# readers of a chain through it. It is not `mark:`, which a plain reopening of a module moves (the bucket's
|
|
1308
|
+
# module mark) and which would re-check every reader of `Kernel` on any `module Kernel` reopening.
|
|
1309
|
+
def mark_affected(paths, summary)
|
|
1310
|
+
def_index = summary && summary[:def_index]
|
|
1311
|
+
before = paths.map { |path| @seed_bundles.dig(path, :parameter_envelopes) }
|
|
1312
|
+
moved = Incremental.changed_class_marks(before, [def_index && def_index[:parameter_envelopes]])
|
|
1313
|
+
tainted = Incremental.changed_unpositioned_owners(
|
|
1314
|
+
paths.map { |path| @seed_bundles.dig(path, :unpositioned_mixins) },
|
|
1315
|
+
[def_index && def_index[:unpositioned_mixins]]
|
|
1316
|
+
)
|
|
1317
|
+
return Set.new if moved.empty? && tainted.empty?
|
|
1318
|
+
|
|
1319
|
+
keys = moved.map { |(class_name, _mark)| "mark:#{class_name.split('::').last}" }.uniq
|
|
1320
|
+
keys.concat(mixin_keys(tainted))
|
|
1321
|
+
extended = Scope::DiscoveryIndex::ENVELOPE_OBJECT_EXTENDED_MARK
|
|
1322
|
+
keys << "mark:*" if moved.any? { |(_class_name, mark)| mark == extended }
|
|
1323
|
+
Incremental.negative_closure(keys, @negative_dependents)
|
|
1324
|
+
end
|
|
1325
|
+
|
|
1326
|
+
# Issue #1661 — the `mixin:` keys an owner of a moved unpositioned edge fires. The table keys a receiver-form
|
|
1327
|
+
# owner by the spelling written (`class File; Constants.include(*MODS); end` is `"Constants"`), not the constant
|
|
1328
|
+
# Ruby resolves it to, while the chain files the qualified candidate (`mixin:File::Constants`). The table does
|
|
1329
|
+
# not carry the nesting, and every lexical-prefix candidate of the owner ends in `::<owner>`, so the owner fires
|
|
1330
|
+
# its own key and every recorded `mixin:` key ending in `::<owner>`. A relative spelling over-fires to the
|
|
1331
|
+
# readers of every module sharing its trailing segments; a qualified one fires only its suffix matches.
|
|
1332
|
+
def mixin_keys(owners)
|
|
1333
|
+
keys = owners.map { |name| "mixin:#{name}" }
|
|
1334
|
+
suffixes = owners.map { |name| "::#{name}" }
|
|
1335
|
+
@negative_dependents.each_key do |key|
|
|
1336
|
+
keys << key if key.start_with?("mixin:") && suffixes.any? { |suffix| key.end_with?(suffix) }
|
|
1337
|
+
end
|
|
1338
|
+
keys.uniq
|
|
1339
|
+
end
|
|
1340
|
+
|
|
1155
1341
|
# The negative keys a class-declaration diff fires: `class:<last segment>` for a class name, and (issue #1820)
|
|
1156
1342
|
# `subclass_of:<last segment of the superclass>` for a `[class, superclass]` pair appearing or vanishing. Any
|
|
1157
1343
|
# class declaration moving may move the set of classes whose superclass the tables do not know, which
|
|
@@ -1177,6 +1363,22 @@ module Rigor
|
|
|
1177
1363
|
Incremental.negative_closure(names.map { |name| "refinement:#{name}" }, @negative_dependents)
|
|
1178
1364
|
end
|
|
1179
1365
|
|
|
1366
|
+
# Issue #1459 — the consumers whose answer asked whether the program defines a method name anywhere, for each
|
|
1367
|
+
# name a file in `paths` started or stopped defining. The before-state is each file's seed bundle, the
|
|
1368
|
+
# after-state the scan's per-file name sets. Unlike the appeared-symbol diff, it sees a name an `attr_*`,
|
|
1369
|
+
# `alias` or `define_method` adds, and one that vanishes, which brings a declined reading back.
|
|
1370
|
+
def defined_name_affected(paths, summary)
|
|
1371
|
+
after = (summary && summary[:defined_method_names]) || {}
|
|
1372
|
+
before = paths.to_h do |path|
|
|
1373
|
+
bundle = @seed_bundles[path]
|
|
1374
|
+
[path, bundle && Inference::ScopeIndexer.defined_method_names(bundle)]
|
|
1375
|
+
end
|
|
1376
|
+
names = Incremental.changed_defined_names(paths, before, after)
|
|
1377
|
+
return Set.new if names.empty?
|
|
1378
|
+
|
|
1379
|
+
Incremental.negative_closure(names.map { |name| "defines:#{name}" }, @negative_dependents)
|
|
1380
|
+
end
|
|
1381
|
+
|
|
1180
1382
|
# The qualified class/module names declared in the pre-parsed `index` (shared with
|
|
1181
1383
|
# {#symbol_fingerprints_from_index}, so the changed set is parsed once). `{ path => Set<class name> }`.
|
|
1182
1384
|
def class_declarations_from_index(index)
|
|
@@ -1335,6 +1537,46 @@ module Rigor
|
|
|
1335
1537
|
[forced, (affected | forced) & current]
|
|
1336
1538
|
end
|
|
1337
1539
|
|
|
1540
|
+
# What the session serves a narrowed run for the files it does not analyse: their cached per-file rows (#1524),
|
|
1541
|
+
# and (#1898) their effect collections, so the run closes its effect graph over the whole project.
|
|
1542
|
+
def served_state(reused)
|
|
1543
|
+
{ served_per_file_diagnostics: @cache.slice(*reused),
|
|
1544
|
+
served_effect_collections: @effect_collections.slice(*reused) }
|
|
1545
|
+
end
|
|
1546
|
+
|
|
1547
|
+
# Issue #1898 — the plugin effect-fact digest a run's collections were stamped under. A plugin writes its
|
|
1548
|
+
# attributions and edges into a file's collection when the file is collected, and what it contributes can
|
|
1549
|
+
# follow project state no file digest sees (rigor-activejob reads `config.active_job.queue_adapter`), so a
|
|
1550
|
+
# served collection is current only while this digest is. nil when collection is off.
|
|
1551
|
+
def effect_plugin_digest_of(runner)
|
|
1552
|
+
@configuration.effects_enabled? ? runner.effect_plugin_facts.digest : nil
|
|
1553
|
+
end
|
|
1554
|
+
|
|
1555
|
+
# Whether `runner` judged envelopes over served collections stamped under a different plugin effect-fact
|
|
1556
|
+
# digest, or an unknown one. The comparison waits for the run because the plugins load inside it.
|
|
1557
|
+
def served_effects_stale?(runner, reused)
|
|
1558
|
+
return false if reused.empty? || @effect_collections.empty?
|
|
1559
|
+
|
|
1560
|
+
effect_plugin_digest_of(runner) != @effect_plugin_digest
|
|
1561
|
+
end
|
|
1562
|
+
|
|
1563
|
+
# The answer {#recheck} gives when {#served_effects_stale?}: a full baseline, reported as a recheck that
|
|
1564
|
+
# re-analysed every file. {#run_incremental} reads `rebaselined` so it neither runs a second baseline
|
|
1565
|
+
# nor reports the run as warm.
|
|
1566
|
+
def rebaseline_recheck(changed, added, removed)
|
|
1567
|
+
diagnostics = baseline
|
|
1568
|
+
recheck_result(diagnostics, [changed, added, removed],
|
|
1569
|
+
affected: @analyzed.to_set, reused: Set.new, runner: @last_runner, rebaselined: true)
|
|
1570
|
+
end
|
|
1571
|
+
|
|
1572
|
+
# A {Recheck} over `deltas` (`[changed, added, removed]`), counting `runner`'s template units.
|
|
1573
|
+
def recheck_result(diagnostics, deltas, affected:, reused:, runner:, rebaselined: false, stale_readers: Set.new)
|
|
1574
|
+
changed, added, removed = deltas.map(&:to_set)
|
|
1575
|
+
Recheck.new(diagnostics: diagnostics, changed: changed, added: added, removed: removed, affected: affected,
|
|
1576
|
+
stale_readers: stale_readers,
|
|
1577
|
+
reused: reused, templates: runner.template_unit_paths.size, rebaselined: rebaselined)
|
|
1578
|
+
end
|
|
1579
|
+
|
|
1338
1580
|
# A recheck that forces targets analyses in this process, so the index callback reaches them.
|
|
1339
1581
|
def forced_target_options(forced)
|
|
1340
1582
|
forced.empty? ? {} : { workers: 0 }
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../rooted_glob"
|
|
4
|
+
|
|
3
5
|
module Rigor
|
|
4
6
|
module Analysis
|
|
5
7
|
# ADR-87 WD4 — project file-set expansion shared by the {Analysis::Runner} (miss path) and the
|
|
@@ -27,7 +29,7 @@ module Rigor
|
|
|
27
29
|
end
|
|
28
30
|
|
|
29
31
|
def directory_files(dir, exclude_patterns, base: Dir.pwd)
|
|
30
|
-
reject_excluded(
|
|
32
|
+
reject_excluded(RootedGlob.glob(dir, RUBY_GLOB), exclude_patterns, base: base)
|
|
31
33
|
end
|
|
32
34
|
|
|
33
35
|
# `exclude_patterns` are matched with `File.fnmatch?` WITHOUT `FNM_PATHNAME`, so `**`/`*` span separators
|