rigortype 0.4.5 → 0.4.6
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/docs/handbook/11-sig-gen.md +11 -2
- data/docs/manual/02-cli-reference.md +46 -7
- data/docs/manual/03-configuration.md +2 -1
- data/docs/manual/04-diagnostics.md +1 -1
- data/docs/manual/10-mcp-server.md +9 -1
- data/docs/manual/12-caching.md +8 -1
- data/docs/manual/plugins/rigor-actionmailer.md +30 -0
- data/docs/manual/plugins/rigor-actionpack.md +30 -0
- data/docs/manual/plugins/rigor-devise.md +15 -0
- data/docs/manual/plugins/rigor-rspec.md +2 -1
- data/lib/rigor/analysis/buffer_binding.rb +14 -0
- data/lib/rigor/analysis/check_rules/inferred_param_guard.rb +30 -9
- data/lib/rigor/analysis/check_rules/ivar_write_collector.rb +142 -15
- data/lib/rigor/analysis/check_rules/lexical_method_sites.rb +4 -0
- data/lib/rigor/analysis/check_rules/rbs_receiver_surface.rb +66 -3
- data/lib/rigor/analysis/check_rules/rebinding_block_callees/dispatch.rb +112 -0
- data/lib/rigor/analysis/check_rules/rebinding_block_callees.rb +394 -0
- data/lib/rigor/analysis/check_rules/ruby_deprecations.rb +7 -0
- data/lib/rigor/analysis/check_rules/unreachable_clause_collector.rb +51 -0
- data/lib/rigor/analysis/check_rules.rb +93 -8
- data/lib/rigor/analysis/dependency_recorder.rb +11 -0
- data/lib/rigor/analysis/edit_replay.rb +67 -0
- data/lib/rigor/analysis/incremental.rb +37 -0
- data/lib/rigor/analysis/incremental_session.rb +175 -27
- data/lib/rigor/analysis/path_expansion.rb +41 -0
- data/lib/rigor/analysis/require_relative_closure.rb +100 -0
- data/lib/rigor/analysis/rule_catalog.rb +5 -1
- data/lib/rigor/analysis/run_disclosure_credit.rb +80 -0
- data/lib/rigor/analysis/run_disclosure_ledger.rb +155 -0
- data/lib/rigor/analysis/runner/buffer_pool_dispatcher.rb +6 -2
- data/lib/rigor/analysis/runner/diagnostic_aggregator.rb +11 -3
- data/lib/rigor/analysis/runner/pool_coordinator.rb +17 -4
- data/lib/rigor/analysis/runner/project_pre_passes.rb +45 -3
- data/lib/rigor/analysis/runner/scan_environment.rb +51 -0
- data/lib/rigor/analysis/runner.rb +156 -49
- data/lib/rigor/analysis/worker_session.rb +14 -2
- data/lib/rigor/cache/incremental_snapshot.rb +30 -7
- data/lib/rigor/cli/check_command.rb +93 -4
- data/lib/rigor/cli/diff_command.rb +2 -1
- data/lib/rigor/cli/lens_command.rb +5 -5
- data/lib/rigor/cli/mcp_command.rb +2 -1
- data/lib/rigor/cli/options.rb +20 -3
- data/lib/rigor/cli/plugins_command.rb +5 -0
- data/lib/rigor/cli/plugins_renderer.rb +24 -2
- data/lib/rigor/cli/show_bleedingedge_command.rb +2 -1
- data/lib/rigor/cli/sig_gen_command.rb +46 -7
- data/lib/rigor/cli/type_of_command.rb +2 -1
- data/lib/rigor/cli/type_of_project_scope.rb +27 -0
- data/lib/rigor/config_audit.rb +16 -5
- data/lib/rigor/configuration.rb +14 -4
- data/lib/rigor/environment/rbs_loader.rb +115 -6
- data/lib/rigor/environment/required_features.rb +136 -29
- data/lib/rigor/environment/ruby_library_roots.rb +122 -0
- data/lib/rigor/environment.rb +4 -2
- data/lib/rigor/inference/callee_mutation_chain.rb +115 -0
- data/lib/rigor/inference/deep_erasure.rb +138 -0
- data/lib/rigor/inference/def_return_typer.rb +159 -9
- data/lib/rigor/inference/definer_resolution.rb +17 -3
- data/lib/rigor/inference/error_info.rb +17 -3
- data/lib/rigor/inference/expression_typer.rb +62 -13
- data/lib/rigor/inference/global_write_census.rb +36 -1
- data/lib/rigor/inference/ivar_writer_census.rb +195 -0
- data/lib/rigor/inference/loop_value_record.rb +53 -0
- data/lib/rigor/inference/match_rebinding/frame.rb +19 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +110 -9
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +6 -2
- data/lib/rigor/inference/method_dispatcher/set_folding.rb +53 -20
- data/lib/rigor/inference/method_dispatcher.rb +46 -16
- data/lib/rigor/inference/module_function_state/spellings.rb +64 -0
- data/lib/rigor/inference/module_function_state.rb +12 -0
- data/lib/rigor/inference/mutation_widening.rb +5 -2
- data/lib/rigor/inference/narrowing.rb +66 -5
- data/lib/rigor/inference/object_mixins.rb +13 -1
- data/lib/rigor/inference/project_seed_tables.rb +2 -1
- data/lib/rigor/inference/project_yielder.rb +128 -0
- data/lib/rigor/inference/scope_indexer/certainty.rb +9 -0
- data/lib/rigor/inference/scope_indexer/declared_ivar_types.rb +64 -0
- data/lib/rigor/inference/scope_indexer/declared_param_writes.rb +162 -0
- data/lib/rigor/inference/scope_indexer/unseen_ivar_writers.rb +133 -0
- data/lib/rigor/inference/scope_indexer.rb +480 -103
- data/lib/rigor/inference/self_call_ivar_writes.rb +18 -6
- data/lib/rigor/inference/statement_evaluator.rb +658 -90
- data/lib/rigor/inference/synthetic_method_index.rb +23 -0
- data/lib/rigor/inference/synthetic_method_scanner/project_members.rb +130 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +72 -12
- data/lib/rigor/language_server/buffer_table.rb +6 -0
- data/lib/rigor/language_server/diagnostic_publisher.rb +20 -6
- data/lib/rigor/language_server/project_context.rb +59 -3
- data/lib/rigor/language_server/server.rb +5 -3
- data/lib/rigor/mcp/server.rb +9 -1
- data/lib/rigor/plugin/base.rb +26 -11
- data/lib/rigor/plugin/manifest.rb +29 -3
- data/lib/rigor/plugin/registry.rb +38 -0
- data/lib/rigor/protection/discovery_seed.rb +57 -2
- data/lib/rigor/rooted_glob.rb +10 -1
- data/lib/rigor/scope.rb +14 -5
- data/lib/rigor/sig_gen/generator.rb +308 -37
- data/lib/rigor/sig_gen/observation_collector.rb +4 -1
- data/lib/rigor/sig_gen/rbs_validity.rb +1 -1
- data/lib/rigor/sig_gen/renderer.rb +6 -3
- data/lib/rigor/sig_gen/type_elaborator.rb +21 -5
- data/lib/rigor/type/combinator.rb +4 -3
- data/lib/rigor/type/constant.rb +45 -3
- data/lib/rigor/type/hash_shape.rb +14 -9
- data/lib/rigor/type/struct_instance.rb +3 -2
- data/lib/rigor/version.rb +1 -1
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer/mailer_discoverer.rb +60 -43
- data/plugins/rigor-actionmailer/lib/rigor/plugin/actionmailer.rb +20 -1
- data/plugins/rigor-actionpack/lib/rigor/plugin/actionpack.rb +13 -0
- data/plugins/rigor-activejob/lib/rigor/plugin/activejob.rb +4 -2
- data/plugins/rigor-devise/lib/rigor/plugin/devise.rb +5 -3
- data/plugins/rigor-dry-types/lib/rigor/plugin/dry_types.rb +6 -5
- data/plugins/rigor-rspec/lib/rigor/plugin/rspec/matcher_analyzer.rb +102 -11
- data/plugins/rigor-sidekiq/lib/rigor/plugin/sidekiq.rb +4 -2
- data/plugins/rigor-typelizer/lib/rigor/plugin/typelizer.rb +1 -1
- data/sig/rigor/analysis/baseline.rbs +3 -2
- data/sig/rigor/effects/file_collection.rbs +5 -6
- data/sig/rigor/effects/registry.rbs +8 -6
- data/sig/rigor/effects/summary.rbs +6 -5
- data/sig/rigor/environment.rbs +5 -3
- data/sig/rigor/inference/optimistic_origin.rbs +11 -0
- data/sig/rigor/inference.rbs +6 -0
- data/sig/rigor/plugin/additional_initializer.rbs +5 -5
- data/sig/rigor/plugin/effect_ancestry.rbs +6 -4
- data/sig/rigor/plugin/effect_attribution.rbs +25 -10
- data/sig/rigor/plugin/effect_edge.rbs +12 -5
- data/sig/rigor/plugin/effect_entry_points.rbs +8 -3
- data/sig/rigor/plugin/io_boundary.rbs +4 -4
- data/sig/rigor/plugin/manifest.rbs +16 -18
- data/sig/rigor/plugin/protocol_contract.rbs +10 -9
- data/sig/rigor/plugin/registry.rbs +1 -1
- data/sig/rigor/scope.rbs +31 -50
- data/sig/rigor/type.rbs +0 -6
- metadata +20 -1
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "run_disclosure_credit"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
module Analysis
|
|
7
|
+
# Issue #2020 — the `--incremental` session's record of the run-scoped plugin rows ({Plugin::Base#disclose_once},
|
|
8
|
+
# {Plugin::Base#emit_once}) each analysed file's hooks registered, so a recheck can replay them for the files it
|
|
9
|
+
# serves from cache.
|
|
10
|
+
#
|
|
11
|
+
# - `files` maps a path to its records as the aggregator replays them, one per `[plugin id, key]` identity.
|
|
12
|
+
# - `reads` maps an identity to the registering plugin's boundary rows when it was last registered
|
|
13
|
+
# ({RunDisclosureCredit}), `#prepare`'s reads included.
|
|
14
|
+
#
|
|
15
|
+
# Staleness is per identity, not per file. A plugin memoises the input a notice is about and re-registers the
|
|
16
|
+
# notice from every file's hook, so crediting the rows to every registering file would re-analyse all of them on
|
|
17
|
+
# any edit to an input the plugin read (one view edit re-analysed 400 of 400 files in an app with one i18n view
|
|
18
|
+
# row). Instead a recheck finds the identities whose rows went stale, re-analyses ONE live registering file for
|
|
19
|
+
# each (the first by path), and withholds those identities' replays. When that file registers the identity, every
|
|
20
|
+
# other holder takes its record. When it registers nothing — the edit took the notice out of its text, it no
|
|
21
|
+
# longer parses, the plugin raised — the recheck cannot tell whether the identity is gone, so it runs again with
|
|
22
|
+
# every other live holder of the identity analysed ({#unresolved_holders}), and the identity is dropped only if
|
|
23
|
+
# none of them registers it either.
|
|
24
|
+
#
|
|
25
|
+
# One shape stays unsound: a key whose registration or record depends both on an input the plugin read and on the
|
|
26
|
+
# registering file's own text. When the input changes and the chosen file still registers the key, the other
|
|
27
|
+
# holders take its record even where, re-analysed, they would register another record or none, until each is
|
|
28
|
+
# re-analysed itself. {Plugin::Base#disclose_once} asks a key not to vary between workers, which rules it out for a
|
|
29
|
+
# plugin that keeps the contract. Only inputs read through the plugin's boundary are tracked; state a plugin carries
|
|
30
|
+
# across files, or an input it reads around the boundary (`ENV`, a direct `File.read`), can leave a replayed row
|
|
31
|
+
# that never clears.
|
|
32
|
+
class RunDisclosureLedger
|
|
33
|
+
attr_reader :files, :reads
|
|
34
|
+
|
|
35
|
+
def initialize(files: {}, reads: {})
|
|
36
|
+
@files = files
|
|
37
|
+
@reads = reads
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The ledger a baseline builds from `fresh` (`{path => [record with :reads]}`, `Runner#analysis_disclosures`).
|
|
41
|
+
def self.from(fresh)
|
|
42
|
+
new.tap { |ledger| ledger.absorb(fresh, fresh.keys, Set.new) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.load(data)
|
|
46
|
+
data.is_a?(Hash) ? new(files: data.fetch(:files, {}), reads: data.fetch(:reads, {})) : new
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def to_h
|
|
50
|
+
{ files: @files, reads: @reads }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# The identities with a boundary row that no longer validates under `fresh_row` (a row → Boolean predicate).
|
|
54
|
+
def stale_identities(&fresh_row)
|
|
55
|
+
verdicts = {}
|
|
56
|
+
@reads.each_with_object(Set.new) do |(identity, rows), stale|
|
|
57
|
+
stale << identity if rows.any? { |row| !verdicts.fetch(row) { verdicts[row] = fresh_row.call(row) } }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# One live registering file per stale identity, the first by path, skipping `removed`.
|
|
62
|
+
def refreshers(stale, removed)
|
|
63
|
+
return Set.new if stale.empty?
|
|
64
|
+
|
|
65
|
+
holders = {}
|
|
66
|
+
@files.each do |path, records|
|
|
67
|
+
next if removed.include?(path)
|
|
68
|
+
|
|
69
|
+
records.each do |record|
|
|
70
|
+
identity = RunDisclosureCredit.identity(record)
|
|
71
|
+
holders[identity] = [holders[identity], path].compact.min if stale.include?(identity)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
holders.values.to_set
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# The live holders, outside `analyzed`, of every `stale` identity no file in `analyzed` registered in `fresh`:
|
|
78
|
+
# the files that must be analysed when the chosen refresher registered nothing. Analysing every one of them
|
|
79
|
+
# settles the identity, since every holder has then decided it afresh.
|
|
80
|
+
def unresolved_holders(fresh, analyzed, stale, removed)
|
|
81
|
+
return Set.new if stale.empty?
|
|
82
|
+
|
|
83
|
+
registered = analyzed.flat_map do |path|
|
|
84
|
+
Array(fresh[path]).map { |record| RunDisclosureCredit.identity(record) }
|
|
85
|
+
end
|
|
86
|
+
unresolved = stale - registered
|
|
87
|
+
return Set.new if unresolved.empty?
|
|
88
|
+
|
|
89
|
+
@files.each_with_object(Set.new) do |(path, records), holders|
|
|
90
|
+
next if removed.include?(path) || analyzed.include?(path)
|
|
91
|
+
|
|
92
|
+
holders << path if records.any? { |record| unresolved.include?(RunDisclosureCredit.identity(record)) }
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# The records to replay for `paths`, without the `stale` identities. Unpinned defence: a stale identity's replay
|
|
97
|
+
# cannot reach the output today, since a refresher that registers it wins over the replay and one that does not
|
|
98
|
+
# sends every holder into the second pass.
|
|
99
|
+
def served(paths, stale = Set.new)
|
|
100
|
+
@files.values_at(*paths).compact.flatten(1).reject do |record|
|
|
101
|
+
stale.include?(RunDisclosureCredit.identity(record))
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Folds a run's registrations in: every path in `analyzed` takes this run's records (or none), every identity
|
|
106
|
+
# this run registered takes this run's rows, and each `stale` identity is refreshed or dropped in the files the
|
|
107
|
+
# run did not analyse. Identities no file holds any more lose their rows.
|
|
108
|
+
def absorb(fresh, analyzed, stale)
|
|
109
|
+
registered = {}
|
|
110
|
+
analyzed.each do |path|
|
|
111
|
+
records = Array(fresh[path])
|
|
112
|
+
records.each { |record| registered[RunDisclosureCredit.identity(record)] ||= record }
|
|
113
|
+
store(path, records.map { |record| RunDisclosureCredit.replayable(record) })
|
|
114
|
+
end
|
|
115
|
+
registered.each { |identity, record| @reads[identity] = record[:reads] }
|
|
116
|
+
refresh_unanalysed(stale, registered, analyzed.to_set)
|
|
117
|
+
prune
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def forget(path)
|
|
121
|
+
@files.delete(path)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
def refresh_unanalysed(stale, registered, analyzed)
|
|
127
|
+
return if stale.empty?
|
|
128
|
+
|
|
129
|
+
@files.each_key.to_a.each do |path|
|
|
130
|
+
next if analyzed.include?(path)
|
|
131
|
+
|
|
132
|
+
store(path, @files[path].filter_map do |record|
|
|
133
|
+
identity = RunDisclosureCredit.identity(record)
|
|
134
|
+
next record unless stale.include?(identity)
|
|
135
|
+
|
|
136
|
+
registered[identity] && RunDisclosureCredit.replayable(registered[identity])
|
|
137
|
+
end)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def store(path, records)
|
|
142
|
+
records.empty? ? @files.delete(path) : @files[path] = records.freeze
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Unpinned hygiene: an identity no file holds is never replayed, so keeping its rows changes no answer; pruning
|
|
146
|
+
# them keeps a stale row from re-analysing a file for nothing and the snapshot from growing.
|
|
147
|
+
def prune
|
|
148
|
+
held = @files.each_value.flat_map do |records|
|
|
149
|
+
records.map { |record| RunDisclosureCredit.identity(record) }
|
|
150
|
+
end.to_set
|
|
151
|
+
@reads.select! { |identity, _| held.include?(identity) }
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
@@ -79,6 +79,8 @@ module Rigor
|
|
|
79
79
|
# (`ProjectContext#environment`) every job's Runner reuses instead of rebuilding.
|
|
80
80
|
# @param prebuilt — the warm, shared pre-pass snapshot
|
|
81
81
|
# (`ProjectContext#project_scan`) every job's Runner adopts instead of re-scanning.
|
|
82
|
+
# @param discovery_seed_for — issue #2147 — a callable returning the cross-file discovery seed for one
|
|
83
|
+
# binding (`ProjectContext#discovery_seed_for`), or nil for none.
|
|
82
84
|
# @param workers — pool size. `#analyze` degrades to sequential in-process execution — one
|
|
83
85
|
# job at a time, no `fork` — when fewer than 2 bindings are submitted, fewer than `min_batch_size`
|
|
84
86
|
# bindings are submitted, `workers` is not positive, `fork` is unavailable on this platform, or
|
|
@@ -88,12 +90,13 @@ module Rigor
|
|
|
88
90
|
# @param min_batch_size — see {DEFAULT_MIN_BATCH_SIZE}. Exposed as a constructor param
|
|
89
91
|
# (rather than read from the env internally) purely for spec control; production callers get the
|
|
90
92
|
# resolved default.
|
|
91
|
-
def initialize(configuration:, cache_store:, environment:, prebuilt:, workers:,
|
|
93
|
+
def initialize(configuration:, cache_store:, environment:, prebuilt:, workers:, discovery_seed_for: nil,
|
|
92
94
|
min_batch_size: self.class.resolve_min_batch_size)
|
|
93
95
|
@configuration = configuration
|
|
94
96
|
@cache_store = cache_store
|
|
95
97
|
@environment = environment
|
|
96
98
|
@prebuilt = prebuilt
|
|
99
|
+
@discovery_seed_for = discovery_seed_for
|
|
97
100
|
@workers = workers
|
|
98
101
|
@min_batch_size = min_batch_size
|
|
99
102
|
end
|
|
@@ -123,7 +126,8 @@ module Rigor
|
|
|
123
126
|
def run_one(binding)
|
|
124
127
|
Runner.new(
|
|
125
128
|
configuration: @configuration, cache_store: @cache_store, collect_stats: false,
|
|
126
|
-
buffer: binding, prebuilt: @prebuilt, environment: @environment
|
|
129
|
+
buffer: binding, prebuilt: @prebuilt, environment: @environment,
|
|
130
|
+
discovery_seed: @discovery_seed_for&.call(binding)
|
|
127
131
|
).run([binding.logical_path]).diagnostics
|
|
128
132
|
end
|
|
129
133
|
|
|
@@ -58,7 +58,7 @@ module Rigor
|
|
|
58
58
|
definition_build_failures_snapshot:, hkt_scan_failure_snapshot:,
|
|
59
59
|
conformance_results_snapshot:, signature_standdowns_snapshot: -> { [] },
|
|
60
60
|
vendored_standdowns_snapshot: -> { [] }, pooled_run_disclosures: -> { [] },
|
|
61
|
-
pooled_trust_refusals: -> { [] })
|
|
61
|
+
served_run_disclosures: -> { [] }, pooled_trust_refusals: -> { [] })
|
|
62
62
|
@configuration = configuration
|
|
63
63
|
@rbs_extended_reporter = rbs_extended_reporter
|
|
64
64
|
@boundary_cross_reporter = boundary_cross_reporter
|
|
@@ -79,6 +79,8 @@ module Rigor
|
|
|
79
79
|
# Issue #1051 — reader for the pool coordinator's de-duplicated run-scoped disclosure table.
|
|
80
80
|
# Empty on a sequential run, where `plugin_registry`'s own instances are the ones that registered.
|
|
81
81
|
@pooled_run_disclosures_reader = pooled_run_disclosures
|
|
82
|
+
# Issue #2020 — reader for the rows an incremental recheck replays for the files it served unanalysed.
|
|
83
|
+
@served_run_disclosures_reader = served_run_disclosures
|
|
82
84
|
# Issue #1900 — reader for the pool coordinator's summed trust-refusal table. Empty on a
|
|
83
85
|
# sequential run, where `plugin_registry`'s own boundaries made every read.
|
|
84
86
|
@pooled_trust_refusals_reader = pooled_trust_refusals
|
|
@@ -1116,12 +1118,14 @@ module Rigor
|
|
|
1116
1118
|
end
|
|
1117
1119
|
|
|
1118
1120
|
# Issue #1051 — the run-scoped plugin disclosures ({Plugin::Base#disclose_once}), the run's
|
|
1119
|
-
# project-global notices about its own inputs.
|
|
1121
|
+
# project-global notices about its own inputs. Three sources feed one table:
|
|
1120
1122
|
#
|
|
1121
1123
|
# - the coordinator-side registry, which is where a SEQUENTIAL run's `#prepare` and per-file hooks
|
|
1122
1124
|
# ran; and
|
|
1123
1125
|
# - `pooled_run_disclosures`, the pool coordinator's already-de-duplicated collection from the
|
|
1124
|
-
# pre-fork parent session and every worker payload
|
|
1126
|
+
# pre-fork parent session and every worker payload; and
|
|
1127
|
+
# - `served_run_disclosures` (#2020), the rows an incremental recheck replays for the files it served
|
|
1128
|
+
# from cache, since their per-file hooks did not run.
|
|
1125
1129
|
#
|
|
1126
1130
|
# Both are keyed `[plugin id, key]` and the first registration of a pair wins, so a run emits one
|
|
1127
1131
|
# row per pair no matter how many plugin instances (workers) reached it — the #1051 bug, where a
|
|
@@ -1183,6 +1187,10 @@ module Rigor
|
|
|
1183
1187
|
rescue StandardError
|
|
1184
1188
|
next
|
|
1185
1189
|
end
|
|
1190
|
+
# Issue #2020 — last, so a row this run registered afresh wins over the replay of the same key.
|
|
1191
|
+
Array(@served_run_disclosures_reader.call).each do |record|
|
|
1192
|
+
records[[record[:plugin_id], record[:key]]] ||= record
|
|
1193
|
+
end
|
|
1186
1194
|
records.values
|
|
1187
1195
|
end
|
|
1188
1196
|
|
|
@@ -118,6 +118,12 @@ module Rigor
|
|
|
118
118
|
@collected_analysis_reads ||= {}
|
|
119
119
|
end
|
|
120
120
|
|
|
121
|
+
# Issue #2020 — the per-file run-scoped registrations the fork pool's workers recorded, folded into
|
|
122
|
+
# `Runner#analysis_disclosures` beside {#collected_analysis_reads}.
|
|
123
|
+
def collected_analysis_disclosures
|
|
124
|
+
@collected_analysis_disclosures ||= {}
|
|
125
|
+
end
|
|
126
|
+
|
|
121
127
|
# ADR-103 WD12 — the per-file {Effects::FileCollection}s the fork pool captured this run (empty
|
|
122
128
|
# unless the configuration carries an `effects:` block and a pool run occurred). The runner merges
|
|
123
129
|
# these with its own sequential records before the post-pool fixpoint.
|
|
@@ -914,7 +920,10 @@ module Rigor
|
|
|
914
920
|
merge_run_disclosures(session.drain_run_disclosures)
|
|
915
921
|
# The degraded slice was re-analysed on the parent session, so its dependency records live there,
|
|
916
922
|
# not in a child payload — fold them in alongside the successful children's.
|
|
917
|
-
|
|
923
|
+
if @record_dependencies
|
|
924
|
+
collect_dependencies(session.drain_dependencies, session.drain_analysis_reads,
|
|
925
|
+
session.drain_analysis_disclosures)
|
|
926
|
+
end
|
|
918
927
|
@collected_effects.merge!(session.drain_effects) if @record_effects
|
|
919
928
|
end
|
|
920
929
|
|
|
@@ -952,6 +961,7 @@ module Rigor
|
|
|
952
961
|
payload = { results: results, reporters: session.drain_reporters,
|
|
953
962
|
dependencies: session.drain_dependencies,
|
|
954
963
|
analysis_reads: session.drain_analysis_reads,
|
|
964
|
+
analysis_disclosures: session.drain_analysis_disclosures,
|
|
955
965
|
disclosures: session.drain_run_disclosures,
|
|
956
966
|
refusals: session.drain_trust_refusals }
|
|
957
967
|
# ADR-103 WD13 — the effects slot exists only on a collecting run, so a normal `--workers N` run
|
|
@@ -991,7 +1001,8 @@ module Rigor
|
|
|
991
1001
|
refusals = payload.fetch(:refusals, [])
|
|
992
1002
|
refusals_by_slice ? refusals_by_slice[child[:index]] = refusals : merge_trust_refusals(refusals)
|
|
993
1003
|
if @record_dependencies
|
|
994
|
-
collect_dependencies(payload.fetch(:dependencies, {}), payload.fetch(:analysis_reads, {})
|
|
1004
|
+
collect_dependencies(payload.fetch(:dependencies, {}), payload.fetch(:analysis_reads, {}),
|
|
1005
|
+
payload.fetch(:analysis_disclosures, {}))
|
|
995
1006
|
end
|
|
996
1007
|
@collected_effects.merge!(payload.fetch(:effects, {})) if @record_effects
|
|
997
1008
|
else
|
|
@@ -1001,10 +1012,12 @@ module Rigor
|
|
|
1001
1012
|
degraded
|
|
1002
1013
|
end
|
|
1003
1014
|
|
|
1004
|
-
# ADR-46 /
|
|
1005
|
-
|
|
1015
|
+
# ADR-46 / issues #1553, #2020 — fold one worker's dependency records, per-file plugin reads and per-file
|
|
1016
|
+
# run-scoped registrations.
|
|
1017
|
+
def collect_dependencies(dependencies, analysis_reads, analysis_disclosures)
|
|
1006
1018
|
@collected_dependencies.merge!(dependencies)
|
|
1007
1019
|
collected_analysis_reads.merge!(analysis_reads)
|
|
1020
|
+
collected_analysis_disclosures.merge!(analysis_disclosures)
|
|
1008
1021
|
end
|
|
1009
1022
|
|
|
1010
1023
|
# @return the child's `{results:, reporters:}` payload, or nil when the child exited
|
|
@@ -11,6 +11,8 @@ require_relative "../project_scan"
|
|
|
11
11
|
require_relative "../worker_session"
|
|
12
12
|
require_relative "../template_units"
|
|
13
13
|
require_relative "../template_unit_collector"
|
|
14
|
+
require_relative "../../project_environment"
|
|
15
|
+
require_relative "scan_environment"
|
|
14
16
|
|
|
15
17
|
module Rigor
|
|
16
18
|
module Analysis
|
|
@@ -100,7 +102,7 @@ module Rigor
|
|
|
100
102
|
synthetic_method_index = Inference::SyntheticMethodScanner.scan(
|
|
101
103
|
plugin_registry: plugin_registry,
|
|
102
104
|
paths: expansion.fetch(:files),
|
|
103
|
-
environment:
|
|
105
|
+
environment: scan_environment(plugin_registry, dependency_source_index, expansion),
|
|
104
106
|
fact_store: shared_fact_store(plugin_registry),
|
|
105
107
|
buffer: @buffer
|
|
106
108
|
)
|
|
@@ -135,7 +137,9 @@ module Rigor
|
|
|
135
137
|
# collectors walk the tree, and the AST is dropped before the next file (peak RSS stays flat).
|
|
136
138
|
def discover(expansion:)
|
|
137
139
|
build_discovery(
|
|
138
|
-
Inference::ScopeIndexer.discovered_project_index_for_paths(
|
|
140
|
+
Inference::ScopeIndexer.discovered_project_index_for_paths(
|
|
141
|
+
expansion.fetch(:files), buffer: @buffer, uncertain: uncertain_paths(expansion)
|
|
142
|
+
)
|
|
139
143
|
)
|
|
140
144
|
end
|
|
141
145
|
|
|
@@ -145,11 +149,18 @@ module Rigor
|
|
|
145
149
|
# (`seed_bundles` empty) this is `#discover` plus a fresh bundle set — the discovery index is identical.
|
|
146
150
|
def discover_from_bundles(expansion:, seed_bundles:)
|
|
147
151
|
index = Inference::ScopeIndexer.discovered_project_index_incremental(
|
|
148
|
-
expansion.fetch(:files), seed_bundles: seed_bundles, buffer: @buffer
|
|
152
|
+
expansion.fetch(:files), seed_bundles: seed_bundles, buffer: @buffer, uncertain: uncertain_paths(expansion)
|
|
149
153
|
)
|
|
150
154
|
[build_discovery(index), index.fetch(:bundles)]
|
|
151
155
|
end
|
|
152
156
|
|
|
157
|
+
# Issue #2021 — the files only the `require_relative` walk added ({Runner#required_outside_paths}): their
|
|
158
|
+
# declarations resolve constants and their methods exist, but every method contribution is `possible`, so no
|
|
159
|
+
# return type, arity or visibility is read from them.
|
|
160
|
+
def uncertain_paths(expansion)
|
|
161
|
+
expansion.fetch(:uncertain, Inference::ScopeIndexer::EMPTY_PATH_SET)
|
|
162
|
+
end
|
|
163
|
+
|
|
153
164
|
# Builds the {Discovery} bundle from a `{ classes:, def_index: }` index hash (shared by the plain and
|
|
154
165
|
# seed-bundle discovery paths).
|
|
155
166
|
def build_discovery(index)
|
|
@@ -294,6 +305,37 @@ module Rigor
|
|
|
294
305
|
|
|
295
306
|
private
|
|
296
307
|
|
|
308
|
+
# Issue #476 — the environment the Tier B (trait registry) half of the synthetic-method scan reads, or
|
|
309
|
+
# nil when no loaded plugin registers a trait registry (the scan then has no use for one, and Tier C's
|
|
310
|
+
# template and nested-class lanes behave exactly as they did). The view is lazy ({ScanEnvironment}): it
|
|
311
|
+
# builds an {Environment} on the first question a matching call site asks. That environment is built
|
|
312
|
+
# WITHOUT `synthetic_method_index:` — the index is this scan's output and only the run's real
|
|
313
|
+
# environment stores it; the loader never reads it — and without the diagnostic reporters, which the
|
|
314
|
+
# run's real environment attaches (a second set here would double-report). Every input that decides
|
|
315
|
+
# what the loader resolves (libraries, signature paths, plugin signatures, the bundle / collection
|
|
316
|
+
# axes, required features) is the one the real build passes, so the module a trait names resolves to
|
|
317
|
+
# the same RBS the dispatcher later promotes against.
|
|
318
|
+
def scan_environment(plugin_registry, dependency_source_index, expansion)
|
|
319
|
+
return nil if plugin_registry.nil? || plugin_registry.empty?
|
|
320
|
+
return nil unless plugin_registry.plugins.any? { |plugin| plugin.manifest.trait_registries.any? }
|
|
321
|
+
|
|
322
|
+
files = expansion.fetch(:files)
|
|
323
|
+
ScanEnvironment.new do
|
|
324
|
+
Environment.for_project(
|
|
325
|
+
libraries: @configuration.libraries,
|
|
326
|
+
signature_paths: @configuration.signature_paths,
|
|
327
|
+
cache_store: @cache_store,
|
|
328
|
+
plugin_registry: plugin_registry,
|
|
329
|
+
dependency_source_index: dependency_source_index,
|
|
330
|
+
source_files: files,
|
|
331
|
+
required_features: ProjectEnvironment.required_features(
|
|
332
|
+
@configuration, files, sources: ProjectEnvironment.buffer_sources(@buffer)
|
|
333
|
+
),
|
|
334
|
+
**ProjectEnvironment.dependency_discovery_options(@configuration)
|
|
335
|
+
)
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
297
339
|
def trust_refusals(registry, baseline)
|
|
298
340
|
registry.plugins.filter_map do |plugin|
|
|
299
341
|
summary = plugin.io_boundary.refusal_summary
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
module Analysis
|
|
5
|
+
class Runner
|
|
6
|
+
# Issue #476 — the read-only, lazily built view of the project's RBS universe that the ADR-16 Tier B
|
|
7
|
+
# (trait registry) half of {Inference::SyntheticMethodScanner} reads.
|
|
8
|
+
#
|
|
9
|
+
# The scanner needs exactly two answers from an environment: `#rbs_loader` (to explode a trait module such
|
|
10
|
+
# as `Devise::Models::Recoverable` into its instance-method names) and `#class_ordering` (to decide
|
|
11
|
+
# whether a project class descends from an RBS-known `receiver_constraint` such as
|
|
12
|
+
# `ActiveRecord::Base`). Neither reads the synthetic-method index — `Environment.for_project` only stores
|
|
13
|
+
# that index, the RBS loader never consults it — so the chicken-and-egg the issue describes (the env
|
|
14
|
+
# consumes the scan's output) does not reach the loader: the view builds an environment WITHOUT the
|
|
15
|
+
# index, the scan runs against it, and the run's real environment is built afterwards WITH the index.
|
|
16
|
+
#
|
|
17
|
+
# The build is deferred to the first question. A scan whose trait registries never match a call site
|
|
18
|
+
# (no `devise` call in any class body) never asks, so it never pays the environment build. The build is
|
|
19
|
+
# fail-soft: a raise leaves the view answering "unknown", which is what the nil environment answered
|
|
20
|
+
# before — Tier B emits nothing rather than aborting the pre-pass.
|
|
21
|
+
class ScanEnvironment
|
|
22
|
+
# @param builder — called at most once, returns the {Rigor::Environment} (without a synthetic index).
|
|
23
|
+
def initialize(&builder)
|
|
24
|
+
@builder = builder
|
|
25
|
+
@built = false
|
|
26
|
+
@environment = nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def rbs_loader
|
|
30
|
+
environment&.rbs_loader
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def class_ordering(lhs, rhs)
|
|
34
|
+
env = environment
|
|
35
|
+
env.nil? ? :unknown : env.class_ordering(lhs, rhs)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def environment
|
|
41
|
+
return @environment if @built
|
|
42
|
+
|
|
43
|
+
@built = true
|
|
44
|
+
@environment = @builder.call
|
|
45
|
+
rescue StandardError
|
|
46
|
+
@environment = nil
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|