rigortype 0.4.3 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/data/core_overlay/ostruct.rbs +40 -0
- data/docs/handbook/11-sig-gen.md +16 -0
- data/docs/manual/02-cli-reference.md +4 -3
- data/docs/manual/03-configuration.md +16 -5
- data/docs/manual/04-diagnostics.md +44 -2
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/10-mcp-server.md +32 -3
- data/docs/manual/18-removing-dead-code.md +11 -1
- data/docs/manual/plugins/rigor-rails-routes.md +2 -2
- data/docs/manual/plugins/rigor-shoulda-matchers.md +4 -1
- data/exe/rigor +12 -17
- data/lib/rigor/analysis/check_rules/rule_ids.rb +57 -0
- data/lib/rigor/analysis/check_rules.rb +46 -36
- data/lib/rigor/analysis/incremental.rb +12 -0
- data/lib/rigor/analysis/incremental_session.rb +121 -21
- data/lib/rigor/analysis/reachability/graph.rb +23 -3
- data/lib/rigor/analysis/reachability/scan.rb +51 -9
- data/lib/rigor/analysis/rule_catalog.rb +6 -1
- 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 +110 -15
- data/lib/rigor/analysis/runner/project_pre_passes.rb +29 -4
- data/lib/rigor/analysis/runner.rb +35 -9
- data/lib/rigor/analysis/severity_stamp.rb +34 -1
- data/lib/rigor/analysis/worker_session.rb +57 -1
- data/lib/rigor/cache/descriptor.rb +40 -6
- data/lib/rigor/cache/incremental_snapshot.rb +20 -4
- data/lib/rigor/cli/mcp_command.rb +1 -1
- data/lib/rigor/config_audit.rb +111 -15
- data/lib/rigor/configuration/dependencies.rb +19 -11
- data/lib/rigor/environment/rbs_loader.rb +46 -10
- data/lib/rigor/inference/block_call_timing.rb +5 -0
- data/lib/rigor/inference/captured_locals.rb +26 -3
- data/lib/rigor/inference/definer_resolution.rb +16 -7
- data/lib/rigor/inference/expression_typer.rb +59 -4
- data/lib/rigor/inference/guard_rebinding.rb +7 -2
- data/lib/rigor/inference/last_line/self_evidence.rb +1 -1
- data/lib/rigor/inference/module_function_state.rb +80 -13
- data/lib/rigor/inference/narrowing.rb +184 -68
- data/lib/rigor/inference/reflective_ivar_write.rb +116 -0
- data/lib/rigor/inference/repeated_or_writes.rb +414 -1
- data/lib/rigor/inference/scope_indexer.rb +439 -70
- data/lib/rigor/inference/self_call_ivar_writes.rb +341 -0
- data/lib/rigor/inference/statement_evaluator.rb +84 -29
- data/lib/rigor/lens/anchor.rb +3 -3
- data/lib/rigor/lens/contract_reader.rb +15 -0
- data/lib/rigor/lens/document.rb +2 -2
- data/lib/rigor/lens/file_reader.rb +442 -73
- data/lib/rigor/lens/members.rb +65 -15
- data/lib/rigor/lens/run.rb +44 -2
- data/lib/rigor/lens.rb +2 -1
- data/lib/rigor/mcp/server.rb +61 -20
- data/lib/rigor/plugin/box_probe.rb +154 -7
- data/lib/rigor/project_environment.rb +6 -3
- data/lib/rigor/scope.rb +11 -6
- data/lib/rigor/sig_gen/layout_index.rb +36 -0
- data/lib/rigor/sig_gen/method_candidate.rb +8 -0
- data/lib/rigor/sig_gen/path_mapper.rb +18 -2
- data/lib/rigor/sig_gen/writer.rb +47 -34
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-dry-schema/lib/rigor/plugin/dry_schema.rb +5 -1
- data/plugins/rigor-dry-validation/lib/rigor/plugin/dry_validation.rb +5 -1
- data/plugins/rigor-ffi/lib/rigor/plugin/ffi/discoverer.rb +28 -6
- 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-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +155 -12
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec.rb +10 -8
- metadata +4 -1
|
@@ -38,7 +38,13 @@ 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
|
-
|
|
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
|
+
Recheck = Data.define(:diagnostics, :changed, :added, :removed, :affected, :reused, :templates, :rebaselined) do
|
|
44
|
+
def initialize(rebaselined: false, **rest)
|
|
45
|
+
super
|
|
46
|
+
end
|
|
47
|
+
|
|
42
48
|
# A recheck that changed, added, and removed nothing — the session state is byte-equivalent to the
|
|
43
49
|
# snapshot that was restored, so persisting it again would only rewrite identical bytes.
|
|
44
50
|
def no_change?
|
|
@@ -208,6 +214,7 @@ module Rigor
|
|
|
208
214
|
# ADR-103 WD13 / #382 — a baseline collects every file, so its collections are the whole world.
|
|
209
215
|
@effect_collections = runner.effect_collections_by_path
|
|
210
216
|
@effects_identity = current_effects_identity
|
|
217
|
+
@effect_plugin_digest = effect_plugin_digest_of(runner)
|
|
211
218
|
@cache = per_file(runner.per_file_diagnostics)
|
|
212
219
|
@digests = @analyzed.to_h { |path| [path, pack_digest(path)] }
|
|
213
220
|
forget_unbound_digests
|
|
@@ -238,15 +245,13 @@ module Rigor
|
|
|
238
245
|
# go in too, so the runner lists them in the full run's file order (#1524).
|
|
239
246
|
runner = build_runner(analyze_only: analyze_set, record_dependencies: true,
|
|
240
247
|
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))
|
|
248
|
+
restored_run_level_rows: @run_level_rows, **served_state(reused))
|
|
243
249
|
merged = run_runner(runner).diagnostics
|
|
244
|
-
|
|
250
|
+
return rebaseline_recheck(changed, added, removed) if served_effects_stale?(runner, reused)
|
|
251
|
+
|
|
245
252
|
absorb(runner, current, analyze_set, removed)
|
|
246
253
|
@param_table = fresh_params
|
|
247
|
-
|
|
248
|
-
removed: removed.to_set, affected: affected, reused: reused.to_set,
|
|
249
|
-
templates: runner.template_unit_paths.size)
|
|
254
|
+
recheck_result(merged, [changed, added, removed], affected: affected, reused: reused.to_set, runner: runner)
|
|
250
255
|
end
|
|
251
256
|
|
|
252
257
|
# Editor mode option B (#146) — a whole-project recheck with the editor's buffer substituted for one
|
|
@@ -271,6 +276,9 @@ module Rigor
|
|
|
271
276
|
# latency this mode exists to avoid, so decline and let the caller drop to single-file scope.
|
|
272
277
|
break nil unless @plugin_fact_reusable.reusable_against?(restored.plugin_fact_digest)
|
|
273
278
|
break nil if source_rbs_unverified?(result)
|
|
279
|
+
# #1898 — a recheck that re-baselined over stale plugin effect facts ran its baseline with the buffer set,
|
|
280
|
+
# which analyses the buffer alone, so it is not a whole-project answer. Decline, as above.
|
|
281
|
+
break nil if result.rebaselined
|
|
274
282
|
|
|
275
283
|
result
|
|
276
284
|
end
|
|
@@ -342,6 +350,35 @@ module Rigor
|
|
|
342
350
|
closure.freeze
|
|
343
351
|
end
|
|
344
352
|
|
|
353
|
+
# 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 envelopes to the constant's bucket,
|
|
355
|
+
# but a reader's class edge names only the files that declared the constant when the reader last ran, and the
|
|
356
|
+
# class-declaration diff does not move while the constant stays in the file (`:own` becoming `:keep`, or the
|
|
357
|
+
# call becoming a `class D` reopening). So a declaration-unstable edit to such a file fires `class:<last
|
|
358
|
+
# segment>` for each constant that, before the edit, both sat in the file's singleton-source set and held a
|
|
359
|
+
# singleton envelope entry or an unpositioned singleton side the file recorded (a computed name there leaves the
|
|
360
|
+
# side unpositioned, and becoming literal reads it again). A file that only includes into a constant
|
|
361
|
+
# (`ActiveRecord::Base.include(X)`) records neither and fires nothing. A constant the edit makes the file
|
|
362
|
+
# rewrite needs no key: the class-declaration diff fires if it is new to the file, and the reader's class edge
|
|
363
|
+
# names the file if the file declared it.
|
|
364
|
+
def singleton_rewrite_affected(unstable)
|
|
365
|
+
names = unstable.flat_map { |path| singleton_rewritten_constants(@seed_bundles[path]) }.uniq
|
|
366
|
+
return Set.new if names.empty?
|
|
367
|
+
|
|
368
|
+
Incremental.negative_closure(names.map { |name| "class:#{name.split('::').last}" }, @negative_dependents)
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def singleton_rewritten_constants(bundle)
|
|
372
|
+
return [] if bundle.nil?
|
|
373
|
+
|
|
374
|
+
envelopes = bundle[:parameter_envelopes] || {}
|
|
375
|
+
unpositioned = bundle[:unpositioned_mixins] || {}
|
|
376
|
+
(bundle[:singleton_class_source_names] || []).select do |name|
|
|
377
|
+
(envelopes[name] || {}).each_key.any? { |key| key.is_a?(Array) && key.first == :singleton } ||
|
|
378
|
+
unpositioned.dig(name, :extend)
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
|
|
345
382
|
# Issue #1367 — every file whose last result holds a `global.*` diagnostic re-checks on any recheck. What
|
|
346
383
|
# exempts such a write is a fact of the whole program — an alias, or a `write` / `to_str` / `to_int` / hatch
|
|
347
384
|
# defined in any spelling on any receiver, a rewritten ancestor, a mixin — which no name-keyed edge can
|
|
@@ -365,13 +402,15 @@ module Rigor
|
|
|
365
402
|
|
|
366
403
|
# The dependents contributed by the declaration-unstable changed files and the behaviourally-unstable
|
|
367
404
|
# 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
|
|
405
|
+
# unstable files + symbol deps of the changed pairs), else the coarse file-level fan-out; plus the readers of
|
|
406
|
+
# the constants an unstable file rewrote from outside their bodies ({#singleton_rewrite_affected}).
|
|
369
407
|
def dependents_base(unstable, symbol_pairs)
|
|
370
|
-
if symbol_pairs.any? || unstable.any? { |f| @ancestry_dependents[f] }
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
408
|
+
base = if symbol_pairs.any? || unstable.any? { |f| @ancestry_dependents[f] }
|
|
409
|
+
Incremental.affected_with_symbols(unstable, symbol_pairs, @symbol_dependents, @ancestry_dependents)
|
|
410
|
+
else
|
|
411
|
+
Incremental.affected(unstable, @dependents)
|
|
412
|
+
end
|
|
413
|
+
base | singleton_rewrite_affected(unstable)
|
|
375
414
|
end
|
|
376
415
|
|
|
377
416
|
# ADR-67 WD6c lift — the fresh whole-project inferred-param table for this recheck ({} when the gate
|
|
@@ -466,7 +505,7 @@ module Rigor
|
|
|
466
505
|
def whole_project_closure(changed, added)
|
|
467
506
|
(@analyzed.to_set | changed | added).freeze
|
|
468
507
|
end
|
|
469
|
-
private :gated_closure, :whole_project_closure
|
|
508
|
+
private :gated_closure, :whole_project_closure, :singleton_rewrite_affected, :singleton_rewritten_constants
|
|
470
509
|
|
|
471
510
|
# The current project file set (cheap directory expansion, no analysis), used to detect files added /
|
|
472
511
|
# removed since the last run. Also where an editor buffer's binding is re-spelled onto the set, since
|
|
@@ -489,8 +528,7 @@ module Rigor
|
|
|
489
528
|
# ADR-67 WD6c lift — seed the subset run from the baseline's own table so the verification engine
|
|
490
529
|
# exercises the exact seeds the served cache entries were computed under (and skips a re-collect).
|
|
491
530
|
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))
|
|
531
|
+
restored_run_level_rows: @run_level_rows, **served_state(reused))
|
|
494
532
|
run_runner(runner).diagnostics
|
|
495
533
|
end
|
|
496
534
|
|
|
@@ -534,7 +572,7 @@ module Rigor
|
|
|
534
572
|
result = recheck
|
|
535
573
|
adopt_plugin_fact_fingerprint
|
|
536
574
|
fact_reusable = @plugin_fact_reusable.reusable_against?(restored.plugin_fact_digest)
|
|
537
|
-
if fact_reusable && effects_reusable && !source_rbs_unverified?(result)
|
|
575
|
+
if fact_reusable && effects_reusable && !result.rebaselined && !source_rbs_unverified?(result)
|
|
538
576
|
diagnostics = keep_recheck(result)
|
|
539
577
|
warm = true
|
|
540
578
|
# ADR-87 WD3 — a warm recheck that changed nothing leaves the session state byte-equivalent to the
|
|
@@ -549,7 +587,8 @@ module Rigor
|
|
|
549
587
|
# digest (from the recheck runner) is unchanged by the re-analysis, so it is kept for the save.
|
|
550
588
|
# Only a genuine fact-surface reason sets the reporting flag the CLI banner reads.
|
|
551
589
|
@fact_surface_invalidated = true unless fact_reusable
|
|
552
|
-
|
|
590
|
+
# #1898 — a recheck that already re-baselined over stale plugin effect facts is that re-analysis.
|
|
591
|
+
diagnostics = result.rebaselined ? result.diagnostics : baseline
|
|
553
592
|
warm = false
|
|
554
593
|
skip_save = false
|
|
555
594
|
end
|
|
@@ -873,6 +912,7 @@ module Rigor
|
|
|
873
912
|
# a partial re-collection cannot be closed into a whole-project fixpoint.
|
|
874
913
|
@effects_identity = payload.effects_identity
|
|
875
914
|
@effect_collections = effect_collections_reusable?(payload) ? payload.effect_collections.dup : {}
|
|
915
|
+
@effect_plugin_digest = payload.effect_plugin_digest
|
|
876
916
|
@symbol_dependents = Incremental.invert_symbols(@symbol_sources)
|
|
877
917
|
@ancestry_dependents = Incremental.invert(@ancestry_sources)
|
|
878
918
|
@negative_dependents = Incremental.invert(@missing)
|
|
@@ -901,7 +941,8 @@ module Rigor
|
|
|
901
941
|
param_table: marshal_safe_param_table,
|
|
902
942
|
effect_collections: marshal_safe_effect_collections,
|
|
903
943
|
effects_identity: @effects_identity,
|
|
904
|
-
run_level_rows: @run_level_rows
|
|
944
|
+
run_level_rows: @run_level_rows,
|
|
945
|
+
effect_plugin_digest: @effect_plugin_digest
|
|
905
946
|
)
|
|
906
947
|
end
|
|
907
948
|
|
|
@@ -913,6 +954,7 @@ module Rigor
|
|
|
913
954
|
def reset_effect_state
|
|
914
955
|
@effect_collections = {}
|
|
915
956
|
@effects_identity = nil
|
|
957
|
+
@effect_plugin_digest = nil
|
|
916
958
|
end
|
|
917
959
|
|
|
918
960
|
# The state this session carries ACROSS processes in the snapshot rather than deriving each run: the
|
|
@@ -1003,6 +1045,7 @@ module Rigor
|
|
|
1003
1045
|
# multiple edits: the analyzed set gets fresh diagnostics + digests + dependency edges, removed files
|
|
1004
1046
|
# are evicted from every map, and the analyzed-file list advances to `current`.
|
|
1005
1047
|
def absorb(runner, current, analyze_set, removed)
|
|
1048
|
+
@last_runner = runner # ADR-88 WD1 — the post-hoc fact-surface fingerprint reads this prepared registry.
|
|
1006
1049
|
removed.each { |path| forget(path) }
|
|
1007
1050
|
@analyzed = current
|
|
1008
1051
|
# ADR-85 WD2 — the recheck's discovery folded the restored bundles and refreshed them (changed files
|
|
@@ -1036,6 +1079,7 @@ module Rigor
|
|
|
1036
1079
|
@effect_collections.reject! { |path, _| analyzed.include?(path) }
|
|
1037
1080
|
@effect_collections.merge!(runner.effect_collections_by_path)
|
|
1038
1081
|
@effects_identity = current_effects_identity
|
|
1082
|
+
@effect_plugin_digest = effect_plugin_digest_of(runner)
|
|
1039
1083
|
end
|
|
1040
1084
|
|
|
1041
1085
|
# ADR-89 WD2 — replace the behavioural summaries of every re-analyzed file with THIS run's harvest: drop
|
|
@@ -1131,7 +1175,8 @@ module Rigor
|
|
|
1131
1175
|
# appeared `"ClassName#method"` to the negative-dependency key it would satisfy (`toplevel:foo` for a
|
|
1132
1176
|
# top-level def, `method:C#m` otherwise), then unions the recorded negative-dependents of those keys.
|
|
1133
1177
|
# 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}).
|
|
1178
|
+
# 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}).
|
|
1135
1180
|
def negative_affected(changed, removed, new_fingerprints, new_class_decls, new_constant_decls, summary = nil)
|
|
1136
1181
|
appeared_methods = Incremental.appeared_symbols(changed, @symbol_fingerprints, new_fingerprints)
|
|
1137
1182
|
# Issue #639 — a class DISAPPEARING satisfies the `class:` kind too, now that a resolved bare
|
|
@@ -1149,7 +1194,7 @@ module Rigor
|
|
|
1149
1194
|
keys.concat(class_move_keys(moved_classes))
|
|
1150
1195
|
keys.concat(moved_constants.map { |name| "constant:#{name.split('::').last}" })
|
|
1151
1196
|
Incremental.negative_closure(keys, @negative_dependents) | refinement_affected(changed + removed, summary) |
|
|
1152
|
-
global_write_reporters
|
|
1197
|
+
defined_name_affected(changed + removed, summary) | global_write_reporters
|
|
1153
1198
|
end
|
|
1154
1199
|
|
|
1155
1200
|
# The negative keys a class-declaration diff fires: `class:<last segment>` for a class name, and (issue #1820)
|
|
@@ -1177,6 +1222,22 @@ module Rigor
|
|
|
1177
1222
|
Incremental.negative_closure(names.map { |name| "refinement:#{name}" }, @negative_dependents)
|
|
1178
1223
|
end
|
|
1179
1224
|
|
|
1225
|
+
# Issue #1459 — the consumers whose answer asked whether the program defines a method name anywhere, for each
|
|
1226
|
+
# name a file in `paths` started or stopped defining. The before-state is each file's seed bundle, the
|
|
1227
|
+
# after-state the scan's per-file name sets. Unlike the appeared-symbol diff, it sees a name an `attr_*`,
|
|
1228
|
+
# `alias` or `define_method` adds, and one that vanishes, which brings a declined reading back.
|
|
1229
|
+
def defined_name_affected(paths, summary)
|
|
1230
|
+
after = (summary && summary[:defined_method_names]) || {}
|
|
1231
|
+
before = paths.to_h do |path|
|
|
1232
|
+
bundle = @seed_bundles[path]
|
|
1233
|
+
[path, bundle && Inference::ScopeIndexer.defined_method_names(bundle)]
|
|
1234
|
+
end
|
|
1235
|
+
names = Incremental.changed_defined_names(paths, before, after)
|
|
1236
|
+
return Set.new if names.empty?
|
|
1237
|
+
|
|
1238
|
+
Incremental.negative_closure(names.map { |name| "defines:#{name}" }, @negative_dependents)
|
|
1239
|
+
end
|
|
1240
|
+
|
|
1180
1241
|
# The qualified class/module names declared in the pre-parsed `index` (shared with
|
|
1181
1242
|
# {#symbol_fingerprints_from_index}, so the changed set is parsed once). `{ path => Set<class name> }`.
|
|
1182
1243
|
def class_declarations_from_index(index)
|
|
@@ -1335,6 +1396,45 @@ module Rigor
|
|
|
1335
1396
|
[forced, (affected | forced) & current]
|
|
1336
1397
|
end
|
|
1337
1398
|
|
|
1399
|
+
# What the session serves a narrowed run for the files it does not analyse: their cached per-file rows (#1524),
|
|
1400
|
+
# and (#1898) their effect collections, so the run closes its effect graph over the whole project.
|
|
1401
|
+
def served_state(reused)
|
|
1402
|
+
{ served_per_file_diagnostics: @cache.slice(*reused),
|
|
1403
|
+
served_effect_collections: @effect_collections.slice(*reused) }
|
|
1404
|
+
end
|
|
1405
|
+
|
|
1406
|
+
# Issue #1898 — the plugin effect-fact digest a run's collections were stamped under. A plugin writes its
|
|
1407
|
+
# attributions and edges into a file's collection when the file is collected, and what it contributes can
|
|
1408
|
+
# follow project state no file digest sees (rigor-activejob reads `config.active_job.queue_adapter`), so a
|
|
1409
|
+
# served collection is current only while this digest is. nil when collection is off.
|
|
1410
|
+
def effect_plugin_digest_of(runner)
|
|
1411
|
+
@configuration.effects_enabled? ? runner.effect_plugin_facts.digest : nil
|
|
1412
|
+
end
|
|
1413
|
+
|
|
1414
|
+
# Whether `runner` judged envelopes over served collections stamped under a different plugin effect-fact
|
|
1415
|
+
# digest, or an unknown one. The comparison waits for the run because the plugins load inside it.
|
|
1416
|
+
def served_effects_stale?(runner, reused)
|
|
1417
|
+
return false if reused.empty? || @effect_collections.empty?
|
|
1418
|
+
|
|
1419
|
+
effect_plugin_digest_of(runner) != @effect_plugin_digest
|
|
1420
|
+
end
|
|
1421
|
+
|
|
1422
|
+
# The answer {#recheck} gives when {#served_effects_stale?}: a full baseline, reported as a recheck that
|
|
1423
|
+
# re-analysed every file. {#run_incremental} reads `rebaselined` so it neither runs a second baseline
|
|
1424
|
+
# nor reports the run as warm.
|
|
1425
|
+
def rebaseline_recheck(changed, added, removed)
|
|
1426
|
+
diagnostics = baseline
|
|
1427
|
+
recheck_result(diagnostics, [changed, added, removed],
|
|
1428
|
+
affected: @analyzed.to_set, reused: Set.new, runner: @last_runner, rebaselined: true)
|
|
1429
|
+
end
|
|
1430
|
+
|
|
1431
|
+
# A {Recheck} over `deltas` (`[changed, added, removed]`), counting `runner`'s template units.
|
|
1432
|
+
def recheck_result(diagnostics, deltas, affected:, reused:, runner:, rebaselined: false)
|
|
1433
|
+
changed, added, removed = deltas.map(&:to_set)
|
|
1434
|
+
Recheck.new(diagnostics: diagnostics, changed: changed, added: added, removed: removed, affected: affected,
|
|
1435
|
+
reused: reused, templates: runner.template_unit_paths.size, rebaselined: rebaselined)
|
|
1436
|
+
end
|
|
1437
|
+
|
|
1338
1438
|
# A recheck that forces targets analyses in this process, so the index callback reaches them.
|
|
1339
1439
|
def forced_target_options(forced)
|
|
1340
1440
|
forced.empty? ? {} : { workers: 0 }
|
|
@@ -50,11 +50,31 @@ module Rigor
|
|
|
50
50
|
return [nil] if receiver.nil?
|
|
51
51
|
|
|
52
52
|
local = @shadows[use.path]
|
|
53
|
+
runtime = use.receiver_open ? runtime_targets(receiver, name, use.inherit, local) : []
|
|
53
54
|
own = declared_member(receiver, name, local)
|
|
54
|
-
return [own] if own
|
|
55
|
-
return
|
|
55
|
+
return [own, *runtime].uniq if own
|
|
56
|
+
return runtime unless use.inherit
|
|
56
57
|
|
|
57
|
-
plausible_ancestors(receiver).filter_map { |scope| declared_member(scope, name, local) }
|
|
58
|
+
(plausible_ancestors(receiver).filter_map { |scope| declared_member(scope, name, local) } + runtime)
|
|
59
|
+
.uniq + [nil]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# The declarations a descendant of `receiver` holding `name` adds (#1878): a `self` standing for the
|
|
63
|
+
# enclosing declaration is whichever subclass, includer, prepender or extender the method runs on, and
|
|
64
|
+
# Ruby looks the name up there first. Each descendant is searched with its own ancestors unless the call
|
|
65
|
+
# passes `inherit = false`. The descendants are over-approximated as {#plausible_ancestors} is.
|
|
66
|
+
def runtime_targets(receiver, name, inherit, local)
|
|
67
|
+
descendants_of(receiver).flat_map do |descendant|
|
|
68
|
+
scopes = inherit ? [descendant, *plausible_ancestors(descendant)] : [descendant]
|
|
69
|
+
scopes.filter_map { |scope| declared_member(scope, name, local) }
|
|
70
|
+
end.uniq
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def descendants_of(fqn)
|
|
74
|
+
@plausible_descendants ||= @by_fqn.keys.each_with_object(Hash.new { |h, k| h[k] = [] }) do |declared, out|
|
|
75
|
+
plausible_ancestors(declared).each { |ancestor| out[ancestor] << declared }
|
|
76
|
+
end
|
|
77
|
+
@plausible_descendants.fetch(fqn, [])
|
|
58
78
|
end
|
|
59
79
|
|
|
60
80
|
# `scope::name`, or the longest declared prefix of it, when one is declared.
|
|
@@ -57,8 +57,16 @@ module Rigor
|
|
|
57
57
|
# matching. A literal `name` keeps `within` too, and {Graph} looks it up from there (#1761). `inherit`
|
|
58
58
|
# is false when the call passes `inherit = false`, which confines the lookup to the receiver itself;
|
|
59
59
|
# otherwise the receiver's ancestors are searched as well. `role` is the referring file's (WD8).
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
#
|
|
61
|
+
# `receiver_open` is true when `within` is the enclosing declaration standing for a runtime `self` that
|
|
62
|
+
# may be one of its descendants (#1878): a `def self.x = const_get(:X)` runs on whichever subclass it
|
|
63
|
+
# is called on, and a module's method on whichever class includes, prepends or extends it. {Graph}
|
|
64
|
+
# then looks the literal name up in those descendants as well.
|
|
65
|
+
DynamicUse = Data.define(:name, :prefix, :reason, :site, :path, :line, :scope, :within, :inherit, :role,
|
|
66
|
+
:receiver_open) do
|
|
67
|
+
def initialize(scope: :namespace, within: nil, inherit: true, role: :production, receiver_open: false, **)
|
|
68
|
+
super
|
|
69
|
+
end
|
|
62
70
|
|
|
63
71
|
# Whether this site's evidence reaches `fqn`.
|
|
64
72
|
def taints?(fqn)
|
|
@@ -251,10 +259,21 @@ module Rigor
|
|
|
251
259
|
module_exec instance_eval instance_exec].freeze
|
|
252
260
|
private_constant :SELF_REBINDING
|
|
253
261
|
|
|
262
|
+
# The `*_eval` / `*_exec` family: on a constant receiver, the block's `self` IS that constant (#1878).
|
|
263
|
+
EVAL_FAMILY = %i[class_eval module_eval class_exec module_exec instance_eval instance_exec].freeze
|
|
264
|
+
private_constant :EVAL_FAMILY
|
|
265
|
+
|
|
254
266
|
# A call is a dynamic-resolution site, and its block may run with another `self` ({#rebinds_self?}).
|
|
255
267
|
def record_call(node, nesting)
|
|
256
268
|
record_dynamic_use(node, nesting)
|
|
257
|
-
|
|
269
|
+
return unless node.block.is_a?(Prism::BlockNode) && rebinds_self?(node)
|
|
270
|
+
|
|
271
|
+
@rebinding_blocks << node.block
|
|
272
|
+
@eval_receivers[node.block] = receiver_reference(node.receiver, nesting) if eval_on_constant?(node)
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def eval_on_constant?(call)
|
|
276
|
+
EVAL_FAMILY.include?(call.name) && constant_receiver?(call.receiver)
|
|
258
277
|
end
|
|
259
278
|
|
|
260
279
|
# Rigor knows the argument's shape, which is the whole reason this can be tiered rather than treated
|
|
@@ -290,6 +309,7 @@ module Rigor
|
|
|
290
309
|
bounds.each do |prefix, scope, within|
|
|
291
310
|
@dynamic_uses << DynamicUse.new(name: name, prefix: prefix, scope: scope, within: within,
|
|
292
311
|
inherit: anchors.include?(TOP_LEVEL), role: @role,
|
|
312
|
+
receiver_open: !within.nil? && runtime_self?(node),
|
|
293
313
|
site: site(node), path: @path, line: node.location.start_line,
|
|
294
314
|
reason: reason)
|
|
295
315
|
end
|
|
@@ -318,12 +338,21 @@ module Rigor
|
|
|
318
338
|
end
|
|
319
339
|
end
|
|
320
340
|
|
|
341
|
+
# Whether a `const_get` on `self` can run on a descendant of the enclosing declaration (#1878): in a
|
|
342
|
+
# method, or a block that rebinds `self`, `self` is whichever subclass or includer the method was
|
|
343
|
+
# called on. Directly in a class body, or inside `Other.class_eval { }`, it is the declaration itself.
|
|
344
|
+
def runtime_self?(node)
|
|
345
|
+
node.name == :const_get && self_receiver?(node.receiver) && @eval_receiver.nil? &&
|
|
346
|
+
!(@self_kind == :declaration && !@in_block && !@self_rebound)
|
|
347
|
+
end
|
|
348
|
+
|
|
321
349
|
# Where a `const_get` on `self` (implicit, `self`, or `self.class`) looks its name up: `:declaration`
|
|
322
350
|
# when `self` is the enclosing class or module, `:top_level` when it is `Class` or a singleton class,
|
|
323
351
|
# `:unknown` otherwise. A block's `self` is unknown — `included do`, `class_eval` and an instance-
|
|
324
352
|
# evaluated DSL block (`configure { }`, an RSpec `describe` body) all rebind it, and the walk cannot
|
|
325
353
|
# tell those from an `each` block. So is the `self` of a `def Other.x` or of a `class << Other` body.
|
|
326
354
|
def self_lookup(receiver)
|
|
355
|
+
return :declaration if @eval_receiver && !receiver.is_a?(Prism::CallNode)
|
|
327
356
|
return :unknown if @in_block || @self_rebound
|
|
328
357
|
|
|
329
358
|
class_of_self = receiver.is_a?(Prism::CallNode)
|
|
@@ -345,7 +374,7 @@ module Rigor
|
|
|
345
374
|
receiver = node.receiver
|
|
346
375
|
namespaces =
|
|
347
376
|
if self_receiver?(receiver)
|
|
348
|
-
self_references(node, nesting)
|
|
377
|
+
eval_self_references(receiver) || self_references(node, nesting)
|
|
349
378
|
elsif constant_receiver?(receiver)
|
|
350
379
|
[receiver_reference(receiver, nesting)]
|
|
351
380
|
else
|
|
@@ -357,6 +386,11 @@ module Rigor
|
|
|
357
386
|
inherit.is_a?(Prism::FalseNode) ? namespaces : namespaces + [TOP_LEVEL]
|
|
358
387
|
end
|
|
359
388
|
|
|
389
|
+
# Inside `Other.class_eval { }` an implicit or `self` receiver is `Other` (#1878).
|
|
390
|
+
def eval_self_references(receiver)
|
|
391
|
+
[@eval_receiver] if @eval_receiver && !receiver.is_a?(Prism::CallNode)
|
|
392
|
+
end
|
|
393
|
+
|
|
360
394
|
# The enclosing declaration as a rooted {Reference}, so {Graph} anchors it as it anchors a constant
|
|
361
395
|
# receiver and can search its ancestors. Inside a meta-new rvalue `self` is the declared class in its
|
|
362
396
|
# block but the outer scope in its arguments; the walk does not tell the two apart, so both are taken.
|
|
@@ -478,6 +512,10 @@ module Rigor
|
|
|
478
512
|
# `class_eval` or anonymous `Class.new` block. See {#rebinds_self?}.
|
|
479
513
|
@self_rebound = false
|
|
480
514
|
@rebinding_blocks = Set.new.compare_by_identity
|
|
515
|
+
# The constant receiver of each `*_eval` block, and the one the walk is inside (nil when `self` is not
|
|
516
|
+
# such a constant). See {#eval_on_constant?}.
|
|
517
|
+
@eval_receivers = {}.compare_by_identity
|
|
518
|
+
@eval_receiver = nil
|
|
481
519
|
# What `self` is here, for {#self_lookup}: `:declaration` in a class or module body (and at the top
|
|
482
520
|
# level), `:singleton_method` in a `def self.x` or a def in a `class << self` body,
|
|
483
521
|
# `:instance_method` in any other def, `:singleton_class` in a `class << self` body outside its
|
|
@@ -602,13 +640,14 @@ module Rigor
|
|
|
602
640
|
end
|
|
603
641
|
|
|
604
642
|
def in_local_scope(scope, self_kind = self_kind_in(scope))
|
|
605
|
-
saved = [@local_scope, @block_params, @self_kind]
|
|
643
|
+
saved = [@local_scope, @block_params, @self_kind, @eval_receiver]
|
|
644
|
+
@eval_receiver = nil
|
|
606
645
|
@local_scope = scope
|
|
607
646
|
@block_params = Set.new.freeze
|
|
608
647
|
@self_kind = self_kind
|
|
609
648
|
yield
|
|
610
649
|
ensure
|
|
611
|
-
@local_scope, @block_params, @self_kind = saved
|
|
650
|
+
@local_scope, @block_params, @self_kind, @eval_receiver = saved
|
|
612
651
|
end
|
|
613
652
|
|
|
614
653
|
# The {#self_lookup} kind inside `scope`, from the kind around it. Only a receiver-less def or a `def
|
|
@@ -642,14 +681,17 @@ module Rigor
|
|
|
642
681
|
end
|
|
643
682
|
|
|
644
683
|
def in_block(block)
|
|
645
|
-
saved = [@block_params, @self_rebound, @in_block]
|
|
684
|
+
saved = [@block_params, @self_rebound, @in_block, @eval_receiver]
|
|
646
685
|
names = block.parameters ? LocalValues.bindings(block.parameters) : []
|
|
647
686
|
@block_params = (@block_params | names).freeze unless names.empty?
|
|
648
|
-
|
|
687
|
+
if @rebinding_blocks.include?(block)
|
|
688
|
+
@self_rebound = true
|
|
689
|
+
@eval_receiver = @eval_receivers[block]
|
|
690
|
+
end
|
|
649
691
|
@in_block = true
|
|
650
692
|
yield
|
|
651
693
|
ensure
|
|
652
|
-
@block_params, @self_rebound, @in_block = saved
|
|
694
|
+
@block_params, @self_rebound, @in_block, @eval_receiver = saved
|
|
653
695
|
end
|
|
654
696
|
|
|
655
697
|
def site(node)
|
|
@@ -94,7 +94,12 @@ module Rigor
|
|
|
94
94
|
does_not_fire_when: [
|
|
95
95
|
"Implicit-self calls (no receiver) — too noisy without per-method RBS for every helper.",
|
|
96
96
|
"Receiver is `Dynamic[T]` / `Top` / `Union` — by definition the method set isn't enumerable.",
|
|
97
|
-
"Receiver class is in the loader but its RBS definition cannot be built (constant aliases)."
|
|
97
|
+
"Receiver class is in the loader but its RBS definition cannot be built (constant aliases).",
|
|
98
|
+
"Receiver class answers beyond any signature: a plugin-declared open receiver (ADR-26), or one " \
|
|
99
|
+
"Rigor's bundled overlays name (`OpenStruct`, whose fields are per-instance singleton methods; " \
|
|
100
|
+
"`ActiveSupport::Duration` and `ActiveSupport::Concern` when their gem overlay loads). Both sides " \
|
|
101
|
+
"of the class are open, and an inherited Object / Kernel signature no longer decides " \
|
|
102
|
+
"`call.wrong-arity` or `call.argument-type-mismatch` on it."
|
|
98
103
|
],
|
|
99
104
|
suppression: "`# rigor:disable call.undefined-method` on the call line, " \
|
|
100
105
|
"or `disable: [\"call.undefined-method\"]` in `.rigor.yml`.",
|
|
@@ -70,13 +70,14 @@ module Rigor
|
|
|
70
70
|
RUN_EFFECTS_TABLE_PRODUCER_ID = "analysis.run-effects-table"
|
|
71
71
|
|
|
72
72
|
# The run-result producer's declared compaction budget (`Cache::Store#evict!` pass 2). Whole-project,
|
|
73
|
-
# but unlike the `rbs.*` producers several generations can be live at once: the `paths`
|
|
74
|
-
# one entry per analyzed-path SET
|
|
75
|
-
# single file are three separate live generations
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
#
|
|
79
|
-
#
|
|
73
|
+
# but unlike the `rbs.*` producers several generations can be live at once: the `paths` and `roots` key
|
|
74
|
+
# slots mean one entry per analyzed-path SET x root list as written (#1559), so `rigor check` over the
|
|
75
|
+
# whole project, over `lib`, and over a single file are three separate live generations, and `rigor
|
|
76
|
+
# check` and `rigor check lib` are two even when `lib` is the configured `paths:`. 16 is a judgement call
|
|
77
|
+
# sized for that churn; it is a cap on GENERATIONS, not on correctness — over-evicting here costs a
|
|
78
|
+
# recompute, never a wrong answer. Measured 2026-07-25 against a real project's `.rigor/cache`: the cap
|
|
79
|
+
# does bind (60 distinct path-set generations observed, differing in the `paths` slot alone), with no
|
|
80
|
+
# evidence yet on how often an evicted generation is asked for again — see issue #151.
|
|
80
81
|
GENERATION_CAP = 16
|
|
81
82
|
|
|
82
83
|
# The effects sidecar's own compaction budget. One generation per (path set × effects identity), and
|
|
@@ -109,6 +110,29 @@ module Rigor
|
|
|
109
110
|
nil
|
|
110
111
|
end
|
|
111
112
|
|
|
113
|
+
# #1559 — the run-result key's analysis ROOTS, as the run was given them. The `paths` slot holds only the
|
|
114
|
+
# files the roots expand to, so `check lib extra` with `extra` missing and `check lib` shared a key while
|
|
115
|
+
# only the first prints `extra:1:1: warning: no such file or directory (skipped)`, which changes the exit
|
|
116
|
+
# code under `--fail-on=warning`. The roots are kept in the order and spelling written, not normalised: a
|
|
117
|
+
# missing root is reported as written (`./extra` is not `extra`), and the order of the roots is the order
|
|
118
|
+
# the diagnostics come out in, so two runs that differ only in either are two slots (a forgone hit, never a
|
|
119
|
+
# wrong one).
|
|
120
|
+
#
|
|
121
|
+
# Added to the DIAGNOSTICS key only. The ADR-103 effects sidecar derives its identity from {#descriptor}
|
|
122
|
+
# without it: the effect table is a function of the analysed files, and no path-error row reaches it.
|
|
123
|
+
# The `--incremental` slot builds its own, normalised, roots entry ({IncrementalRunSlot}).
|
|
124
|
+
#
|
|
125
|
+
# @param descriptor — a {#descriptor} result (nil passes through, so a disabled cache stays disabled).
|
|
126
|
+
def with_roots(descriptor, roots)
|
|
127
|
+
return nil if descriptor.nil?
|
|
128
|
+
|
|
129
|
+
entry = config_entry("roots", Marshal.dump(Array(roots).map(&:to_s)))
|
|
130
|
+
Cache::Descriptor.new(
|
|
131
|
+
gems: descriptor.gems, dependencies: descriptor.dependencies, plugins: descriptor.plugins,
|
|
132
|
+
configs: descriptor.configs + [entry]
|
|
133
|
+
)
|
|
134
|
+
end
|
|
135
|
+
|
|
112
136
|
# Issue #285 — the `engine` slot above pins the engine by VERSION, which identifies the source only
|
|
113
137
|
# for a released gem. A checkout (a contributor's, or a `bundle add rigor, github:` clone) gets one
|
|
114
138
|
# extra slot carrying a digest of the engine's own source, so editing `lib/rigor/inference/*.rb` no
|
|
@@ -65,9 +65,11 @@ module Rigor
|
|
|
65
65
|
# probe must never turn a servable run into a crash.
|
|
66
66
|
def serve(paths)
|
|
67
67
|
files = PathExpansion.ruby_files(paths, @configuration.exclude_patterns, base: @configuration.exclude_base)
|
|
68
|
-
key = RunCacheKey.
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
key = RunCacheKey.with_roots(
|
|
69
|
+
RunCacheKey.descriptor(
|
|
70
|
+
configuration: @configuration, files: files, explain: @explain,
|
|
71
|
+
rbs_config_entries: RunCacheKey.libraries_config_entries(@configuration)
|
|
72
|
+
), paths
|
|
71
73
|
)
|
|
72
74
|
return nil if key.nil?
|
|
73
75
|
|
|
@@ -57,7 +57,8 @@ module Rigor
|
|
|
57
57
|
quarantined_signatures_snapshot:, env_build_failure_snapshot:,
|
|
58
58
|
definition_build_failures_snapshot:, hkt_scan_failure_snapshot:,
|
|
59
59
|
conformance_results_snapshot:, signature_standdowns_snapshot: -> { [] },
|
|
60
|
-
vendored_standdowns_snapshot: -> { [] }, pooled_run_disclosures: -> { [] }
|
|
60
|
+
vendored_standdowns_snapshot: -> { [] }, pooled_run_disclosures: -> { [] },
|
|
61
|
+
pooled_trust_refusals: -> { [] })
|
|
61
62
|
@configuration = configuration
|
|
62
63
|
@rbs_extended_reporter = rbs_extended_reporter
|
|
63
64
|
@boundary_cross_reporter = boundary_cross_reporter
|
|
@@ -78,6 +79,9 @@ module Rigor
|
|
|
78
79
|
# Issue #1051 — reader for the pool coordinator's de-duplicated run-scoped disclosure table.
|
|
79
80
|
# Empty on a sequential run, where `plugin_registry`'s own instances are the ones that registered.
|
|
80
81
|
@pooled_run_disclosures_reader = pooled_run_disclosures
|
|
82
|
+
# Issue #1900 — reader for the pool coordinator's summed trust-refusal table. Empty on a
|
|
83
|
+
# sequential run, where `plugin_registry`'s own boundaries made every read.
|
|
84
|
+
@pooled_trust_refusals_reader = pooled_trust_refusals
|
|
81
85
|
end
|
|
82
86
|
|
|
83
87
|
# Pre-file diagnostic streams that fire once per run rather than per analyzed file: plugin load /
|
|
@@ -613,9 +617,18 @@ module Rigor
|
|
|
613
617
|
# culprit and reads the same on a cache hit, where every `RBS::Location` has been dumped to a
|
|
614
618
|
# sentinel. The error class is attributed to that first failure explicitly rather than to all of
|
|
615
619
|
# them: the per-class memo keeps only the first reason, and one run can mix error classes.
|
|
620
|
+
#
|
|
621
|
+
# Issue #1902 — "first" is first by CLASS NAME, not first recorded. The snapshot arrives in demand
|
|
622
|
+
# order, and that order is a property of the invocation, not of the project: an `--incremental`
|
|
623
|
+
# recheck records the classes its closure demanded before it unions in the replayed set, and a worker
|
|
624
|
+
# pool unions per-worker lists in drain order. So the classes are sorted by name and the files
|
|
625
|
+
# sorted and deduplicated here. Which tuple each class carries is fixed upstream
|
|
626
|
+
# ({Environment::RbsLoader.definition_build_detail_order}), not here.
|
|
616
627
|
def build_rbs_definition_build_failed_diagnostic(failures)
|
|
628
|
+
failures = failures.sort_by { |failure| failure.first.to_s }
|
|
617
629
|
sample_size = 5
|
|
618
|
-
files = failures.flat_map { |failure| Array(failure[3]) }
|
|
630
|
+
files = failures.flat_map { |failure| Array(failure[3]) }
|
|
631
|
+
.map { |name| relative_signature_path(name) }.uniq.sort
|
|
619
632
|
Diagnostic.new(
|
|
620
633
|
path: ".rigor.yml",
|
|
621
634
|
line: 1,
|
|
@@ -1084,16 +1097,22 @@ module Rigor
|
|
|
1084
1097
|
# cache"). It is also never routed through `IncrementalSession`'s per-file cache (`#788`): that cache
|
|
1085
1098
|
# holds only `Runner#per_file_diagnostics`, and this row is not part of that stream.
|
|
1086
1099
|
#
|
|
1087
|
-
#
|
|
1088
|
-
#
|
|
1089
|
-
#
|
|
1090
|
-
#
|
|
1091
|
-
#
|
|
1092
|
-
#
|
|
1100
|
+
# Two sources (issue #1900), summed per plugin: the coordinator-side registry's own boundaries — where
|
|
1101
|
+
# a sequential run, and the pool's no-worker fallback, made every read — and `pooled_trust_refusals`,
|
|
1102
|
+
# what a pooled run drained from its pre-fork parent session and every worker
|
|
1103
|
+
# ({Runner::PoolCoordinator#merge_trust_refusals}). The count is per refused ATTEMPT, as
|
|
1104
|
+
# `--workers 0` always counted it; the pool drains the parent's `#prepare` refusals before forking and
|
|
1105
|
+
# each worker reports only what it added, so the workers, which inherit the parent's boundaries, do
|
|
1106
|
+
# not count them again. Rows follow registry
|
|
1107
|
+
# (plugin load) order, which every mode shares.
|
|
1093
1108
|
def plugin_trust_refusal_diagnostics
|
|
1094
1109
|
return [] if plugin_registry.empty?
|
|
1095
1110
|
|
|
1096
|
-
|
|
1111
|
+
pooled = Array(pooled_trust_refusals).to_h { |record| [record[:plugin_id], record] }
|
|
1112
|
+
plugin_registry.plugins.filter_map do |plugin|
|
|
1113
|
+
summary = combined_refusal_summary(pooled[disclosure_plugin_id(plugin)], plugin)
|
|
1114
|
+
plugin_trust_refusal_diagnostic(plugin, summary) if summary
|
|
1115
|
+
end
|
|
1097
1116
|
end
|
|
1098
1117
|
|
|
1099
1118
|
# Issue #1051 — the run-scoped plugin disclosures ({Plugin::Base#disclose_once}), the run's
|
|
@@ -1193,10 +1212,23 @@ module Rigor
|
|
|
1193
1212
|
plugin.class.to_s
|
|
1194
1213
|
end
|
|
1195
1214
|
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1215
|
+
# The coordinator's own record first when it has one: on a pooled run it holds the `#init` reads
|
|
1216
|
+
# (every registry runs `#init`; the workers' drains start after it), which `--workers 0` makes before
|
|
1217
|
+
# `#prepare` and before any file, so its first path is the one a sequential run names. Not so after a
|
|
1218
|
+
# Ractor-backend degrade, where this record also holds the re-analysed files' refusals and is named
|
|
1219
|
+
# ahead of a worker's `#prepare` path; the count is right either way.
|
|
1220
|
+
def combined_refusal_summary(pooled, plugin)
|
|
1221
|
+
local = begin
|
|
1222
|
+
plugin.io_boundary.refusal_summary
|
|
1223
|
+
rescue StandardError
|
|
1224
|
+
nil
|
|
1225
|
+
end
|
|
1226
|
+
return local || pooled if pooled.nil? || local.nil?
|
|
1227
|
+
|
|
1228
|
+
local.merge(count: local[:count] + pooled[:count])
|
|
1229
|
+
end
|
|
1199
1230
|
|
|
1231
|
+
def plugin_trust_refusal_diagnostic(plugin, summary)
|
|
1200
1232
|
plugin_id = plugin.manifest.id
|
|
1201
1233
|
Diagnostic.new(
|
|
1202
1234
|
path: ".rigor.yml", line: 1, column: 1,
|
|
@@ -1283,6 +1315,10 @@ module Rigor
|
|
|
1283
1315
|
@pooled_run_disclosures_reader.call
|
|
1284
1316
|
end
|
|
1285
1317
|
|
|
1318
|
+
def pooled_trust_refusals
|
|
1319
|
+
@pooled_trust_refusals_reader.call
|
|
1320
|
+
end
|
|
1321
|
+
|
|
1286
1322
|
def pre_eval_diagnostics_from_scanner
|
|
1287
1323
|
@pre_eval_diagnostics_from_scanner_reader.call
|
|
1288
1324
|
end
|