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
|
@@ -46,7 +46,8 @@ module Rigor
|
|
|
46
46
|
synthetic_method_index:, project_patched_methods:,
|
|
47
47
|
analyze_file:, project_scope_seed: -> { {} }, record_dependencies: false,
|
|
48
48
|
restored_run_level_rows: nil, template_units: -> { TemplateUnits.empty },
|
|
49
|
-
feature_sources: -> { {} }, plugin_prepare: -> { [[], []] }
|
|
49
|
+
feature_sources: -> { {} }, plugin_prepare: -> { [[], [], []] },
|
|
50
|
+
prepare_coordinator_plugins: -> { [] })
|
|
50
51
|
@configuration = configuration
|
|
51
52
|
@cache_store = cache_store
|
|
52
53
|
@explain = explain
|
|
@@ -66,6 +67,12 @@ module Rigor
|
|
|
66
67
|
# The de-duplication has to happen here, on the parent, because each fork worker owns its own
|
|
67
68
|
# plugin instances and therefore its own `@run_disclosures` table.
|
|
68
69
|
@collected_run_disclosures = {}
|
|
70
|
+
# Issue #1900 — the trust-policy refusals ({Analysis::WorkerSession#drain_trust_refusals}) a pool run
|
|
71
|
+
# collected from the pre-fork parent session and every worker, keyed by plugin id and summed. The
|
|
72
|
+
# coordinator-side registry, which {DiagnosticAggregator#plugin_trust_refusal_diagnostics} also
|
|
73
|
+
# reads, runs neither `#prepare` nor per-file analysis on a pooled run, so without this table a
|
|
74
|
+
# refusal recorded in `--workers 0` vanished in `--workers N`.
|
|
75
|
+
@collected_trust_refusals = {}
|
|
69
76
|
@collect_stats = collect_stats
|
|
70
77
|
@buffer = buffer
|
|
71
78
|
@feature_sources = feature_sources
|
|
@@ -84,10 +91,15 @@ module Rigor
|
|
|
84
91
|
# inherits the same compiled bytes.
|
|
85
92
|
@template_units_reader = template_units
|
|
86
93
|
@analyze_file = analyze_file
|
|
87
|
-
# Issue #1541 — `() -> [rows, disclosure records]`, a `#prepare` run on a throwaway
|
|
88
|
-
# only by {#serve_empty_closure}: a pooled run's `#prepare` happens inside its
|
|
89
|
-
# empty closure dispatches none.
|
|
94
|
+
# Issue #1541 — `() -> [rows, disclosure records, trust refusals]`, a `#prepare` run on a throwaway
|
|
95
|
+
# registry. Read only by {#serve_empty_closure}: a pooled run's `#prepare` happens inside its
|
|
96
|
+
# workers, and an empty closure dispatches none.
|
|
90
97
|
@plugin_prepare = plugin_prepare
|
|
98
|
+
# Issue #1900 — `() -> rows`, `#prepare` run on the coordinator-side registry itself. Read only by
|
|
99
|
+
# {#analyze_files_sequentially_fallback}, which analyses on that registry: the pre-pass skips
|
|
100
|
+
# `#prepare` whenever `pool_mode?`, so without this the fallback ran every plugin unprepared.
|
|
101
|
+
@prepare_coordinator_plugins = prepare_coordinator_plugins
|
|
102
|
+
@coordinator_plugins_prepared = false
|
|
91
103
|
# Issues #796 / #794 — the previous full run's run-level rows, handed down by
|
|
92
104
|
# {Analysis::IncrementalSession} on a NARROWED run only (a recheck's closure, a
|
|
93
105
|
# `--verify-incremental` partition). A full run leaves this nil and derives everything itself.
|
|
@@ -99,6 +111,13 @@ module Rigor
|
|
|
99
111
|
# so the incremental session's dependency graph is refreshed whether analysis ran sequential or pooled.
|
|
100
112
|
attr_reader :collected_dependencies
|
|
101
113
|
|
|
114
|
+
# Issue #1553 — the per-file plugin reads the fork pool's workers recorded this run (empty unless
|
|
115
|
+
# `record_dependencies:` and a pool run occurred), folded into `Runner#analysis_reads`. Each worker's are
|
|
116
|
+
# marshalled back beside its dependency records.
|
|
117
|
+
def collected_analysis_reads
|
|
118
|
+
@collected_analysis_reads ||= {}
|
|
119
|
+
end
|
|
120
|
+
|
|
102
121
|
# ADR-103 WD12 — the per-file {Effects::FileCollection}s the fork pool captured this run (empty
|
|
103
122
|
# unless the configuration carries an `effects:` block and a pool run occurred). The runner merges
|
|
104
123
|
# these with its own sequential records before the post-pool fixpoint.
|
|
@@ -123,6 +142,27 @@ module Rigor
|
|
|
123
142
|
end
|
|
124
143
|
end
|
|
125
144
|
|
|
145
|
+
# Issue #1900 — the trust refusals a pool run collected, one `{plugin_id:, count:, first_path:,
|
|
146
|
+
# nearest_root:}` per plugin, in first-merge order. Empty on a sequential run, where the runner's own
|
|
147
|
+
# registry made every read.
|
|
148
|
+
def collected_trust_refusals
|
|
149
|
+
@collected_trust_refusals.values
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Issue #1900 — folds one session's / one worker payload's refusal drain in: counts add, and the
|
|
153
|
+
# first path stays the one merged first. Callers merge in the order `--workers 0` would have made the
|
|
154
|
+
# reads — the parent's (or one worker's) `#prepare`, then the files in analysis order — so the summed
|
|
155
|
+
# row names the same first path the sequential run does, except where a drain's own first path is
|
|
156
|
+
# not this merge's to choose (see {DiagnosticAggregator#plugin_trust_refusal_diagnostics}).
|
|
157
|
+
def merge_trust_refusals(records)
|
|
158
|
+
Array(records).each do |record|
|
|
159
|
+
plugin_id = record[:plugin_id]
|
|
160
|
+
existing = @collected_trust_refusals[plugin_id]
|
|
161
|
+
@collected_trust_refusals[plugin_id] =
|
|
162
|
+
existing ? existing.merge(count: existing[:count] + record[:count]) : record
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
126
166
|
# ADR-15 Phase 4b — pool mode is enabled when `@workers > 0`. Editor mode (`buffer:` non-nil)
|
|
127
167
|
# silently overrides pool mode to sequential: per design § "Ractor pool mode", the pool's warm-up
|
|
128
168
|
# cost dominates one-file wall time, so the pool gains nothing on a per-buffer invocation. The
|
|
@@ -164,13 +204,17 @@ module Rigor
|
|
|
164
204
|
if pool_mode?
|
|
165
205
|
dispatch_pool(files, source_files: source_files)
|
|
166
206
|
else
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
)
|
|
207
|
+
environment ||= resolve_sequential_environment(source_files: source_files)
|
|
208
|
+
analyze_files_sequentially(files, environment)
|
|
170
209
|
end
|
|
171
210
|
# Issues #796 / #794 — a narrowed run's own demand set is a SUBSET of the full run's, so the two
|
|
172
|
-
# rows below are folded in after it, whichever backend produced the per-file stream.
|
|
173
|
-
|
|
211
|
+
# rows below are folded in after it, whichever backend produced the per-file stream. Issue #1929 —
|
|
212
|
+
# the sequential run's environment re-derives a stale replayed failure; a pool run has none on
|
|
213
|
+
# this side and resolves one only when a stale failure needs it.
|
|
214
|
+
redemand_env = lambda do
|
|
215
|
+
environment ||= resolve_sequential_environment(source_files: source_files)
|
|
216
|
+
end
|
|
217
|
+
replayed_restored_run_level_rows?(closure: files, redemand_env: redemand_env)
|
|
174
218
|
result
|
|
175
219
|
end
|
|
176
220
|
|
|
@@ -228,15 +272,18 @@ module Rigor
|
|
|
228
272
|
# `env` rather than `resolve.call`: read the demand the snapshots above just made when they built
|
|
229
273
|
# an environment, and force nothing when they did not.
|
|
230
274
|
record_definition_build_failures(env&.rbs_loader&.definition_build_failures)
|
|
231
|
-
|
|
275
|
+
# An empty closure analyses no file, so only a deleted file's buffer can make a tuple stale here.
|
|
276
|
+
replayed = replayed_restored_run_level_rows?(closure: [], redemand_env: resolve)
|
|
277
|
+
record_hkt_scan_failure(hkt_scan_outcome(resolve.call)) unless replayed
|
|
232
278
|
prepare_rows
|
|
233
279
|
end
|
|
234
280
|
|
|
235
281
|
# Issue #1541 — the pooled empty closure's `#prepare`: the rows go to the caller, the run-scoped
|
|
236
282
|
# registrations into the table the workers' payloads would have filled.
|
|
237
283
|
def pooled_prepare_rows
|
|
238
|
-
rows, records = @plugin_prepare.call
|
|
284
|
+
rows, records, refusals = @plugin_prepare.call
|
|
239
285
|
merge_run_disclosures(records)
|
|
286
|
+
merge_trust_refusals(refusals)
|
|
240
287
|
Array(rows)
|
|
241
288
|
end
|
|
242
289
|
|
|
@@ -285,18 +332,46 @@ module Rigor
|
|
|
285
332
|
# of its own — and so no environment.
|
|
286
333
|
#
|
|
287
334
|
# @param closure — the files this run analyses; see {#stale_replayed_failure?}.
|
|
288
|
-
|
|
335
|
+
# @param redemand_env — resolves the environment {#redemand_stale_failures} re-derives a stale
|
|
336
|
+
# failure in; nil (the empty closure, which has no stale failure) re-derives nothing.
|
|
337
|
+
def replayed_restored_run_level_rows?(closure: nil, redemand_env: nil)
|
|
289
338
|
rows = @restored_run_level_rows
|
|
290
339
|
return false if rows.nil?
|
|
291
340
|
|
|
292
|
-
replayable = Array(rows.definition_build_failures).
|
|
341
|
+
stale, replayable = Array(rows.definition_build_failures).partition do |failure|
|
|
293
342
|
stale_replayed_failure?(failure, closure)
|
|
294
343
|
end
|
|
295
344
|
record_definition_build_failures(replayable)
|
|
345
|
+
redemand_stale_failures(stale, redemand_env)
|
|
296
346
|
record_hkt_scan_failure(rows.hkt_scan_failure)
|
|
297
347
|
true
|
|
298
348
|
end
|
|
299
349
|
|
|
350
|
+
# Issue #1929 — a stale replayed failure is RE-DERIVED, not dropped. Dropping it was right for the
|
|
351
|
+
# class whose collision the edit removed, and wrong for every class the edit left failing: the file
|
|
352
|
+
# that demanded such a class usually sits outside the recheck's closure (the edited file only
|
|
353
|
+
# contributes a conflicting buffer, such as a missing superclass every subclass inherits), so
|
|
354
|
+
# nothing in the recheck asks for it again, and the row lost classes a cold run reports. The class
|
|
355
|
+
# is asked for again in this run's environment ({Environment::RbsLoader#redemand_definition}), so
|
|
356
|
+
# it is reported exactly when it still fails, with the tuple the current signatures give it.
|
|
357
|
+
#
|
|
358
|
+
# A class this run's own analysis already reported is skipped; its tuple is current. The residue:
|
|
359
|
+
# a class whose only demand was in an edited file that no longer makes it, and which still fails,
|
|
360
|
+
# stays reported until a full run. The failure itself is real, unlike the dropped-collision case
|
|
361
|
+
# this replaced.
|
|
362
|
+
def redemand_stale_failures(stale, redemand_env)
|
|
363
|
+
return if stale.empty? || redemand_env.nil?
|
|
364
|
+
|
|
365
|
+
reported = @snapshots.definition_build_failures.to_set(&:first)
|
|
366
|
+
pending = stale.map(&:first).reject { |name| reported.include?(name) }
|
|
367
|
+
return if pending.empty?
|
|
368
|
+
|
|
369
|
+
loader = redemand_env.call&.rbs_loader
|
|
370
|
+
return if loader.nil?
|
|
371
|
+
|
|
372
|
+
record_definition_build_failures(pending.filter_map { |name| loader.redemand_definition(name) })
|
|
373
|
+
end
|
|
374
|
+
|
|
300
375
|
# A replayed definition-build failure the snapshot's own fingerprint cannot vouch for. The
|
|
301
376
|
# fingerprint digests the three signature sources it can read WITHOUT an environment — `sig:`,
|
|
302
377
|
# `Gemfile.lock` / `rbs_collection.lock.yaml`, and the configuration — but NOT the virtual RBS a
|
|
@@ -306,11 +381,11 @@ module Rigor
|
|
|
306
381
|
# closure, its analysis demands nothing that would re-derive the row, and the run reports a
|
|
307
382
|
# collision between files that no longer collide.
|
|
308
383
|
#
|
|
309
|
-
#
|
|
310
|
-
#
|
|
311
|
-
#
|
|
312
|
-
#
|
|
313
|
-
#
|
|
384
|
+
# Such a failure is not replayed as it stands; {#redemand_stale_failures} re-derives it instead
|
|
385
|
+
# (#1929), so the row asserts no collision the current signatures do not have. A failure whose
|
|
386
|
+
# buffers name no file this run touches still replays. A buffer whose file no longer exists is stale
|
|
387
|
+
# too: deleting an inline source a tuple names would otherwise replay the deleted declaration's
|
|
388
|
+
# failure, where a full run reports what the remaining signatures give.
|
|
314
389
|
def stale_replayed_failure?(failure, closure)
|
|
315
390
|
return false if closure.nil?
|
|
316
391
|
|
|
@@ -320,7 +395,10 @@ module Rigor
|
|
|
320
395
|
closure_paths = closure.to_set { |path| File.absolute_path(path.to_s) }
|
|
321
396
|
Array(buffers).any? do |name|
|
|
322
397
|
path = virtual_buffer_path(name)
|
|
323
|
-
|
|
398
|
+
next false if path.nil?
|
|
399
|
+
|
|
400
|
+
absolute = File.absolute_path(path)
|
|
401
|
+
closure_paths.include?(absolute) || !File.exist?(absolute)
|
|
324
402
|
end
|
|
325
403
|
end
|
|
326
404
|
|
|
@@ -639,13 +717,15 @@ module Rigor
|
|
|
639
717
|
required_features: features
|
|
640
718
|
)
|
|
641
719
|
main = Ractor.main
|
|
642
|
-
|
|
720
|
+
# Issue #1900 — each message carries the refusals made since the previous one, so the coordinator
|
|
721
|
+
# can count ONE worker's `#prepare` (every worker repeats it) and sum the files in file order.
|
|
722
|
+
main.send([:prepare, session.prepare_diagnostics, session.drain_trust_refusals])
|
|
643
723
|
|
|
644
724
|
loop do
|
|
645
725
|
msg = Ractor.receive
|
|
646
726
|
break if msg.nil?
|
|
647
727
|
|
|
648
|
-
main.send([:file, msg, session.analyze(msg)])
|
|
728
|
+
main.send([:file, msg, session.analyze(msg), session.drain_trust_refusals])
|
|
649
729
|
end
|
|
650
730
|
|
|
651
731
|
# Issue #1051 — sent with `:done`, not with `:prepare`: this backend builds no parent session,
|
|
@@ -664,6 +744,8 @@ module Rigor
|
|
|
664
744
|
pool.each { |worker| worker.send(nil) }
|
|
665
745
|
|
|
666
746
|
prepare_diagnostics = nil
|
|
747
|
+
prepare_refusals = nil
|
|
748
|
+
refusals_by_path = {}
|
|
667
749
|
results_by_path = {}
|
|
668
750
|
terminated = 0
|
|
669
751
|
|
|
@@ -681,9 +763,11 @@ module Rigor
|
|
|
681
763
|
|
|
682
764
|
case message.first
|
|
683
765
|
when :prepare
|
|
684
|
-
prepare_diagnostics ||= message
|
|
766
|
+
prepare_diagnostics ||= message[1]
|
|
767
|
+
prepare_refusals ||= message[2]
|
|
685
768
|
when :file
|
|
686
769
|
results_by_path[message[1]] = message[2]
|
|
770
|
+
refusals_by_path[message[1]] = message[3]
|
|
687
771
|
when :done
|
|
688
772
|
merge_worker_reporters(message[1])
|
|
689
773
|
merge_run_disclosures(message[2])
|
|
@@ -702,6 +786,8 @@ module Rigor
|
|
|
702
786
|
# it did. Re-analysed in process, exactly as the fork backend re-analyses a dead child's slice.
|
|
703
787
|
degraded = files.reject { |path| results_by_path.key?(path) }
|
|
704
788
|
reanalyze_degraded_in_process(degraded, results_by_path, source_files: source_files)
|
|
789
|
+
merge_trust_refusals(prepare_refusals)
|
|
790
|
+
files.each { |path| merge_trust_refusals(refusals_by_path[path]) }
|
|
705
791
|
|
|
706
792
|
diagnostics = Array(prepare_diagnostics) + files.flat_map { |path| results_by_path.fetch(path, []) }
|
|
707
793
|
degraded.empty? ? diagnostics : diagnostics.unshift(pool_degraded_diagnostic(degraded.size, "ractor"))
|
|
@@ -795,37 +881,62 @@ module Rigor
|
|
|
795
881
|
# `merge_run_disclosures` drops it as a duplicate; only a disclosure a worker registers while
|
|
796
882
|
# analysing its own slice adds anything.
|
|
797
883
|
merge_run_disclosures(session.drain_run_disclosures)
|
|
884
|
+
# Issue #1900 — the parent's `#prepare` refusals, drained BEFORE the fork: every child inherits the
|
|
885
|
+
# session's record of what it already reported and drains only its own slice's, so the prepare-time
|
|
886
|
+
# refusal is counted once, as `--workers 0` counts it, rather than once per worker.
|
|
887
|
+
merge_trust_refusals(session.drain_trust_refusals)
|
|
798
888
|
snapshot_fork_pool_stats(session) if @collect_stats
|
|
799
889
|
|
|
800
890
|
worker_count = [@workers, files.size].min
|
|
801
891
|
slices = files.each_slice((files.size.to_f / worker_count).ceil).to_a
|
|
802
892
|
results_by_path = {}
|
|
893
|
+
# Issue #1900 — each surviving child's refusal drain by slice index, and each re-analysed file's by
|
|
894
|
+
# path, merged below in file order so the first refused path is the one `--workers 0` names.
|
|
895
|
+
refusals_by_slice = {}
|
|
896
|
+
refusals_by_path = {}
|
|
803
897
|
|
|
804
898
|
degraded = Dir.mktmpdir("rigor-fork-pool") do |tmpdir|
|
|
805
899
|
children = slices.each_with_index.map do |slice, index|
|
|
806
900
|
out_path = File.join(tmpdir, "worker-#{index}")
|
|
807
901
|
{ pid: fork { run_fork_worker(session, slice, out_path) },
|
|
808
|
-
slice: slice, out_path: out_path }
|
|
902
|
+
slice: slice, out_path: out_path, index: index }
|
|
809
903
|
end
|
|
810
|
-
collect_fork_results(children, results_by_path)
|
|
904
|
+
collect_fork_results(children, results_by_path, refusals_by_slice: refusals_by_slice)
|
|
811
905
|
end
|
|
812
906
|
|
|
813
907
|
unless degraded.empty?
|
|
814
|
-
degraded.each
|
|
908
|
+
degraded.each do |path|
|
|
909
|
+
results_by_path[path] = session.analyze(path)
|
|
910
|
+
refusals_by_path[path] = session.drain_trust_refusals
|
|
911
|
+
end
|
|
815
912
|
merge_worker_reporters(session.drain_reporters)
|
|
816
913
|
# The degraded slice ran on the parent session, so a disclosure it registered lives there.
|
|
817
914
|
merge_run_disclosures(session.drain_run_disclosures)
|
|
818
915
|
# The degraded slice was re-analysed on the parent session, so its dependency records live there,
|
|
819
916
|
# not in a child payload — fold them in alongside the successful children's.
|
|
820
|
-
|
|
917
|
+
collect_dependencies(session.drain_dependencies, session.drain_analysis_reads) if @record_dependencies
|
|
821
918
|
@collected_effects.merge!(session.drain_effects) if @record_effects
|
|
822
919
|
end
|
|
823
920
|
|
|
921
|
+
merge_slice_refusals(slices, refusals_by_slice, refusals_by_path)
|
|
824
922
|
diagnostics = Array(session.prepare_diagnostics) +
|
|
825
923
|
files.flat_map { |path| results_by_path.fetch(path, []) }
|
|
826
924
|
degraded.empty? ? diagnostics : diagnostics.unshift(pool_degraded_diagnostic(degraded.size, "fork"))
|
|
827
925
|
end
|
|
828
926
|
|
|
927
|
+
# Issue #1900 — a surviving child's drain covers its whole slice; a degraded slice was re-analysed on
|
|
928
|
+
# the parent and drained file by file. Slices are contiguous runs of `files`, so walking them in index
|
|
929
|
+
# order merges every refusal in the order the sequential run made it.
|
|
930
|
+
def merge_slice_refusals(slices, refusals_by_slice, refusals_by_path)
|
|
931
|
+
slices.each_with_index do |slice, index|
|
|
932
|
+
if refusals_by_slice.key?(index)
|
|
933
|
+
merge_trust_refusals(refusals_by_slice[index])
|
|
934
|
+
else
|
|
935
|
+
slice.each { |path| merge_trust_refusals(refusals_by_path[path]) }
|
|
936
|
+
end
|
|
937
|
+
end
|
|
938
|
+
end
|
|
939
|
+
|
|
829
940
|
# Child-process body for {#analyze_files_in_fork_pool}. Analyses the slice with the
|
|
830
941
|
# copy-on-write-inherited session and writes the Marshal'd payload to `out_path`. `exit!` skips
|
|
831
942
|
# `at_exit` / stdio flush — the payload is already durable on disk by then.
|
|
@@ -840,7 +951,9 @@ module Rigor
|
|
|
840
951
|
results = slice.to_h { |path| [path, session.analyze(path)] }
|
|
841
952
|
payload = { results: results, reporters: session.drain_reporters,
|
|
842
953
|
dependencies: session.drain_dependencies,
|
|
843
|
-
|
|
954
|
+
analysis_reads: session.drain_analysis_reads,
|
|
955
|
+
disclosures: session.drain_run_disclosures,
|
|
956
|
+
refusals: session.drain_trust_refusals }
|
|
844
957
|
# ADR-103 WD13 — the effects slot exists only on a collecting run, so a normal `--workers N` run
|
|
845
958
|
# marshals exactly the payload it marshalled before the feature existed.
|
|
846
959
|
payload[:effects] = session.drain_effects if @record_effects
|
|
@@ -864,7 +977,9 @@ module Rigor
|
|
|
864
977
|
|
|
865
978
|
# Waits for every forked child, merges each successful payload into `results_by_path`, and returns
|
|
866
979
|
# the file paths whose worker exited abnormally (for in-process degrade).
|
|
867
|
-
|
|
980
|
+
# @param refusals_by_slice — issue #1900 — receives each successful payload's trust-refusal drain
|
|
981
|
+
# under the child's slice index, for the caller to merge in file order; nil merges it at once.
|
|
982
|
+
def collect_fork_results(children, results_by_path, refusals_by_slice: nil)
|
|
868
983
|
degraded = []
|
|
869
984
|
children.each do |child|
|
|
870
985
|
_, status = Process.waitpid2(child[:pid])
|
|
@@ -873,7 +988,11 @@ module Rigor
|
|
|
873
988
|
results_by_path.merge!(payload.fetch(:results))
|
|
874
989
|
merge_worker_reporters(payload.fetch(:reporters))
|
|
875
990
|
merge_run_disclosures(payload.fetch(:disclosures, []))
|
|
876
|
-
|
|
991
|
+
refusals = payload.fetch(:refusals, [])
|
|
992
|
+
refusals_by_slice ? refusals_by_slice[child[:index]] = refusals : merge_trust_refusals(refusals)
|
|
993
|
+
if @record_dependencies
|
|
994
|
+
collect_dependencies(payload.fetch(:dependencies, {}), payload.fetch(:analysis_reads, {}))
|
|
995
|
+
end
|
|
877
996
|
@collected_effects.merge!(payload.fetch(:effects, {})) if @record_effects
|
|
878
997
|
else
|
|
879
998
|
degraded.concat(child[:slice])
|
|
@@ -882,6 +1001,12 @@ module Rigor
|
|
|
882
1001
|
degraded
|
|
883
1002
|
end
|
|
884
1003
|
|
|
1004
|
+
# ADR-46 / issue #1553 — fold one worker's dependency records and per-file plugin reads.
|
|
1005
|
+
def collect_dependencies(dependencies, analysis_reads)
|
|
1006
|
+
@collected_dependencies.merge!(dependencies)
|
|
1007
|
+
collected_analysis_reads.merge!(analysis_reads)
|
|
1008
|
+
end
|
|
1009
|
+
|
|
885
1010
|
# @return the child's `{results:, reporters:}` payload, or nil when the child exited
|
|
886
1011
|
# abnormally or wrote no readable payload. `Marshal.load` is safe here: the blob was written by
|
|
887
1012
|
# our own forked child to a temp file we created.
|
|
@@ -957,7 +1082,15 @@ module Rigor
|
|
|
957
1082
|
# @param source_files — issue #793 — the whole project, so this path's environment
|
|
958
1083
|
# carries the same plugin-synthesized RBS the pool workers' would. It used to build over `[]`,
|
|
959
1084
|
# i.e. with no synthesized RBS at all, even on a full run.
|
|
1085
|
+
#
|
|
1086
|
+
# Issue #1900 — it also runs `#prepare` on the coordinator-side registry it analyses with, BEFORE the
|
|
1087
|
+
# environment build as the sequential pre-pass does. A pooled run's pre-pass skips `#prepare` (the
|
|
1088
|
+
# workers run it), and this path has no workers, so every plugin used to analyse unprepared and its
|
|
1089
|
+
# prepare rows and prepare-time trust refusals were lost. The rows head the stream, where the pool
|
|
1090
|
+
# backends put their workers' prepare rows; the refusals land on the coordinator's own
|
|
1091
|
+
# {Plugin::IoBoundary}s, which {DiagnosticAggregator#plugin_trust_refusal_diagnostics} reads.
|
|
960
1092
|
def analyze_files_sequentially_fallback(files, reason:, source_files: files)
|
|
1093
|
+
prepare_rows = prepare_coordinator_plugins_once
|
|
961
1094
|
environment = build_runner_environment(source_files: source_files)
|
|
962
1095
|
# Same set, same ORDER as `#analyze_files_sequentially`. The signature state used to be hand-copied
|
|
963
1096
|
# here slot by slot, which dropped `synthesized-namespace` and every conformance row. The conformance
|
|
@@ -965,7 +1098,7 @@ module Rigor
|
|
|
965
1098
|
# BEFORE the definition-build failures are read below, or a failure only that scan reaches is lost.
|
|
966
1099
|
snapshot_project_signature_state(environment)
|
|
967
1100
|
snapshot_effect_annotation_carrier(environment.rbs_loader)
|
|
968
|
-
diagnostics = files.flat_map { |path| @analyze_file.call(path, environment) }
|
|
1101
|
+
diagnostics = prepare_rows + files.flat_map { |path| @analyze_file.call(path, environment) }
|
|
969
1102
|
loader = environment.rbs_loader
|
|
970
1103
|
# Issue #696 — this path analyses on the coordinator, so its loader IS the one that reached the
|
|
971
1104
|
# failing build, and it must snapshot the same slot the two real paths do. Reached whenever
|
|
@@ -987,6 +1120,15 @@ module Rigor
|
|
|
987
1120
|
)
|
|
988
1121
|
end
|
|
989
1122
|
|
|
1123
|
+
# Issue #1900 — `#prepare` on the coordinator-side registry, at most once per run: a second call would
|
|
1124
|
+
# re-publish every cross-plugin fact into the shared fact store.
|
|
1125
|
+
def prepare_coordinator_plugins_once
|
|
1126
|
+
return [] if @coordinator_plugins_prepared
|
|
1127
|
+
|
|
1128
|
+
@coordinator_plugins_prepared = true
|
|
1129
|
+
Array(@prepare_coordinator_plugins.call)
|
|
1130
|
+
end
|
|
1131
|
+
|
|
990
1132
|
def merge_worker_reporters(drained)
|
|
991
1133
|
merge_rbs_extended_reporter(drained.fetch(:rbs_extended))
|
|
992
1134
|
drained.fetch(:boundary_cross).each do |entry|
|
|
@@ -1081,8 +1223,14 @@ module Rigor
|
|
|
1081
1223
|
def record_definition_build_failures(failures)
|
|
1082
1224
|
return if failures.nil? || failures.empty?
|
|
1083
1225
|
|
|
1226
|
+
# Issue #1902 — one tuple per class, chosen by {Environment::RbsLoader.definition_build_detail_order}
|
|
1227
|
+
# rather than by arrival: a fresh record and a replayed one (or two workers' records) for the same
|
|
1228
|
+
# class can name different failures, and the earlier one used to win.
|
|
1084
1229
|
@snapshots.definition_build_failures =
|
|
1085
|
-
(@snapshots.definition_build_failures + failures)
|
|
1230
|
+
(@snapshots.definition_build_failures + failures)
|
|
1231
|
+
.group_by(&:first)
|
|
1232
|
+
.map { |_, candidates| candidates.min_by { |c| Environment::RbsLoader.definition_build_detail_order(c) } }
|
|
1233
|
+
.freeze
|
|
1086
1234
|
end
|
|
1087
1235
|
|
|
1088
1236
|
# Issue #784 — first-wins, unlike {#record_definition_build_failures}'s accumulate-and-dedup. That
|
|
@@ -8,6 +8,7 @@ require_relative "../../inference/synthetic_method_scanner"
|
|
|
8
8
|
require_relative "../../inference/project_patched_scanner"
|
|
9
9
|
require_relative "../dependency_source_inference"
|
|
10
10
|
require_relative "../project_scan"
|
|
11
|
+
require_relative "../worker_session"
|
|
11
12
|
require_relative "../template_units"
|
|
12
13
|
require_relative "../template_unit_collector"
|
|
13
14
|
|
|
@@ -256,11 +257,15 @@ module Rigor
|
|
|
256
257
|
# Issue #1541 — what a pooled run's workers would have produced from `#prepare`, for the coordinator's
|
|
257
258
|
# empty-closure branch, which dispatches none: the `#prepare` error rows and the run-scoped
|
|
258
259
|
# registrations (`#disclose_once` / `#emit_once`) made there. It runs on a THROWAWAY registry, so
|
|
259
|
-
# nothing `#prepare` records on a plugin instance
|
|
260
|
-
#
|
|
261
|
-
#
|
|
260
|
+
# nothing `#prepare` records on a plugin instance reaches the coordinator-side instances
|
|
261
|
+
# {DiagnosticAggregator} reads; issue #1900 — the trust refusals are therefore handed back too, in
|
|
262
|
+
# the shape {Analysis::WorkerSession#drain_trust_refusals} gives a pooled cold run's.
|
|
263
|
+
# Returns the rows first, then the disclosure records, then the refusals.
|
|
262
264
|
def prepare_throwaway_registry
|
|
263
265
|
registry = load_plugins
|
|
266
|
+
# Issue #1900 — `#init` already ran (in `load_plugins`) and its refusals are the coordinator-side
|
|
267
|
+
# registry's to report, so only what `#prepare` refuses from here on is handed back.
|
|
268
|
+
baseline = WorkerSession.refusal_counts(registry)
|
|
264
269
|
rows = plugin_prepare_diagnostics(registry)
|
|
265
270
|
records = registry.plugins.flat_map do |plugin|
|
|
266
271
|
plugin_id = safe_plugin_id(plugin)
|
|
@@ -268,7 +273,14 @@ module Rigor
|
|
|
268
273
|
rescue StandardError
|
|
269
274
|
[]
|
|
270
275
|
end
|
|
271
|
-
[rows, records]
|
|
276
|
+
[rows, records, trust_refusals(registry, baseline)]
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# Issue #1900 — `#prepare` on `plugin_registry` itself, returning its error rows. For the pool
|
|
280
|
+
# coordinator's sequential fallback, which analyses on the coordinator-side registry that {#run}
|
|
281
|
+
# left unprepared because the run is in pool mode.
|
|
282
|
+
def prepare_registry(plugin_registry)
|
|
283
|
+
plugin_prepare_diagnostics(plugin_registry)
|
|
272
284
|
end
|
|
273
285
|
|
|
274
286
|
# Returns the per-run shared `Plugin::FactStore` instance. All loaded plugins share this store
|
|
@@ -282,6 +294,19 @@ module Rigor
|
|
|
282
294
|
|
|
283
295
|
private
|
|
284
296
|
|
|
297
|
+
def trust_refusals(registry, baseline)
|
|
298
|
+
registry.plugins.filter_map do |plugin|
|
|
299
|
+
summary = plugin.io_boundary.refusal_summary
|
|
300
|
+
next nil if summary.nil?
|
|
301
|
+
|
|
302
|
+
plugin_id = safe_plugin_id(plugin)
|
|
303
|
+
delta = summary[:count] - baseline.fetch(plugin_id, 0)
|
|
304
|
+
summary.merge(count: delta, plugin_id: plugin_id) if delta.positive?
|
|
305
|
+
rescue StandardError
|
|
306
|
+
nil
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
285
310
|
def pool_mode?
|
|
286
311
|
@pool_mode_reader.call
|
|
287
312
|
end
|