rigortype 0.4.2 → 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 +28 -5
- data/docs/manual/02-cli-reference.md +23 -4
- data/docs/manual/03-configuration.md +21 -8
- data/docs/manual/04-diagnostics.md +44 -2
- data/docs/manual/05-inspecting-types.md +50 -0
- data/docs/manual/07-plugins.md +1 -1
- data/docs/manual/10-mcp-server.md +32 -3
- data/docs/manual/12-caching.md +18 -1
- data/docs/manual/14-rails-quickstart.md +2 -2
- data/docs/manual/18-removing-dead-code.md +26 -9
- 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 +64 -0
- data/lib/rigor/analysis/check_rules.rb +68 -57
- data/lib/rigor/analysis/effects_cache_probe.rb +1 -1
- data/lib/rigor/analysis/incremental.rb +12 -0
- data/lib/rigor/analysis/incremental_run_slot.rb +3 -3
- data/lib/rigor/analysis/incremental_session.rb +207 -40
- data/lib/rigor/analysis/path_expansion.rb +62 -6
- data/lib/rigor/analysis/plugin_fact_fingerprint.rb +124 -37
- data/lib/rigor/analysis/reachability/graph.rb +139 -15
- data/lib/rigor/analysis/reachability/scan.rb +374 -173
- data/lib/rigor/analysis/reachability/scan_cache.rb +4 -2
- 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 +6 -4
- data/lib/rigor/analysis/run_stats.rb +28 -6
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +74 -13
- data/lib/rigor/analysis/runner/pool_coordinator.rb +133 -19
- data/lib/rigor/analysis/runner/project_pre_passes.rb +46 -1
- data/lib/rigor/analysis/runner.rb +62 -18
- data/lib/rigor/analysis/severity_stamp.rb +34 -1
- data/lib/rigor/analysis/worker_session.rb +58 -2
- data/lib/rigor/cache/descriptor.rb +40 -6
- data/lib/rigor/cache/engine_source.rb +21 -7
- data/lib/rigor/cache/incremental_snapshot.rb +56 -12
- data/lib/rigor/cli/check_command.rb +138 -39
- data/lib/rigor/cli/docs_command.rb +5 -4
- data/lib/rigor/cli/lens_command.rb +144 -0
- data/lib/rigor/cli/lsp_command.rb +5 -2
- data/lib/rigor/cli/mcp_command.rb +1 -1
- data/lib/rigor/cli/skill_command.rb +3 -3
- data/lib/rigor/cli/skill_describe.rb +1 -1
- data/lib/rigor/cli/type_of_command.rb +2 -1
- data/lib/rigor/cli/unused_command.rb +157 -24
- data/lib/rigor/cli.rb +30 -4
- data/lib/rigor/config_audit.rb +111 -15
- data/lib/rigor/configuration/dependencies.rb +19 -11
- data/lib/rigor/configuration.rb +15 -0
- data/lib/rigor/environment/bundle_sig_discovery.rb +11 -7
- data/lib/rigor/environment/installed_gem_set.rb +38 -11
- data/lib/rigor/environment/lockfile_resolver.rb +37 -6
- data/lib/rigor/environment/missing_gem_constant_index.rb +22 -7
- data/lib/rigor/environment/rbs_loader.rb +79 -10
- data/lib/rigor/environment/required_features.rb +4 -2
- data/lib/rigor/environment.rb +12 -5
- data/lib/rigor/inference/block_call_timing.rb +5 -0
- data/lib/rigor/inference/builtins/cruby_definers.rb +22 -5
- data/lib/rigor/inference/captured_locals.rb +69 -6
- data/lib/rigor/inference/definer_resolution.rb +16 -7
- data/lib/rigor/inference/expression_typer.rb +117 -29
- data/lib/rigor/inference/guard_rebinding.rb +7 -2
- data/lib/rigor/inference/in_effect_refinements/scope_reads.rb +21 -17
- data/lib/rigor/inference/last_line/self_evidence.rb +1 -1
- data/lib/rigor/inference/match_rebinding.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/argument_nodes.rb +25 -0
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +3 -2
- data/lib/rigor/inference/method_dispatcher/match_data_folding.rb +2 -2
- data/lib/rigor/inference/method_dispatcher/member_shape_projection.rb +3 -3
- data/lib/rigor/inference/method_dispatcher/optional_count.rb +63 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +4 -16
- data/lib/rigor/inference/method_dispatcher/struct_folding.rb +4 -7
- data/lib/rigor/inference/method_dispatcher/struct_materialization.rb +8 -26
- data/lib/rigor/inference/method_dispatcher/unit_denominator_rational.rb +67 -0
- data/lib/rigor/inference/method_dispatcher.rb +52 -3
- data/lib/rigor/inference/module_function_state.rb +80 -13
- data/lib/rigor/inference/narrowing.rb +184 -68
- data/lib/rigor/inference/project_patched_definedness_guard.rb +86 -0
- data/lib/rigor/inference/project_patched_methods.rb +2 -2
- data/lib/rigor/inference/project_patched_scanner.rb +38 -24
- data/lib/rigor/inference/refined_dispatch/class_object.rb +350 -0
- data/lib/rigor/inference/refined_dispatch/own_method.rb +112 -40
- data/lib/rigor/inference/refined_dispatch.rb +62 -14
- data/lib/rigor/inference/reflective_ivar_write.rb +116 -0
- data/lib/rigor/inference/repeated_or_writes.rb +416 -1
- data/lib/rigor/inference/scope_indexer.rb +565 -61
- data/lib/rigor/inference/self_call_ivar_writes.rb +341 -0
- data/lib/rigor/inference/statement_evaluator.rb +307 -60
- data/lib/rigor/inference/untyped_rejoin.rb +75 -0
- data/lib/rigor/language_server/project_context.rb +56 -5
- data/lib/rigor/lens/anchor.rb +115 -0
- data/lib/rigor/lens/contract_reader.rb +133 -0
- data/lib/rigor/lens/document.rb +113 -0
- data/lib/rigor/lens/file_reader.rb +802 -0
- data/lib/rigor/lens/members.rb +203 -0
- data/lib/rigor/lens/parameters.rb +91 -0
- data/lib/rigor/lens/plugins.rb +40 -0
- data/lib/rigor/lens/run.rb +322 -0
- data/lib/rigor/lens/slot.rb +79 -0
- data/lib/rigor/lens.rb +41 -0
- data/lib/rigor/mcp/server.rb +61 -20
- data/lib/rigor/plugin/box_probe.rb +154 -7
- data/lib/rigor/project_environment.rb +23 -1
- data/lib/rigor/protection/mutation_cache.rb +2 -1
- data/lib/rigor/scope.rb +33 -17
- data/lib/rigor/sig_gen/generator.rb +45 -5
- 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-active-model-serializers/lib/rigor/plugin/active_model_serializers/serializer_discoverer.rb +29 -3
- data/plugins/rigor-active-model-serializers/lib/rigor/plugin/active_model_serializers.rb +5 -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
- data/sig/rigor/scope.rbs +3 -0
- data/sig/rigor.rbs +6 -3
- data/skills/rigor-project-init/references/02-configure.md +2 -2
- metadata +21 -1
|
@@ -48,11 +48,13 @@ module Rigor
|
|
|
48
48
|
# @param cache_path — the configuration's cache root; nil or empty is an inert
|
|
49
49
|
# cache (every fetch computes, nothing persists).
|
|
50
50
|
# @param target_ruby — parse-affecting configuration, part of the identity.
|
|
51
|
-
|
|
51
|
+
# `root` names the project and working directory a run reads roles and spellings from (#1760): a
|
|
52
|
+
# scan cached by a run from another directory carries another role, so it is never served here.
|
|
53
|
+
def self.open(cache_path, target_ruby: nil, root: nil)
|
|
52
54
|
return new(path: nil, header: nil) if cache_path.nil? || cache_path.to_s.empty?
|
|
53
55
|
|
|
54
56
|
new(path: File.join(cache_path.to_s, FILE_NAME),
|
|
55
|
-
header: "#{SCHEMA}:#{Rigor::VERSION}:#{target_ruby}:#{source_digest}")
|
|
57
|
+
header: "#{SCHEMA}:#{Rigor::VERSION}:#{target_ruby}:#{source_digest}:#{root}")
|
|
56
58
|
end
|
|
57
59
|
|
|
58
60
|
def self.source_digest
|
|
@@ -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
|
|
@@ -64,10 +64,12 @@ module Rigor
|
|
|
64
64
|
# the caller loads the engine and runs the full path. Any failure declines rather than raising: the
|
|
65
65
|
# probe must never turn a servable run into a crash.
|
|
66
66
|
def serve(paths)
|
|
67
|
-
files = PathExpansion.ruby_files(paths, @configuration.exclude_patterns)
|
|
68
|
-
key = RunCacheKey.
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
files = PathExpansion.ruby_files(paths, @configuration.exclude_patterns, base: @configuration.exclude_base)
|
|
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
|
|
|
@@ -24,18 +24,31 @@ module Rigor
|
|
|
24
24
|
# `class_decl_paths` (a frozen `Hash<String, String>` populated once per environment by the RBS loader;
|
|
25
25
|
# ~1000-2000 entries × one `String#start_with?`).
|
|
26
26
|
class RunStats
|
|
27
|
-
attr_reader :wall_seconds,
|
|
27
|
+
attr_reader :wall_seconds,
|
|
28
28
|
:target_files,
|
|
29
29
|
:rbs_classes_total, :rbs_classes_project_sig, :rbs_classes_bundled,
|
|
30
30
|
:gem_walk_classes, :gem_walk_gems, :rbs_attribution_available
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
# The live probe a run's summary uses: reads this process's RSS when first asked. The probe is a constant
|
|
33
|
+
# lambda, so the RunStats it rides on never captures the Runner (and its analysis state) through `Result`.
|
|
34
|
+
CURRENT_PROCESS_PEAK_RSS = Ractor.make_shareable(-> { RunStats.peak_rss_bytes })
|
|
35
|
+
|
|
36
|
+
# Issue #1529 — the peak RSS is read on the first {#peak_rss_bytes} call, not when the summary is built. A run
|
|
37
|
+
# that prints no summary (`--incremental` recheck, `--no-stats`) therefore never forks `ps` or reads `/proc`.
|
|
38
|
+
# The read stays in this process: the summary is built and printed by the process that ran the analysis.
|
|
39
|
+
#
|
|
40
|
+
# @param peak_rss_bytes — a value to report directly (specs, or a caller that already read it). Ignored when
|
|
41
|
+
# `peak_rss_probe:` is given.
|
|
42
|
+
# @param peak_rss_probe — a zero-argument callable returning the RSS in bytes, called at most once, lazily.
|
|
43
|
+
def initialize(wall_seconds:, # rubocop:disable Metrics/ParameterLists
|
|
33
44
|
target_files:,
|
|
34
45
|
rbs_classes_total:, rbs_classes_project_sig:, rbs_classes_bundled:,
|
|
35
46
|
gem_walk_classes:, gem_walk_gems:,
|
|
36
|
-
rbs_attribution_available: true)
|
|
47
|
+
rbs_attribution_available: true, peak_rss_bytes: nil, peak_rss_probe: nil)
|
|
37
48
|
@wall_seconds = wall_seconds
|
|
38
|
-
@
|
|
49
|
+
@peak_rss_probe = peak_rss_probe || -> { peak_rss_bytes }
|
|
50
|
+
# A frozen RunStats cannot memoise into an ivar, so the first read lands in this (unfrozen) Hash.
|
|
51
|
+
@peak_rss_memo = {}
|
|
39
52
|
@target_files = target_files
|
|
40
53
|
@rbs_classes_total = rbs_classes_total
|
|
41
54
|
@rbs_classes_project_sig = rbs_classes_project_sig
|
|
@@ -46,6 +59,15 @@ module Rigor
|
|
|
46
59
|
freeze
|
|
47
60
|
end
|
|
48
61
|
|
|
62
|
+
# The peak RSS in bytes, read once on first call and memoised (nil when no route answers). The check command
|
|
63
|
+
# forces the first call straight after the analysis (#1529), before `--coverage` or any output, so the figure
|
|
64
|
+
# covers the analysis alone. On Linux it is the kernel's VmHWM high-water mark, so a later read is never below
|
|
65
|
+
# the run-end value. On macOS it is `ps`'s current RSS, so the reading is the process as it stood at that
|
|
66
|
+
# first call, not a high-water mark.
|
|
67
|
+
def peak_rss_bytes
|
|
68
|
+
@peak_rss_memo.fetch(:value) { @peak_rss_memo[:value] = @peak_rss_probe.call }
|
|
69
|
+
end
|
|
70
|
+
|
|
49
71
|
# Reports the process's resident set size in bytes. Source ordering: `/proc/self/status` (Linux — reads
|
|
50
72
|
# `VmHWM:`, the peak RSS the kernel records) first; otherwise `ps -o rss= -p <pid>` (macOS / BSD —
|
|
51
73
|
# reports CURRENT RSS, the closest universally-available proxy). Returns nil when neither route works
|
|
@@ -146,7 +168,7 @@ module Rigor
|
|
|
146
168
|
end
|
|
147
169
|
out.puts("#{prefix}Process")
|
|
148
170
|
out.puts("#{prefix} Wall time: #{Kernel.format('%.2fs', @wall_seconds)}")
|
|
149
|
-
out.puts("#{prefix} Memory peak: #{format_bytes(
|
|
171
|
+
out.puts("#{prefix} Memory peak: #{format_bytes(peak_rss_bytes)}")
|
|
150
172
|
end
|
|
151
173
|
|
|
152
174
|
def to_h
|
|
@@ -159,7 +181,7 @@ module Rigor
|
|
|
159
181
|
gem_walk_classes: @gem_walk_classes,
|
|
160
182
|
gem_walk_gems: @gem_walk_gems,
|
|
161
183
|
wall_seconds: @wall_seconds,
|
|
162
|
-
peak_rss_bytes:
|
|
184
|
+
peak_rss_bytes: peak_rss_bytes
|
|
163
185
|
}
|
|
164
186
|
end
|
|
165
187
|
|
|
@@ -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 /
|
|
@@ -128,7 +132,32 @@ module Rigor
|
|
|
128
132
|
source_family: :builtin
|
|
129
133
|
)
|
|
130
134
|
end
|
|
131
|
-
|
|
135
|
+
scanned = Array(pre_eval_diagnostics_from_scanner).map { |hash| diagnostic_from_hash(hash) }
|
|
136
|
+
suppress_pre_eval(not_found + scanned)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# The ordinary suppression pipeline for the `pre-eval.*` rows: the `# rigor:disable` /
|
|
140
|
+
# `# rigor:disable-file` comments of the file a row is positioned in (a `pre_eval:` file need not be
|
|
141
|
+
# under `paths:`, so its comments are read here rather than by per-file analysis) and the project's
|
|
142
|
+
# `disable:` list. A row positioned at `.rigor.yml` has no Ruby comments (`.yml` is skipped) and answers to
|
|
143
|
+
# `disable:` only.
|
|
144
|
+
# Stream order is the output contract, so survivors are picked out of the original sequence.
|
|
145
|
+
def suppress_pre_eval(diagnostics)
|
|
146
|
+
kept = diagnostics.group_by(&:path).flat_map do |path, group|
|
|
147
|
+
CheckRules.filter_suppressed(
|
|
148
|
+
group, comments: pre_eval_comments(path), disabled_rules: @configuration.disabled_rules
|
|
149
|
+
)
|
|
150
|
+
end
|
|
151
|
+
kept_ids = kept.to_set(&:object_id)
|
|
152
|
+
diagnostics.select { |diagnostic| kept_ids.include?(diagnostic.object_id) }
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def pre_eval_comments(path)
|
|
156
|
+
return [].freeze unless File.file?(path) && !path.end_with?(".rbs", ".yml", ".yaml")
|
|
157
|
+
|
|
158
|
+
Prism.parse_file(path).comments
|
|
159
|
+
rescue StandardError
|
|
160
|
+
[].freeze
|
|
132
161
|
end
|
|
133
162
|
|
|
134
163
|
def diagnostic_from_hash(hash)
|
|
@@ -588,9 +617,18 @@ module Rigor
|
|
|
588
617
|
# culprit and reads the same on a cache hit, where every `RBS::Location` has been dumped to a
|
|
589
618
|
# sentinel. The error class is attributed to that first failure explicitly rather than to all of
|
|
590
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.
|
|
591
627
|
def build_rbs_definition_build_failed_diagnostic(failures)
|
|
628
|
+
failures = failures.sort_by { |failure| failure.first.to_s }
|
|
592
629
|
sample_size = 5
|
|
593
|
-
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
|
|
594
632
|
Diagnostic.new(
|
|
595
633
|
path: ".rigor.yml",
|
|
596
634
|
line: 1,
|
|
@@ -1059,16 +1097,22 @@ module Rigor
|
|
|
1059
1097
|
# cache"). It is also never routed through `IncrementalSession`'s per-file cache (`#788`): that cache
|
|
1060
1098
|
# holds only `Runner#per_file_diagnostics`, and this row is not part of that stream.
|
|
1061
1099
|
#
|
|
1062
|
-
#
|
|
1063
|
-
#
|
|
1064
|
-
#
|
|
1065
|
-
#
|
|
1066
|
-
#
|
|
1067
|
-
#
|
|
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.
|
|
1068
1108
|
def plugin_trust_refusal_diagnostics
|
|
1069
1109
|
return [] if plugin_registry.empty?
|
|
1070
1110
|
|
|
1071
|
-
|
|
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
|
|
1072
1116
|
end
|
|
1073
1117
|
|
|
1074
1118
|
# Issue #1051 — the run-scoped plugin disclosures ({Plugin::Base#disclose_once}), the run's
|
|
@@ -1168,10 +1212,23 @@ module Rigor
|
|
|
1168
1212
|
plugin.class.to_s
|
|
1169
1213
|
end
|
|
1170
1214
|
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
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?
|
|
1174
1227
|
|
|
1228
|
+
local.merge(count: local[:count] + pooled[:count])
|
|
1229
|
+
end
|
|
1230
|
+
|
|
1231
|
+
def plugin_trust_refusal_diagnostic(plugin, summary)
|
|
1175
1232
|
plugin_id = plugin.manifest.id
|
|
1176
1233
|
Diagnostic.new(
|
|
1177
1234
|
path: ".rigor.yml", line: 1, column: 1,
|
|
@@ -1258,6 +1315,10 @@ module Rigor
|
|
|
1258
1315
|
@pooled_run_disclosures_reader.call
|
|
1259
1316
|
end
|
|
1260
1317
|
|
|
1318
|
+
def pooled_trust_refusals
|
|
1319
|
+
@pooled_trust_refusals_reader.call
|
|
1320
|
+
end
|
|
1321
|
+
|
|
1261
1322
|
def pre_eval_diagnostics_from_scanner
|
|
1262
1323
|
@pre_eval_diagnostics_from_scanner_reader.call
|
|
1263
1324
|
end
|
|
@@ -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: -> { {} }
|
|
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,6 +91,15 @@ module Rigor
|
|
|
84
91
|
# inherits the same compiled bytes.
|
|
85
92
|
@template_units_reader = template_units
|
|
86
93
|
@analyze_file = analyze_file
|
|
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.
|
|
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
|
|
87
103
|
# Issues #796 / #794 — the previous full run's run-level rows, handed down by
|
|
88
104
|
# {Analysis::IncrementalSession} on a NARROWED run only (a recheck's closure, a
|
|
89
105
|
# `--verify-incremental` partition). A full run leaves this nil and derives everything itself.
|
|
@@ -119,6 +135,27 @@ module Rigor
|
|
|
119
135
|
end
|
|
120
136
|
end
|
|
121
137
|
|
|
138
|
+
# Issue #1900 — the trust refusals a pool run collected, one `{plugin_id:, count:, first_path:,
|
|
139
|
+
# nearest_root:}` per plugin, in first-merge order. Empty on a sequential run, where the runner's own
|
|
140
|
+
# registry made every read.
|
|
141
|
+
def collected_trust_refusals
|
|
142
|
+
@collected_trust_refusals.values
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Issue #1900 — folds one session's / one worker payload's refusal drain in: counts add, and the
|
|
146
|
+
# first path stays the one merged first. Callers merge in the order `--workers 0` would have made the
|
|
147
|
+
# reads — the parent's (or one worker's) `#prepare`, then the files in analysis order — so the summed
|
|
148
|
+
# row names the same first path the sequential run does, except where a drain's own first path is
|
|
149
|
+
# not this merge's to choose (see {DiagnosticAggregator#plugin_trust_refusal_diagnostics}).
|
|
150
|
+
def merge_trust_refusals(records)
|
|
151
|
+
Array(records).each do |record|
|
|
152
|
+
plugin_id = record[:plugin_id]
|
|
153
|
+
existing = @collected_trust_refusals[plugin_id]
|
|
154
|
+
@collected_trust_refusals[plugin_id] =
|
|
155
|
+
existing ? existing.merge(count: existing[:count] + record[:count]) : record
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
122
159
|
# ADR-15 Phase 4b — pool mode is enabled when `@workers > 0`. Editor mode (`buffer:` non-nil)
|
|
123
160
|
# silently overrides pool mode to sequential: per design § "Ractor pool mode", the pool's warm-up
|
|
124
161
|
# cost dominates one-file wall time, so the pool gains nothing on a per-buffer invocation. The
|
|
@@ -146,10 +183,8 @@ module Rigor
|
|
|
146
183
|
# (`expansion.fetch(:files)`), independent of any `analyze_only` narrowing of `files`; see
|
|
147
184
|
# {#serve_empty_closure} for what an empty `files` does with it.
|
|
148
185
|
def analyze_files(files, environment: nil, project_files: nil)
|
|
149
|
-
if files.empty?
|
|
150
|
-
|
|
151
|
-
return []
|
|
152
|
-
end
|
|
186
|
+
return serve_empty_closure(environment, project_files) if files.empty?
|
|
187
|
+
|
|
153
188
|
# Issue #784 / #793 — `files` is what this run ANALYSES; `source_files` is what its environment
|
|
154
189
|
# is BUILT over, and the two are the same only on a full run. A narrowed run (`analyze_only`: an
|
|
155
190
|
# incremental closure, a `--verify-incremental` partition) used to build its environment over the
|
|
@@ -199,7 +234,16 @@ module Rigor
|
|
|
199
234
|
# whether anyone could have demanded anything at all: with no files, nobody could have, and an
|
|
200
235
|
# empty project keeps paying no env build. Keyed on the project's files rather than on
|
|
201
236
|
# `analyze_only`, because a recheck over an EMPTY project narrows to `Set[]`, which is non-nil.
|
|
237
|
+
#
|
|
238
|
+
# Issue #1541 — and, in pool mode, its `prepare` rows. A pooled run's `#prepare` happens inside the
|
|
239
|
+
# workers and its rows reach the stream through their first payload, so the coordinator-side
|
|
240
|
+
# pre-pass skips it ({ProjectPrePasses#run}) and {DiagnosticAggregator#pre_file_diagnostics} omits
|
|
241
|
+
# the cached rows; with no worker dispatched the row appeared nowhere, and a plugin whose `#prepare`
|
|
242
|
+
# raises turned a red project green on a nothing-changed `--incremental --workers N` recheck. They
|
|
243
|
+
# are returned as the run's per-file stream head, where the pool path puts them (`.rigor.yml`).
|
|
244
|
+
# Sequential, whose pre-pass already ran `#prepare`, returns no rows here.
|
|
202
245
|
def serve_empty_closure(environment, project_files)
|
|
246
|
+
prepare_rows = pool_mode? ? pooled_prepare_rows : []
|
|
203
247
|
env = environment || @environment_override
|
|
204
248
|
# Resolved over the project's OWN file list — not `[]`, which would drop every plugin-synthesized
|
|
205
249
|
# virtual RBS (`Environment.collect_virtual_rbs` short-circuits on an empty list) and scan a
|
|
@@ -217,9 +261,17 @@ module Rigor
|
|
|
217
261
|
# `env` rather than `resolve.call`: read the demand the snapshots above just made when they built
|
|
218
262
|
# an environment, and force nothing when they did not.
|
|
219
263
|
record_definition_build_failures(env&.rbs_loader&.definition_build_failures)
|
|
220
|
-
|
|
264
|
+
record_hkt_scan_failure(hkt_scan_outcome(resolve.call)) unless replayed_restored_run_level_rows?
|
|
265
|
+
prepare_rows
|
|
266
|
+
end
|
|
221
267
|
|
|
222
|
-
|
|
268
|
+
# Issue #1541 — the pooled empty closure's `#prepare`: the rows go to the caller, the run-scoped
|
|
269
|
+
# registrations into the table the workers' payloads would have filled.
|
|
270
|
+
def pooled_prepare_rows
|
|
271
|
+
rows, records, refusals = @plugin_prepare.call
|
|
272
|
+
merge_run_disclosures(records)
|
|
273
|
+
merge_trust_refusals(refusals)
|
|
274
|
+
Array(rows)
|
|
223
275
|
end
|
|
224
276
|
|
|
225
277
|
# Issue #794 — the signature-state snapshot's own gates, asked BEFORE an environment exists. Every
|
|
@@ -621,13 +673,15 @@ module Rigor
|
|
|
621
673
|
required_features: features
|
|
622
674
|
)
|
|
623
675
|
main = Ractor.main
|
|
624
|
-
|
|
676
|
+
# Issue #1900 — each message carries the refusals made since the previous one, so the coordinator
|
|
677
|
+
# can count ONE worker's `#prepare` (every worker repeats it) and sum the files in file order.
|
|
678
|
+
main.send([:prepare, session.prepare_diagnostics, session.drain_trust_refusals])
|
|
625
679
|
|
|
626
680
|
loop do
|
|
627
681
|
msg = Ractor.receive
|
|
628
682
|
break if msg.nil?
|
|
629
683
|
|
|
630
|
-
main.send([:file, msg, session.analyze(msg)])
|
|
684
|
+
main.send([:file, msg, session.analyze(msg), session.drain_trust_refusals])
|
|
631
685
|
end
|
|
632
686
|
|
|
633
687
|
# Issue #1051 — sent with `:done`, not with `:prepare`: this backend builds no parent session,
|
|
@@ -646,6 +700,8 @@ module Rigor
|
|
|
646
700
|
pool.each { |worker| worker.send(nil) }
|
|
647
701
|
|
|
648
702
|
prepare_diagnostics = nil
|
|
703
|
+
prepare_refusals = nil
|
|
704
|
+
refusals_by_path = {}
|
|
649
705
|
results_by_path = {}
|
|
650
706
|
terminated = 0
|
|
651
707
|
|
|
@@ -663,9 +719,11 @@ module Rigor
|
|
|
663
719
|
|
|
664
720
|
case message.first
|
|
665
721
|
when :prepare
|
|
666
|
-
prepare_diagnostics ||= message
|
|
722
|
+
prepare_diagnostics ||= message[1]
|
|
723
|
+
prepare_refusals ||= message[2]
|
|
667
724
|
when :file
|
|
668
725
|
results_by_path[message[1]] = message[2]
|
|
726
|
+
refusals_by_path[message[1]] = message[3]
|
|
669
727
|
when :done
|
|
670
728
|
merge_worker_reporters(message[1])
|
|
671
729
|
merge_run_disclosures(message[2])
|
|
@@ -684,6 +742,8 @@ module Rigor
|
|
|
684
742
|
# it did. Re-analysed in process, exactly as the fork backend re-analyses a dead child's slice.
|
|
685
743
|
degraded = files.reject { |path| results_by_path.key?(path) }
|
|
686
744
|
reanalyze_degraded_in_process(degraded, results_by_path, source_files: source_files)
|
|
745
|
+
merge_trust_refusals(prepare_refusals)
|
|
746
|
+
files.each { |path| merge_trust_refusals(refusals_by_path[path]) }
|
|
687
747
|
|
|
688
748
|
diagnostics = Array(prepare_diagnostics) + files.flat_map { |path| results_by_path.fetch(path, []) }
|
|
689
749
|
degraded.empty? ? diagnostics : diagnostics.unshift(pool_degraded_diagnostic(degraded.size, "ractor"))
|
|
@@ -700,8 +760,9 @@ module Rigor
|
|
|
700
760
|
# Issue #1051, known exception: run-scoped disclosures a plugin registered from `#prepare` are NOT
|
|
701
761
|
# recovered here — nor, since #1060, a positioned `#emit_once` batch registered there, which rides
|
|
702
762
|
# the same table. There is no session to drain — this backend builds none on the coordinator — and
|
|
703
|
-
# `#prepare`
|
|
704
|
-
#
|
|
763
|
+
# `#prepare` does not run on the coordinator-side registry under pool mode (only an empty closure
|
|
764
|
+
# runs it, on a throwaway registry; see {#serve_empty_closure}), so a prepare-time disclosure lives
|
|
765
|
+
# only inside the Ractor that died with it. A disclosure registered
|
|
705
766
|
# from `#diagnostics_for_file` IS recovered, because the re-analysis below runs the coordinator-side
|
|
706
767
|
# plugin instances that {DiagnosticAggregator#plugin_run_disclosure_diagnostics} reads directly.
|
|
707
768
|
# Left as is: recovering the rest means running `#prepare` on the coordinator registry, which would
|
|
@@ -776,23 +837,34 @@ module Rigor
|
|
|
776
837
|
# `merge_run_disclosures` drops it as a duplicate; only a disclosure a worker registers while
|
|
777
838
|
# analysing its own slice adds anything.
|
|
778
839
|
merge_run_disclosures(session.drain_run_disclosures)
|
|
840
|
+
# Issue #1900 — the parent's `#prepare` refusals, drained BEFORE the fork: every child inherits the
|
|
841
|
+
# session's record of what it already reported and drains only its own slice's, so the prepare-time
|
|
842
|
+
# refusal is counted once, as `--workers 0` counts it, rather than once per worker.
|
|
843
|
+
merge_trust_refusals(session.drain_trust_refusals)
|
|
779
844
|
snapshot_fork_pool_stats(session) if @collect_stats
|
|
780
845
|
|
|
781
846
|
worker_count = [@workers, files.size].min
|
|
782
847
|
slices = files.each_slice((files.size.to_f / worker_count).ceil).to_a
|
|
783
848
|
results_by_path = {}
|
|
849
|
+
# Issue #1900 — each surviving child's refusal drain by slice index, and each re-analysed file's by
|
|
850
|
+
# path, merged below in file order so the first refused path is the one `--workers 0` names.
|
|
851
|
+
refusals_by_slice = {}
|
|
852
|
+
refusals_by_path = {}
|
|
784
853
|
|
|
785
854
|
degraded = Dir.mktmpdir("rigor-fork-pool") do |tmpdir|
|
|
786
855
|
children = slices.each_with_index.map do |slice, index|
|
|
787
856
|
out_path = File.join(tmpdir, "worker-#{index}")
|
|
788
857
|
{ pid: fork { run_fork_worker(session, slice, out_path) },
|
|
789
|
-
slice: slice, out_path: out_path }
|
|
858
|
+
slice: slice, out_path: out_path, index: index }
|
|
790
859
|
end
|
|
791
|
-
collect_fork_results(children, results_by_path)
|
|
860
|
+
collect_fork_results(children, results_by_path, refusals_by_slice: refusals_by_slice)
|
|
792
861
|
end
|
|
793
862
|
|
|
794
863
|
unless degraded.empty?
|
|
795
|
-
degraded.each
|
|
864
|
+
degraded.each do |path|
|
|
865
|
+
results_by_path[path] = session.analyze(path)
|
|
866
|
+
refusals_by_path[path] = session.drain_trust_refusals
|
|
867
|
+
end
|
|
796
868
|
merge_worker_reporters(session.drain_reporters)
|
|
797
869
|
# The degraded slice ran on the parent session, so a disclosure it registered lives there.
|
|
798
870
|
merge_run_disclosures(session.drain_run_disclosures)
|
|
@@ -802,11 +874,25 @@ module Rigor
|
|
|
802
874
|
@collected_effects.merge!(session.drain_effects) if @record_effects
|
|
803
875
|
end
|
|
804
876
|
|
|
877
|
+
merge_slice_refusals(slices, refusals_by_slice, refusals_by_path)
|
|
805
878
|
diagnostics = Array(session.prepare_diagnostics) +
|
|
806
879
|
files.flat_map { |path| results_by_path.fetch(path, []) }
|
|
807
880
|
degraded.empty? ? diagnostics : diagnostics.unshift(pool_degraded_diagnostic(degraded.size, "fork"))
|
|
808
881
|
end
|
|
809
882
|
|
|
883
|
+
# Issue #1900 — a surviving child's drain covers its whole slice; a degraded slice was re-analysed on
|
|
884
|
+
# the parent and drained file by file. Slices are contiguous runs of `files`, so walking them in index
|
|
885
|
+
# order merges every refusal in the order the sequential run made it.
|
|
886
|
+
def merge_slice_refusals(slices, refusals_by_slice, refusals_by_path)
|
|
887
|
+
slices.each_with_index do |slice, index|
|
|
888
|
+
if refusals_by_slice.key?(index)
|
|
889
|
+
merge_trust_refusals(refusals_by_slice[index])
|
|
890
|
+
else
|
|
891
|
+
slice.each { |path| merge_trust_refusals(refusals_by_path[path]) }
|
|
892
|
+
end
|
|
893
|
+
end
|
|
894
|
+
end
|
|
895
|
+
|
|
810
896
|
# Child-process body for {#analyze_files_in_fork_pool}. Analyses the slice with the
|
|
811
897
|
# copy-on-write-inherited session and writes the Marshal'd payload to `out_path`. `exit!` skips
|
|
812
898
|
# `at_exit` / stdio flush — the payload is already durable on disk by then.
|
|
@@ -821,7 +907,8 @@ module Rigor
|
|
|
821
907
|
results = slice.to_h { |path| [path, session.analyze(path)] }
|
|
822
908
|
payload = { results: results, reporters: session.drain_reporters,
|
|
823
909
|
dependencies: session.drain_dependencies,
|
|
824
|
-
disclosures: session.drain_run_disclosures
|
|
910
|
+
disclosures: session.drain_run_disclosures,
|
|
911
|
+
refusals: session.drain_trust_refusals }
|
|
825
912
|
# ADR-103 WD13 — the effects slot exists only on a collecting run, so a normal `--workers N` run
|
|
826
913
|
# marshals exactly the payload it marshalled before the feature existed.
|
|
827
914
|
payload[:effects] = session.drain_effects if @record_effects
|
|
@@ -845,7 +932,9 @@ module Rigor
|
|
|
845
932
|
|
|
846
933
|
# Waits for every forked child, merges each successful payload into `results_by_path`, and returns
|
|
847
934
|
# the file paths whose worker exited abnormally (for in-process degrade).
|
|
848
|
-
|
|
935
|
+
# @param refusals_by_slice — issue #1900 — receives each successful payload's trust-refusal drain
|
|
936
|
+
# under the child's slice index, for the caller to merge in file order; nil merges it at once.
|
|
937
|
+
def collect_fork_results(children, results_by_path, refusals_by_slice: nil)
|
|
849
938
|
degraded = []
|
|
850
939
|
children.each do |child|
|
|
851
940
|
_, status = Process.waitpid2(child[:pid])
|
|
@@ -854,6 +943,8 @@ module Rigor
|
|
|
854
943
|
results_by_path.merge!(payload.fetch(:results))
|
|
855
944
|
merge_worker_reporters(payload.fetch(:reporters))
|
|
856
945
|
merge_run_disclosures(payload.fetch(:disclosures, []))
|
|
946
|
+
refusals = payload.fetch(:refusals, [])
|
|
947
|
+
refusals_by_slice ? refusals_by_slice[child[:index]] = refusals : merge_trust_refusals(refusals)
|
|
857
948
|
@collected_dependencies.merge!(payload.fetch(:dependencies, {})) if @record_dependencies
|
|
858
949
|
@collected_effects.merge!(payload.fetch(:effects, {})) if @record_effects
|
|
859
950
|
else
|
|
@@ -938,7 +1029,15 @@ module Rigor
|
|
|
938
1029
|
# @param source_files — issue #793 — the whole project, so this path's environment
|
|
939
1030
|
# carries the same plugin-synthesized RBS the pool workers' would. It used to build over `[]`,
|
|
940
1031
|
# i.e. with no synthesized RBS at all, even on a full run.
|
|
1032
|
+
#
|
|
1033
|
+
# Issue #1900 — it also runs `#prepare` on the coordinator-side registry it analyses with, BEFORE the
|
|
1034
|
+
# environment build as the sequential pre-pass does. A pooled run's pre-pass skips `#prepare` (the
|
|
1035
|
+
# workers run it), and this path has no workers, so every plugin used to analyse unprepared and its
|
|
1036
|
+
# prepare rows and prepare-time trust refusals were lost. The rows head the stream, where the pool
|
|
1037
|
+
# backends put their workers' prepare rows; the refusals land on the coordinator's own
|
|
1038
|
+
# {Plugin::IoBoundary}s, which {DiagnosticAggregator#plugin_trust_refusal_diagnostics} reads.
|
|
941
1039
|
def analyze_files_sequentially_fallback(files, reason:, source_files: files)
|
|
1040
|
+
prepare_rows = prepare_coordinator_plugins_once
|
|
942
1041
|
environment = build_runner_environment(source_files: source_files)
|
|
943
1042
|
# Same set, same ORDER as `#analyze_files_sequentially`. The signature state used to be hand-copied
|
|
944
1043
|
# here slot by slot, which dropped `synthesized-namespace` and every conformance row. The conformance
|
|
@@ -946,7 +1045,7 @@ module Rigor
|
|
|
946
1045
|
# BEFORE the definition-build failures are read below, or a failure only that scan reaches is lost.
|
|
947
1046
|
snapshot_project_signature_state(environment)
|
|
948
1047
|
snapshot_effect_annotation_carrier(environment.rbs_loader)
|
|
949
|
-
diagnostics = files.flat_map { |path| @analyze_file.call(path, environment) }
|
|
1048
|
+
diagnostics = prepare_rows + files.flat_map { |path| @analyze_file.call(path, environment) }
|
|
950
1049
|
loader = environment.rbs_loader
|
|
951
1050
|
# Issue #696 — this path analyses on the coordinator, so its loader IS the one that reached the
|
|
952
1051
|
# failing build, and it must snapshot the same slot the two real paths do. Reached whenever
|
|
@@ -968,6 +1067,15 @@ module Rigor
|
|
|
968
1067
|
)
|
|
969
1068
|
end
|
|
970
1069
|
|
|
1070
|
+
# Issue #1900 — `#prepare` on the coordinator-side registry, at most once per run: a second call would
|
|
1071
|
+
# re-publish every cross-plugin fact into the shared fact store.
|
|
1072
|
+
def prepare_coordinator_plugins_once
|
|
1073
|
+
return [] if @coordinator_plugins_prepared
|
|
1074
|
+
|
|
1075
|
+
@coordinator_plugins_prepared = true
|
|
1076
|
+
Array(@prepare_coordinator_plugins.call)
|
|
1077
|
+
end
|
|
1078
|
+
|
|
971
1079
|
def merge_worker_reporters(drained)
|
|
972
1080
|
merge_rbs_extended_reporter(drained.fetch(:rbs_extended))
|
|
973
1081
|
drained.fetch(:boundary_cross).each do |entry|
|
|
@@ -1062,8 +1170,14 @@ module Rigor
|
|
|
1062
1170
|
def record_definition_build_failures(failures)
|
|
1063
1171
|
return if failures.nil? || failures.empty?
|
|
1064
1172
|
|
|
1173
|
+
# Issue #1902 — one tuple per class, chosen by {Environment::RbsLoader.definition_build_detail_order}
|
|
1174
|
+
# rather than by arrival: a fresh record and a replayed one (or two workers' records) for the same
|
|
1175
|
+
# class can name different failures, and the earlier one used to win.
|
|
1065
1176
|
@snapshots.definition_build_failures =
|
|
1066
|
-
(@snapshots.definition_build_failures + failures)
|
|
1177
|
+
(@snapshots.definition_build_failures + failures)
|
|
1178
|
+
.group_by(&:first)
|
|
1179
|
+
.map { |_, candidates| candidates.min_by { |c| Environment::RbsLoader.definition_build_detail_order(c) } }
|
|
1180
|
+
.freeze
|
|
1067
1181
|
end
|
|
1068
1182
|
|
|
1069
1183
|
# Issue #784 — first-wins, unlike {#record_definition_build_failures}'s accumulate-and-dedup. That
|