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
|
@@ -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.
|
|
@@ -123,6 +135,27 @@ module Rigor
|
|
|
123
135
|
end
|
|
124
136
|
end
|
|
125
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
|
+
|
|
126
159
|
# ADR-15 Phase 4b — pool mode is enabled when `@workers > 0`. Editor mode (`buffer:` non-nil)
|
|
127
160
|
# silently overrides pool mode to sequential: per design § "Ractor pool mode", the pool's warm-up
|
|
128
161
|
# cost dominates one-file wall time, so the pool gains nothing on a per-buffer invocation. The
|
|
@@ -235,8 +268,9 @@ module Rigor
|
|
|
235
268
|
# Issue #1541 — the pooled empty closure's `#prepare`: the rows go to the caller, the run-scoped
|
|
236
269
|
# registrations into the table the workers' payloads would have filled.
|
|
237
270
|
def pooled_prepare_rows
|
|
238
|
-
rows, records = @plugin_prepare.call
|
|
271
|
+
rows, records, refusals = @plugin_prepare.call
|
|
239
272
|
merge_run_disclosures(records)
|
|
273
|
+
merge_trust_refusals(refusals)
|
|
240
274
|
Array(rows)
|
|
241
275
|
end
|
|
242
276
|
|
|
@@ -639,13 +673,15 @@ module Rigor
|
|
|
639
673
|
required_features: features
|
|
640
674
|
)
|
|
641
675
|
main = Ractor.main
|
|
642
|
-
|
|
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])
|
|
643
679
|
|
|
644
680
|
loop do
|
|
645
681
|
msg = Ractor.receive
|
|
646
682
|
break if msg.nil?
|
|
647
683
|
|
|
648
|
-
main.send([:file, msg, session.analyze(msg)])
|
|
684
|
+
main.send([:file, msg, session.analyze(msg), session.drain_trust_refusals])
|
|
649
685
|
end
|
|
650
686
|
|
|
651
687
|
# Issue #1051 — sent with `:done`, not with `:prepare`: this backend builds no parent session,
|
|
@@ -664,6 +700,8 @@ module Rigor
|
|
|
664
700
|
pool.each { |worker| worker.send(nil) }
|
|
665
701
|
|
|
666
702
|
prepare_diagnostics = nil
|
|
703
|
+
prepare_refusals = nil
|
|
704
|
+
refusals_by_path = {}
|
|
667
705
|
results_by_path = {}
|
|
668
706
|
terminated = 0
|
|
669
707
|
|
|
@@ -681,9 +719,11 @@ module Rigor
|
|
|
681
719
|
|
|
682
720
|
case message.first
|
|
683
721
|
when :prepare
|
|
684
|
-
prepare_diagnostics ||= message
|
|
722
|
+
prepare_diagnostics ||= message[1]
|
|
723
|
+
prepare_refusals ||= message[2]
|
|
685
724
|
when :file
|
|
686
725
|
results_by_path[message[1]] = message[2]
|
|
726
|
+
refusals_by_path[message[1]] = message[3]
|
|
687
727
|
when :done
|
|
688
728
|
merge_worker_reporters(message[1])
|
|
689
729
|
merge_run_disclosures(message[2])
|
|
@@ -702,6 +742,8 @@ module Rigor
|
|
|
702
742
|
# it did. Re-analysed in process, exactly as the fork backend re-analyses a dead child's slice.
|
|
703
743
|
degraded = files.reject { |path| results_by_path.key?(path) }
|
|
704
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]) }
|
|
705
747
|
|
|
706
748
|
diagnostics = Array(prepare_diagnostics) + files.flat_map { |path| results_by_path.fetch(path, []) }
|
|
707
749
|
degraded.empty? ? diagnostics : diagnostics.unshift(pool_degraded_diagnostic(degraded.size, "ractor"))
|
|
@@ -795,23 +837,34 @@ module Rigor
|
|
|
795
837
|
# `merge_run_disclosures` drops it as a duplicate; only a disclosure a worker registers while
|
|
796
838
|
# analysing its own slice adds anything.
|
|
797
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)
|
|
798
844
|
snapshot_fork_pool_stats(session) if @collect_stats
|
|
799
845
|
|
|
800
846
|
worker_count = [@workers, files.size].min
|
|
801
847
|
slices = files.each_slice((files.size.to_f / worker_count).ceil).to_a
|
|
802
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 = {}
|
|
803
853
|
|
|
804
854
|
degraded = Dir.mktmpdir("rigor-fork-pool") do |tmpdir|
|
|
805
855
|
children = slices.each_with_index.map do |slice, index|
|
|
806
856
|
out_path = File.join(tmpdir, "worker-#{index}")
|
|
807
857
|
{ pid: fork { run_fork_worker(session, slice, out_path) },
|
|
808
|
-
slice: slice, out_path: out_path }
|
|
858
|
+
slice: slice, out_path: out_path, index: index }
|
|
809
859
|
end
|
|
810
|
-
collect_fork_results(children, results_by_path)
|
|
860
|
+
collect_fork_results(children, results_by_path, refusals_by_slice: refusals_by_slice)
|
|
811
861
|
end
|
|
812
862
|
|
|
813
863
|
unless degraded.empty?
|
|
814
|
-
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
|
|
815
868
|
merge_worker_reporters(session.drain_reporters)
|
|
816
869
|
# The degraded slice ran on the parent session, so a disclosure it registered lives there.
|
|
817
870
|
merge_run_disclosures(session.drain_run_disclosures)
|
|
@@ -821,11 +874,25 @@ module Rigor
|
|
|
821
874
|
@collected_effects.merge!(session.drain_effects) if @record_effects
|
|
822
875
|
end
|
|
823
876
|
|
|
877
|
+
merge_slice_refusals(slices, refusals_by_slice, refusals_by_path)
|
|
824
878
|
diagnostics = Array(session.prepare_diagnostics) +
|
|
825
879
|
files.flat_map { |path| results_by_path.fetch(path, []) }
|
|
826
880
|
degraded.empty? ? diagnostics : diagnostics.unshift(pool_degraded_diagnostic(degraded.size, "fork"))
|
|
827
881
|
end
|
|
828
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
|
+
|
|
829
896
|
# Child-process body for {#analyze_files_in_fork_pool}. Analyses the slice with the
|
|
830
897
|
# copy-on-write-inherited session and writes the Marshal'd payload to `out_path`. `exit!` skips
|
|
831
898
|
# `at_exit` / stdio flush — the payload is already durable on disk by then.
|
|
@@ -840,7 +907,8 @@ module Rigor
|
|
|
840
907
|
results = slice.to_h { |path| [path, session.analyze(path)] }
|
|
841
908
|
payload = { results: results, reporters: session.drain_reporters,
|
|
842
909
|
dependencies: session.drain_dependencies,
|
|
843
|
-
disclosures: session.drain_run_disclosures
|
|
910
|
+
disclosures: session.drain_run_disclosures,
|
|
911
|
+
refusals: session.drain_trust_refusals }
|
|
844
912
|
# ADR-103 WD13 — the effects slot exists only on a collecting run, so a normal `--workers N` run
|
|
845
913
|
# marshals exactly the payload it marshalled before the feature existed.
|
|
846
914
|
payload[:effects] = session.drain_effects if @record_effects
|
|
@@ -864,7 +932,9 @@ module Rigor
|
|
|
864
932
|
|
|
865
933
|
# Waits for every forked child, merges each successful payload into `results_by_path`, and returns
|
|
866
934
|
# the file paths whose worker exited abnormally (for in-process degrade).
|
|
867
|
-
|
|
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)
|
|
868
938
|
degraded = []
|
|
869
939
|
children.each do |child|
|
|
870
940
|
_, status = Process.waitpid2(child[:pid])
|
|
@@ -873,6 +943,8 @@ module Rigor
|
|
|
873
943
|
results_by_path.merge!(payload.fetch(:results))
|
|
874
944
|
merge_worker_reporters(payload.fetch(:reporters))
|
|
875
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)
|
|
876
948
|
@collected_dependencies.merge!(payload.fetch(:dependencies, {})) if @record_dependencies
|
|
877
949
|
@collected_effects.merge!(payload.fetch(:effects, {})) if @record_effects
|
|
878
950
|
else
|
|
@@ -957,7 +1029,15 @@ module Rigor
|
|
|
957
1029
|
# @param source_files — issue #793 — the whole project, so this path's environment
|
|
958
1030
|
# carries the same plugin-synthesized RBS the pool workers' would. It used to build over `[]`,
|
|
959
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.
|
|
960
1039
|
def analyze_files_sequentially_fallback(files, reason:, source_files: files)
|
|
1040
|
+
prepare_rows = prepare_coordinator_plugins_once
|
|
961
1041
|
environment = build_runner_environment(source_files: source_files)
|
|
962
1042
|
# Same set, same ORDER as `#analyze_files_sequentially`. The signature state used to be hand-copied
|
|
963
1043
|
# here slot by slot, which dropped `synthesized-namespace` and every conformance row. The conformance
|
|
@@ -965,7 +1045,7 @@ module Rigor
|
|
|
965
1045
|
# BEFORE the definition-build failures are read below, or a failure only that scan reaches is lost.
|
|
966
1046
|
snapshot_project_signature_state(environment)
|
|
967
1047
|
snapshot_effect_annotation_carrier(environment.rbs_loader)
|
|
968
|
-
diagnostics = files.flat_map { |path| @analyze_file.call(path, environment) }
|
|
1048
|
+
diagnostics = prepare_rows + files.flat_map { |path| @analyze_file.call(path, environment) }
|
|
969
1049
|
loader = environment.rbs_loader
|
|
970
1050
|
# Issue #696 — this path analyses on the coordinator, so its loader IS the one that reached the
|
|
971
1051
|
# failing build, and it must snapshot the same slot the two real paths do. Reached whenever
|
|
@@ -987,6 +1067,15 @@ module Rigor
|
|
|
987
1067
|
)
|
|
988
1068
|
end
|
|
989
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
|
+
|
|
990
1079
|
def merge_worker_reporters(drained)
|
|
991
1080
|
merge_rbs_extended_reporter(drained.fetch(:rbs_extended))
|
|
992
1081
|
drained.fetch(:boundary_cross).each do |entry|
|
|
@@ -1081,8 +1170,14 @@ module Rigor
|
|
|
1081
1170
|
def record_definition_build_failures(failures)
|
|
1082
1171
|
return if failures.nil? || failures.empty?
|
|
1083
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.
|
|
1084
1176
|
@snapshots.definition_build_failures =
|
|
1085
|
-
(@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
|
|
1086
1181
|
end
|
|
1087
1182
|
|
|
1088
1183
|
# 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
|
|
@@ -297,7 +297,8 @@ module Rigor
|
|
|
297
297
|
record_dependencies: false, record_self_calls: false, analyze_only: nil,
|
|
298
298
|
seed_bundles: nil, collect_seed_bundles: false, param_inferred_types: nil,
|
|
299
299
|
discovery_seed: nil, no_tolerated_effects: false,
|
|
300
|
-
restored_run_level_rows: nil, served_per_file_diagnostics: nil,
|
|
300
|
+
restored_run_level_rows: nil, served_per_file_diagnostics: nil, served_effect_collections: nil,
|
|
301
|
+
on_file_indexed: nil)
|
|
301
302
|
@configuration = configuration
|
|
302
303
|
# ADR-113 WD2 — `rigor lens` reads a target's seeded index here: called with `(path, parse_result, scope,
|
|
303
304
|
# index)` for every file this process indexes, after its rules ran and inside the run window, so the reader
|
|
@@ -311,6 +312,10 @@ module Rigor
|
|
|
311
312
|
# narrowed run does not analyse, spliced into this run's per-file stream by
|
|
312
313
|
# {#interleave_served_diagnostics}. Empty on every other run.
|
|
313
314
|
@served_per_file_diagnostics = served_per_file_diagnostics || {}
|
|
315
|
+
# Issue #1898 — `{path => FileCollection}` the {IncrementalSession} serves for the same unanalysed files, so the
|
|
316
|
+
# effect graph this run closes (and the envelope pass judges) spans the whole project, not just the closure.
|
|
317
|
+
# Never part of {#effect_collections_by_path}, which stays what THIS run collected. Empty on every other run.
|
|
318
|
+
@served_effect_collections = served_effect_collections || {}
|
|
314
319
|
@explain = explain
|
|
315
320
|
@cache_store = enforce_read_only_cache(cache_store, buffer)
|
|
316
321
|
@plugin_requirer = plugin_requirer
|
|
@@ -851,7 +856,8 @@ module Rigor
|
|
|
851
856
|
analysis = serve_effect_collections(expansion, environment, key_descriptor, rbs_descriptor)
|
|
852
857
|
computed = false
|
|
853
858
|
diagnostics = @cache_store.fetch_or_validate(
|
|
854
|
-
producer_id: RunCacheKey::RUN_DIAGNOSTICS_PRODUCER_ID,
|
|
859
|
+
producer_id: RunCacheKey::RUN_DIAGNOSTICS_PRODUCER_ID,
|
|
860
|
+
key_descriptor: RunCacheKey.with_roots(key_descriptor, expansion.fetch(:roots)),
|
|
855
861
|
generation_cap: RunCacheKey::GENERATION_CAP
|
|
856
862
|
) do
|
|
857
863
|
computed = true
|
|
@@ -1380,7 +1386,8 @@ module Rigor
|
|
|
1380
1386
|
|
|
1381
1387
|
def build_run_dependency_descriptor(expansion, rbs_descriptor)
|
|
1382
1388
|
entries = analyzed_file_entries(expansion) + discovery_file_entries(expansion) +
|
|
1383
|
-
pre_eval_file_entries + template_unit_file_entries + rbs_descriptor.files
|
|
1389
|
+
pre_eval_file_entries + template_unit_file_entries + rbs_descriptor.files +
|
|
1390
|
+
root_presence_entries(expansion)
|
|
1384
1391
|
# #979 — the signature ROOTS' listings, one glob row each. `rbs_descriptor.files` covers only the
|
|
1385
1392
|
# `.rbs` files that existed while the run read them, so without these a signature file written after
|
|
1386
1393
|
# the run left the slot validating fresh and the warm run answered without it.
|
|
@@ -1391,6 +1398,20 @@ module Rigor
|
|
|
1391
1398
|
Cache::Descriptor.new(files: entries, globs: globs.uniq)
|
|
1392
1399
|
end
|
|
1393
1400
|
|
|
1401
|
+
# #1962 / #1555 — a kind row per path the run's answer depends on the presence and kind of, not only its
|
|
1402
|
+
# bytes. An analysis root: its path errors (`no such file or directory`, `not a Ruby file`) answer to whether
|
|
1403
|
+
# it exists and whether it is a directory or a file, while what a directory root holds is the analysed-file
|
|
1404
|
+
# set the key carries. A `pre_eval:` entry: `pre-eval.file-not-found` answers to whether it is a file (one
|
|
1405
|
+
# that is has its `:stat` row from {#pre_eval_file_entries}). A project signature root, configured or the
|
|
1406
|
+
# auto-detected `sig`: the loader reads a directory and a file differently, and #979's listing rows cover a
|
|
1407
|
+
# file appearing under a root that existed, not the root appearing. A kind row rather than an existence row,
|
|
1408
|
+
# because a directory replaced by a file of the same name leaves an existence row fresh. The `--incremental`
|
|
1409
|
+
# slot records existence rows for the same paths ({#incremental_slot_rows}, {#baseline_dependency_rows}).
|
|
1410
|
+
def root_presence_entries(expansion)
|
|
1411
|
+
(expansion.fetch(:roots) + @configuration.pre_eval + signature_root_candidates)
|
|
1412
|
+
.uniq.map { |path| Cache::Descriptor::FileEntry.kind(path: path) }
|
|
1413
|
+
end
|
|
1414
|
+
|
|
1394
1415
|
# Every file a plugin read or probed through its `IoBoundary` (#577 / #613), and every directory it listed
|
|
1395
1416
|
# (#954), as the boundaries stand when asked.
|
|
1396
1417
|
def plugin_boundary_rows
|
|
@@ -1580,7 +1601,7 @@ module Rigor
|
|
|
1580
1601
|
end
|
|
1581
1602
|
end
|
|
1582
1603
|
end
|
|
1583
|
-
private :plugin_boundary_rows, :producer_watch_glob_entries, :existence_entries,
|
|
1604
|
+
private :plugin_boundary_rows, :producer_watch_glob_entries, :existence_entries, :root_presence_entries,
|
|
1584
1605
|
:analysis_reads_attributed?, :signature_root_candidates, :discovery_glob_entries,
|
|
1585
1606
|
:unanalysed_pre_eval_entries, :signature_rows, :within?
|
|
1586
1607
|
|
|
@@ -1907,8 +1928,10 @@ module Rigor
|
|
|
1907
1928
|
# The run's per-file effect collections in sorted path order — the sequential half merged with the
|
|
1908
1929
|
# pool's, exactly as {#file_dependencies} reconciles its two halves. Sorted, because the fold below
|
|
1909
1930
|
# it must not depend on pool-completion order.
|
|
1931
|
+
# Issue #1898 — a narrowed incremental run adds the collections its session served for the files it did not
|
|
1932
|
+
# analyse, under this run's own for any path both name.
|
|
1910
1933
|
def effect_collections
|
|
1911
|
-
effect_collections_by_path.sort_by { |path, _| path.to_s }.map(&:last)
|
|
1934
|
+
@served_effect_collections.merge(effect_collections_by_path).sort_by { |path, _| path.to_s }.map(&:last)
|
|
1912
1935
|
end
|
|
1913
1936
|
|
|
1914
1937
|
# Editor mode § "Scope choice — option A". Under `buffer:` non-nil the per-file analysis emits
|
|
@@ -2024,7 +2047,8 @@ module Rigor
|
|
|
2024
2047
|
analyze_file: ->(path, environment) { analyze_file(path, environment) },
|
|
2025
2048
|
restored_run_level_rows: @restored_run_level_rows,
|
|
2026
2049
|
feature_sources: -> { required_feature_sources },
|
|
2027
|
-
plugin_prepare: -> { @pre_passes.prepare_throwaway_registry }
|
|
2050
|
+
plugin_prepare: -> { @pre_passes.prepare_throwaway_registry },
|
|
2051
|
+
prepare_coordinator_plugins: -> { @pre_passes.prepare_registry(@plugin_registry) }
|
|
2028
2052
|
)
|
|
2029
2053
|
@diagnostic_aggregator = DiagnosticAggregator.new(
|
|
2030
2054
|
configuration: @configuration,
|
|
@@ -2044,7 +2068,8 @@ module Rigor
|
|
|
2044
2068
|
definition_build_failures_snapshot: -> { @snapshots.definition_build_failures },
|
|
2045
2069
|
hkt_scan_failure_snapshot: -> { @snapshots.hkt_scan_failure },
|
|
2046
2070
|
conformance_results_snapshot: -> { @snapshots.conformance_results },
|
|
2047
|
-
pooled_run_disclosures: -> { @pool_coordinator.collected_run_disclosures }
|
|
2071
|
+
pooled_run_disclosures: -> { @pool_coordinator.collected_run_disclosures },
|
|
2072
|
+
pooled_trust_refusals: -> { @pool_coordinator.collected_trust_refusals }
|
|
2048
2073
|
)
|
|
2049
2074
|
end
|
|
2050
2075
|
|
|
@@ -2197,7 +2222,7 @@ module Rigor
|
|
|
2197
2222
|
bad << [path, "no such file or directory"]
|
|
2198
2223
|
end
|
|
2199
2224
|
end
|
|
2200
|
-
{ files: files, errors: path_expansion_errors(bad, any_files: files.any?) }
|
|
2225
|
+
{ files: files, roots: Array(paths).map(&:to_s), errors: path_expansion_errors(bad, any_files: files.any?) }
|
|
2201
2226
|
end
|
|
2202
2227
|
|
|
2203
2228
|
# A bad path *among valid ones* is a warn-and-skip — the run still does useful work, so `rigor check
|
|
@@ -2654,7 +2679,8 @@ module Rigor
|
|
|
2654
2679
|
node_collectors: node_collectors,
|
|
2655
2680
|
stated_runtime_ruby: @configuration.stated_runtime_ruby
|
|
2656
2681
|
)
|
|
2657
|
-
|
|
2682
|
+
plugin_rows = plugin_emitted_diagnostics(path, root, scope, node_results)
|
|
2683
|
+
diagnostics + CheckRules.filter_plugin_suppressed(plugin_rows, path: path, comments: parse_result.comments)
|
|
2658
2684
|
end
|
|
2659
2685
|
|
|
2660
2686
|
# ADR-24 slice 4a — runs `block` (the typing pass) with the self-call recorder active when either
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "check_rules/rule_ids"
|
|
3
4
|
require_relative "diagnostic"
|
|
4
5
|
require_relative "../configuration/severity_profile"
|
|
5
6
|
|
|
@@ -9,14 +10,46 @@ module Rigor
|
|
|
9
10
|
# overrides and drops any diagnostic resolving to `:off`. Rules emit with their authored severity; the
|
|
10
11
|
# profile is the final filter.
|
|
11
12
|
#
|
|
13
|
+
# Issue #1895 — the `.rigor.yml` `disable:` list is applied here too, ahead of the profile, with the same
|
|
14
|
+
# token expansion and match per-file analysis uses ({CheckRules.config_disabled?}), but only to a built-in
|
|
15
|
+
# check-rule row (see {.check_rule_row?}): the rows the diagnostic policy already says `disable:` reaches. Most
|
|
16
|
+
# are filtered where they were produced; this catches the ones that are not (the `effect.annotations-unchecked`
|
|
17
|
+
# residual under `disable: [all]`) on every run shape (sequential, pool, `--incremental`, a warm cache hit).
|
|
18
|
+
# Issue #1953 — a plugin's `plugin.<id>.*` row answers here to its qualified id (`plugin.<id>.<rule>`) or to
|
|
19
|
+
# `plugin.<id>`, never to a bare id or `all`. The run-level rows outside the check-rule catalogue
|
|
20
|
+
# (`rbs.coverage.*`, `dynamic.dependency-source.*`, the plugin loader, `plugin_trust.*`, `pool-degraded`) keep
|
|
21
|
+
# answering to `severity_overrides:` only, by any spelling.
|
|
22
|
+
#
|
|
12
23
|
# Extracted (ADR-87 WD4) so the miss path ({Runner::DiagnosticAggregator}) and the boot-slimming hit path
|
|
13
24
|
# ({RunCacheProbe}, which never loads the aggregator) apply the IDENTICAL final filter — a warm HIT's
|
|
14
25
|
# served diagnostics are byte-identical to what a full run would print.
|
|
15
26
|
module SeverityStamp
|
|
16
27
|
module_function
|
|
17
28
|
|
|
29
|
+
CHECK_RULES = CheckRules::ALL_RULES.to_set.freeze
|
|
30
|
+
private_constant :CHECK_RULES
|
|
31
|
+
|
|
18
32
|
def apply(diagnostics, configuration)
|
|
19
|
-
|
|
33
|
+
disabled = CheckRules.expand_rule_tokens(configuration.disabled_rules)
|
|
34
|
+
diagnostics.filter_map do |diagnostic|
|
|
35
|
+
next nil if config_disabled_row?(diagnostic, disabled)
|
|
36
|
+
|
|
37
|
+
stamp(diagnostic, configuration)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# A built-in check-rule row answers to the expanded list by its id or `all`; a plugin's `plugin.<id>.*` row
|
|
42
|
+
# by its qualified id or `plugin.<id>` only ({CheckRules.plugin_row_disabled?}, issue #1953). Every other
|
|
43
|
+
# row is out of the list's reach.
|
|
44
|
+
def config_disabled_row?(diagnostic, disabled)
|
|
45
|
+
return CheckRules.config_disabled?(diagnostic.rule, disabled) if check_rule_row?(diagnostic)
|
|
46
|
+
|
|
47
|
+
CheckRules.plugin_row_disabled?(diagnostic, disabled)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# A built-in check-rule row: its id is in the CheckRules catalogue and no plugin emitted it.
|
|
51
|
+
def check_rule_row?(diagnostic)
|
|
52
|
+
diagnostic.source_family == Diagnostic::DEFAULT_SOURCE_FAMILY && CHECK_RULES.include?(diagnostic.rule)
|
|
20
53
|
end
|
|
21
54
|
|
|
22
55
|
def stamp(diagnostic, configuration)
|
|
@@ -241,7 +241,8 @@ module Rigor
|
|
|
241
241
|
node_collectors: node_collectors,
|
|
242
242
|
stated_runtime_ruby: @configuration.stated_runtime_ruby
|
|
243
243
|
)
|
|
244
|
-
|
|
244
|
+
plugin_rows = plugin_emitted_diagnostics(path, root, scope, node_results)
|
|
245
|
+
diagnostics += CheckRules.filter_plugin_suppressed(plugin_rows, path: path, comments: parse_result.comments)
|
|
245
246
|
diagnostics + explain_diagnostics(path, parse_result.value, scope)
|
|
246
247
|
rescue Errno::ENOENT => e
|
|
247
248
|
[analyzer_error(path, e.message)]
|
|
@@ -336,6 +337,45 @@ module Rigor
|
|
|
336
337
|
end
|
|
337
338
|
end
|
|
338
339
|
|
|
340
|
+
# Issue #1900 — `{plugin id => refused-read count}` over `registry`'s boundaries right now: the baseline
|
|
341
|
+
# a later drain subtracts, so it reports only the refusals made after the baseline was taken.
|
|
342
|
+
def self.refusal_counts(registry)
|
|
343
|
+
registry.plugins.to_h do |plugin|
|
|
344
|
+
summary = begin
|
|
345
|
+
plugin.io_boundary.refusal_summary
|
|
346
|
+
rescue StandardError
|
|
347
|
+
nil
|
|
348
|
+
end
|
|
349
|
+
[safe_plugin_id(plugin), summary ? summary[:count] : 0]
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def self.safe_plugin_id(plugin)
|
|
354
|
+
plugin.manifest.id
|
|
355
|
+
rescue StandardError
|
|
356
|
+
plugin.class.to_s
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# Issue #1900 — the {Plugin::IoBoundary} trust-policy refusals this session's plugin instances made
|
|
360
|
+
# since the previous drain, one `{plugin_id:, count:, first_path:, nearest_root:}` per plugin that hit
|
|
361
|
+
# any, in registry order. Unlike {#drain_run_disclosures} each drain reports only what is new — the
|
|
362
|
+
# boundary keeps its running total and this session remembers how much of it was already reported —
|
|
363
|
+
# because the coordinator SUMS the drains: a fork child inherits the parent's `#prepare` refusals,
|
|
364
|
+
# and reporting them again would count them once per worker. `first_path` is the boundary's first
|
|
365
|
+
# refusal overall, which is the delta's own first path whenever nothing was reported before it, and
|
|
366
|
+
# the coordinator keeps the first-merged path otherwise. It is not baselined after `#init`, so a plugin
|
|
367
|
+
# whose `#init` refuses differently here than on the coordinator registry can have this session's
|
|
368
|
+
# `#init` path named. Marshal-clean and Ractor-copyable.
|
|
369
|
+
def drain_trust_refusals
|
|
370
|
+
return [] if @plugin_registry.empty?
|
|
371
|
+
|
|
372
|
+
@plugin_registry.plugins.filter_map do |plugin|
|
|
373
|
+
trust_refusal_delta(plugin)
|
|
374
|
+
rescue StandardError
|
|
375
|
+
nil
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
|
|
339
379
|
private
|
|
340
380
|
|
|
341
381
|
# Mirrors {Runner#seed_project_scope}: applies the cross-file pre-pass discovery tables the
|
|
@@ -402,6 +442,10 @@ module Rigor
|
|
|
402
442
|
def run_plugin_prepare
|
|
403
443
|
return [] if @plugin_registry.empty?
|
|
404
444
|
|
|
445
|
+
# Issue #1900 — per plugin id, how much of its boundary's count is already reported. Starts at what
|
|
446
|
+
# `#init` refused while the registry was materialised: `#init` runs on the coordinator-side registry
|
|
447
|
+
# too, which reports those reads itself, so counting them here as well would double them.
|
|
448
|
+
@reported_refusal_counts = self.class.refusal_counts(@plugin_registry)
|
|
405
449
|
@plugin_registry.plugins.flat_map do |plugin|
|
|
406
450
|
plugin.prepare(plugin.services)
|
|
407
451
|
[]
|
|
@@ -490,6 +534,18 @@ module Rigor
|
|
|
490
534
|
)
|
|
491
535
|
end
|
|
492
536
|
|
|
537
|
+
def trust_refusal_delta(plugin)
|
|
538
|
+
summary = plugin.io_boundary.refusal_summary
|
|
539
|
+
return nil if summary.nil?
|
|
540
|
+
|
|
541
|
+
plugin_id = safe_plugin_id(plugin)
|
|
542
|
+
delta = summary[:count] - @reported_refusal_counts.fetch(plugin_id, 0)
|
|
543
|
+
return nil unless delta.positive?
|
|
544
|
+
|
|
545
|
+
@reported_refusal_counts[plugin_id] = summary[:count]
|
|
546
|
+
summary.merge(count: delta, plugin_id: plugin_id)
|
|
547
|
+
end
|
|
548
|
+
|
|
493
549
|
def safe_plugin_id(plugin)
|
|
494
550
|
plugin.manifest.id
|
|
495
551
|
rescue StandardError
|