rigortype 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +40 -13
- data/lib/rigor/analysis/fact_store.rb +15 -3
- data/lib/rigor/analysis/result.rb +11 -3
- data/lib/rigor/analysis/run_stats.rb +193 -0
- data/lib/rigor/analysis/runner.rb +387 -12
- data/lib/rigor/analysis/worker_session.rb +327 -0
- data/lib/rigor/builtins/imported_refinements.rb +6 -2
- data/lib/rigor/builtins/regex_refinement.rb +17 -12
- data/lib/rigor/cache/rbs_descriptor.rb +3 -1
- data/lib/rigor/cache/store.rb +40 -7
- data/lib/rigor/cli.rb +52 -2
- data/lib/rigor/configuration.rb +131 -6
- data/lib/rigor/environment/bundle_sig_discovery.rb +198 -0
- data/lib/rigor/environment/class_registry.rb +12 -3
- data/lib/rigor/environment/lockfile_resolver.rb +125 -0
- data/lib/rigor/environment/rbs_collection_discovery.rb +126 -0
- data/lib/rigor/environment/rbs_coverage_report.rb +112 -0
- data/lib/rigor/environment/rbs_loader.rb +194 -6
- data/lib/rigor/environment/reflection.rb +152 -0
- data/lib/rigor/environment.rb +78 -6
- data/lib/rigor/inference/acceptance.rb +35 -1
- data/lib/rigor/inference/builtins/method_catalog.rb +12 -5
- data/lib/rigor/inference/builtins/numeric_catalog.rb +15 -4
- data/lib/rigor/inference/expression_typer.rb +12 -2
- data/lib/rigor/inference/macro_block_self_type.rb +96 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +29 -29
- data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +4 -4
- data/lib/rigor/inference/method_dispatcher/method_folding.rb +18 -1
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +1 -1
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +46 -40
- data/lib/rigor/inference/method_dispatcher.rb +128 -3
- data/lib/rigor/inference/method_parameter_binder.rb +21 -11
- data/lib/rigor/inference/narrowing.rb +127 -8
- data/lib/rigor/inference/synthetic_method.rb +86 -0
- data/lib/rigor/inference/synthetic_method_index.rb +82 -0
- data/lib/rigor/inference/synthetic_method_scanner.rb +521 -0
- data/lib/rigor/plugin/blueprint.rb +60 -0
- data/lib/rigor/plugin/loader.rb +3 -1
- data/lib/rigor/plugin/macro/block_as_method.rb +131 -0
- data/lib/rigor/plugin/macro/external_file.rb +143 -0
- data/lib/rigor/plugin/macro/heredoc_template.rb +201 -0
- data/lib/rigor/plugin/macro/trait_registry.rb +198 -0
- data/lib/rigor/plugin/macro.rb +31 -0
- data/lib/rigor/plugin/manifest.rb +78 -7
- data/lib/rigor/plugin/registry.rb +32 -2
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/trinary.rb +15 -11
- data/lib/rigor/type/bot.rb +6 -3
- data/lib/rigor/type/combinator.rb +12 -1
- data/lib/rigor/type/integer_range.rb +7 -7
- data/lib/rigor/type/refined.rb +18 -12
- data/lib/rigor/type/top.rb +4 -3
- data/lib/rigor/type_node/generic.rb +7 -1
- data/lib/rigor/type_node/identifier.rb +9 -1
- data/lib/rigor/type_node/string_literal.rb +4 -1
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/environment.rbs +5 -2
- data/sig/rigor/plugin/blueprint.rbs +7 -0
- data/sig/rigor/plugin/manifest.rbs +1 -1
- data/sig/rigor/plugin/registry.rbs +14 -1
- data/sig/rigor.rbs +35 -2
- metadata +39 -1
|
@@ -11,11 +11,14 @@ require_relative "../reflection"
|
|
|
11
11
|
require_relative "../type/combinator"
|
|
12
12
|
require_relative "../inference/coverage_scanner"
|
|
13
13
|
require_relative "../inference/scope_indexer"
|
|
14
|
+
require_relative "../inference/synthetic_method_scanner"
|
|
14
15
|
require_relative "../inference/method_dispatcher/file_folding"
|
|
15
16
|
require_relative "check_rules"
|
|
16
17
|
require_relative "dependency_source_inference"
|
|
17
18
|
require_relative "diagnostic"
|
|
18
19
|
require_relative "result"
|
|
20
|
+
require_relative "run_stats"
|
|
21
|
+
require_relative "worker_session"
|
|
19
22
|
|
|
20
23
|
module Rigor
|
|
21
24
|
module Analysis
|
|
@@ -35,13 +38,29 @@ module Rigor
|
|
|
35
38
|
# run; the CLI's `--no-cache` flag wires `nil` through.
|
|
36
39
|
# v0.0.9 group A slice 1 introduces the surface; later
|
|
37
40
|
# slices route real producers through it.
|
|
41
|
+
# @param workers [Integer] ADR-15 Phase 4b — when greater
|
|
42
|
+
# than zero, per-file analysis dispatches across a pool of
|
|
43
|
+
# N Ractor workers built around {WorkerSession}. Default
|
|
44
|
+
# `0` keeps the sequential code path bit-for-bit
|
|
45
|
+
# unchanged. Phase 4c will wire the CLI / `.rigor.yml`
|
|
46
|
+
# surface that produces non-zero values; this slice
|
|
47
|
+
# leaves the parameter as a programmatic opt-in only.
|
|
48
|
+
# @param collect_stats [Boolean] when true (default), `#run`
|
|
49
|
+
# builds a {RunStats} summary exposed via `result.stats`
|
|
50
|
+
# — this forces the RBS env build at end-of-run so the
|
|
51
|
+
# `class_decl_paths` snapshot has real source attribution.
|
|
52
|
+
# Set to false to skip the stats summary entirely; the
|
|
53
|
+
# CLI's `--no-stats` threads `false` through to keep
|
|
54
|
+
# trivial-fixture runs from warming `.rigor/cache`.
|
|
38
55
|
def initialize(configuration:, explain: false,
|
|
39
56
|
cache_store: Cache::Store.new(root: DEFAULT_CACHE_ROOT),
|
|
40
|
-
plugin_requirer: nil)
|
|
57
|
+
plugin_requirer: nil, workers: 0, collect_stats: true)
|
|
41
58
|
@configuration = configuration
|
|
42
59
|
@explain = explain
|
|
43
60
|
@cache_store = cache_store
|
|
44
61
|
@plugin_requirer = plugin_requirer
|
|
62
|
+
@workers = workers
|
|
63
|
+
@collect_stats = collect_stats
|
|
45
64
|
@plugin_registry = Plugin::Registry::EMPTY
|
|
46
65
|
@dependency_source_index = DependencySourceInference::Index::EMPTY
|
|
47
66
|
@rbs_extended_reporter = RbsExtended::Reporter.new
|
|
@@ -60,28 +79,69 @@ module Rigor
|
|
|
60
79
|
Inference::MethodDispatcher::FileFolding.fold_platform_specific_paths =
|
|
61
80
|
@configuration.fold_platform_specific_paths
|
|
62
81
|
|
|
82
|
+
wall_started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
83
|
+
|
|
63
84
|
target_ruby_error = validate_target_ruby
|
|
64
85
|
return Result.new(diagnostics: [target_ruby_error]) if target_ruby_error
|
|
65
86
|
|
|
66
87
|
@plugin_registry = load_plugins
|
|
67
88
|
@dependency_source_index = DependencySourceInference::Builder.build(@configuration.dependencies)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
89
|
+
expansion = expand_paths(paths)
|
|
90
|
+
@class_decl_paths_snapshot = {}.freeze
|
|
91
|
+
@signature_paths_snapshot = []
|
|
92
|
+
# ADR-16 slice 2b — Tier C pre-pass. Built once per run
|
|
93
|
+
# against the resolved file set + the loaded plugin
|
|
94
|
+
# registry's `heredoc_templates` so synthetic methods are
|
|
95
|
+
# visible cross-file when per-file inference dispatches.
|
|
96
|
+
@synthetic_method_index = Inference::SyntheticMethodScanner.scan(
|
|
72
97
|
plugin_registry: @plugin_registry,
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
boundary_cross_reporter: @boundary_cross_reporter
|
|
98
|
+
paths: expansion.fetch(:files),
|
|
99
|
+
environment: nil
|
|
76
100
|
)
|
|
77
|
-
expansion = expand_paths(paths)
|
|
78
101
|
|
|
79
102
|
diagnostics = pre_file_diagnostics(expansion)
|
|
80
|
-
diagnostics += expansion.fetch(:files)
|
|
103
|
+
diagnostics += analyze_files(expansion.fetch(:files))
|
|
81
104
|
diagnostics += rbs_extended_reporter_diagnostics
|
|
82
105
|
diagnostics += boundary_cross_diagnostics
|
|
83
106
|
|
|
84
|
-
Result.new(
|
|
107
|
+
Result.new(
|
|
108
|
+
diagnostics: apply_severity_profile(diagnostics),
|
|
109
|
+
stats: @collect_stats ? build_run_stats(wall_started_at: wall_started_at, expansion: expansion) : nil
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# ADR-15 Phase 4b — routes per-file analysis to either the
|
|
114
|
+
# sequential coordinator-side Environment (legacy path,
|
|
115
|
+
# default) or a Ractor worker pool built around
|
|
116
|
+
# {WorkerSession} (opt-in via `workers:`). The sequential
|
|
117
|
+
# path is bit-for-bit unchanged from v0.1.4 / earlier; the
|
|
118
|
+
# pool path is the substrate exercised by phase 4c when
|
|
119
|
+
# `RIGOR_RACTOR_WORKERS` / `.rigor.yml` `parallel.workers:`
|
|
120
|
+
# is wired.
|
|
121
|
+
#
|
|
122
|
+
# Sequential mode also snapshots `class_decl_paths` from the
|
|
123
|
+
# local environment after the per-file loop completes so
|
|
124
|
+
# `RunStats` can attribute the RBS class universe between
|
|
125
|
+
# project-sig and bundled sources. The env stays a LOCAL
|
|
126
|
+
# variable (not an ivar) so it goes GC-eligible when the
|
|
127
|
+
# method returns — holding it as long-lived state added
|
|
128
|
+
# memory pressure that surfaced as a Bus Error during the
|
|
129
|
+
# spec suite under Ruby 4.0 + rbs 4.0.2.
|
|
130
|
+
def analyze_files(files)
|
|
131
|
+
return [] if files.empty?
|
|
132
|
+
|
|
133
|
+
if pool_mode?
|
|
134
|
+
analyze_files_in_pool(files)
|
|
135
|
+
else
|
|
136
|
+
environment = build_runner_environment
|
|
137
|
+
result = files.flat_map { |path| analyze_file(path, environment) }
|
|
138
|
+
if @collect_stats
|
|
139
|
+
loader = environment.rbs_loader
|
|
140
|
+
@class_decl_paths_snapshot = loader&.class_decl_paths || {}.freeze
|
|
141
|
+
@signature_paths_snapshot = loader&.signature_paths || [].freeze
|
|
142
|
+
end
|
|
143
|
+
result
|
|
144
|
+
end
|
|
85
145
|
end
|
|
86
146
|
|
|
87
147
|
# Pre-file diagnostic streams that fire once per run rather
|
|
@@ -90,12 +150,25 @@ module Rigor
|
|
|
90
150
|
# `expand_paths` errors for `paths:` entries that don't
|
|
91
151
|
# exist or aren't `.rb`. Aggregated here so `#run` stays
|
|
92
152
|
# under the ABC budget.
|
|
153
|
+
#
|
|
154
|
+
# ADR-15 Phase 4b — `plugin_prepare_diagnostics` runs on
|
|
155
|
+
# the coordinator's plugin registry under sequential mode;
|
|
156
|
+
# under pool mode each worker re-runs `prepare` against
|
|
157
|
+
# its own plugin instances, so the pool path drains the
|
|
158
|
+
# first worker's prepare-diagnostic snapshot into the
|
|
159
|
+
# aggregated diagnostic stream instead (see
|
|
160
|
+
# {#analyze_files_in_pool}). Skipping the coordinator
|
|
161
|
+
# prepare in pool mode avoids double-running `#prepare`
|
|
162
|
+
# against the coordinator-side plugin instances (which
|
|
163
|
+
# the pool path never consults for per-file analysis).
|
|
93
164
|
def pre_file_diagnostics(expansion)
|
|
165
|
+
prepare = pool_mode? ? [] : plugin_prepare_diagnostics
|
|
94
166
|
plugin_load_diagnostics +
|
|
95
|
-
|
|
167
|
+
prepare +
|
|
96
168
|
dependency_source_diagnostics +
|
|
97
169
|
dependency_source_budget_diagnostics +
|
|
98
170
|
dependency_source_config_conflict_diagnostics +
|
|
171
|
+
rbs_coverage_diagnostics +
|
|
99
172
|
expansion.fetch(:errors)
|
|
100
173
|
end
|
|
101
174
|
|
|
@@ -120,6 +193,244 @@ module Rigor
|
|
|
120
193
|
|
|
121
194
|
private
|
|
122
195
|
|
|
196
|
+
def pool_mode?
|
|
197
|
+
@workers.is_a?(Integer) && @workers.positive?
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Coordinator-side Environment used by the sequential code
|
|
201
|
+
# path. Pool mode builds one Environment per worker inside
|
|
202
|
+
# the worker Ractor's body instead.
|
|
203
|
+
def build_runner_environment
|
|
204
|
+
Environment.for_project(
|
|
205
|
+
libraries: @configuration.libraries,
|
|
206
|
+
signature_paths: @configuration.signature_paths,
|
|
207
|
+
cache_store: @cache_store,
|
|
208
|
+
plugin_registry: @plugin_registry,
|
|
209
|
+
dependency_source_index: @dependency_source_index,
|
|
210
|
+
rbs_extended_reporter: @rbs_extended_reporter,
|
|
211
|
+
boundary_cross_reporter: @boundary_cross_reporter,
|
|
212
|
+
bundler_bundle_path: @configuration.bundler_bundle_path,
|
|
213
|
+
bundler_auto_detect: @configuration.bundler_auto_detect,
|
|
214
|
+
bundler_lockfile: @configuration.bundler_lockfile,
|
|
215
|
+
rbs_collection_lockfile: @configuration.rbs_collection_lockfile,
|
|
216
|
+
rbs_collection_auto_detect: @configuration.rbs_collection_auto_detect,
|
|
217
|
+
synthetic_method_index: @synthetic_method_index
|
|
218
|
+
)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# ADR-15 Phase 4b — Ractor pool around {WorkerSession}.
|
|
222
|
+
# Spawns `@workers` Ractors; each takes the shareable
|
|
223
|
+
# payload (Configuration, cache_root String, plugin
|
|
224
|
+
# Blueprint Array, explain Boolean) and builds its OWN
|
|
225
|
+
# WorkerSession internally. Files are distributed
|
|
226
|
+
# round-robin across the pool; each worker writes back to
|
|
227
|
+
# the main Ractor's mailbox via `Ractor.main.send` with
|
|
228
|
+
# one of three message kinds:
|
|
229
|
+
#
|
|
230
|
+
# - `[:prepare, diagnostics]` — once at startup, the
|
|
231
|
+
# session's `prepare_diagnostics` snapshot. The
|
|
232
|
+
# coordinator keeps the FIRST worker's snapshot only
|
|
233
|
+
# (plugin `#prepare` is deterministic per plugin, so
|
|
234
|
+
# each worker produces the same diagnostic set; surfacing
|
|
235
|
+
# them once avoids N× duplication).
|
|
236
|
+
# - `[:file, path, diagnostics]` — one per analysed file.
|
|
237
|
+
# - `[:done, drained_reporters]` — once at exit, the
|
|
238
|
+
# per-worker reporter snapshots for end-of-pool merge.
|
|
239
|
+
#
|
|
240
|
+
# The Ruby 4.0+ Ractor model uses a single per-Ractor
|
|
241
|
+
# mailbox (no `Ractor.yield`); workers push back via
|
|
242
|
+
# `Ractor.main.send`. The coordinator drains its mailbox
|
|
243
|
+
# via `Ractor.receive` until it has counted exactly
|
|
244
|
+
# `pool.size` `:done` messages.
|
|
245
|
+
#
|
|
246
|
+
# Diagnostic order: original path order. Workers may
|
|
247
|
+
# complete files out of order; the coordinator re-orders
|
|
248
|
+
# via the `results_by_path` Hash before flattening.
|
|
249
|
+
#
|
|
250
|
+
# Reporter merge: per-worker `RbsExtended::Reporter` and
|
|
251
|
+
# `BoundaryCrossReporter` entries are replayed into the
|
|
252
|
+
# runner-side accumulators via their `record_*` APIs,
|
|
253
|
+
# which dedupe on the same keys as a single-session run
|
|
254
|
+
# would. Net result: reporter state is identical to the
|
|
255
|
+
# sequential path.
|
|
256
|
+
def analyze_files_in_pool(files) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity
|
|
257
|
+
# Pre-warm class-level lazy memos on the MAIN Ractor.
|
|
258
|
+
# `Environment::ClassRegistry.default` is the
|
|
259
|
+
# default kwarg threaded through `Environment.new`
|
|
260
|
+
# inside each worker session; lazy-initialising it
|
|
261
|
+
# from a non-main Ractor would trip
|
|
262
|
+
# `Ractor::IsolationError`. Touching it here forces
|
|
263
|
+
# the (shareable) registry into the class-ivar cache
|
|
264
|
+
# before any worker reads.
|
|
265
|
+
Environment::ClassRegistry.default
|
|
266
|
+
|
|
267
|
+
# ADR-15 Phase 4b.x — pre-warm the RBS cache so
|
|
268
|
+
# workers serve every reflection query from the
|
|
269
|
+
# Marshal blob on disk. Without this, the first
|
|
270
|
+
# cache MISS inside a worker falls through to
|
|
271
|
+
# `RBS::EnvironmentLoader.new`, which reads a chain
|
|
272
|
+
# of non-`Ractor.shareable?` RubyGems / RBS module
|
|
273
|
+
# constants and raises `Ractor::IsolationError`.
|
|
274
|
+
# Pre-warming requires a `cache_store`; the run aborts
|
|
275
|
+
# to sequential mode otherwise. See ADR-15 Phase 4b.x
|
|
276
|
+
# for the full chain of failing constants.
|
|
277
|
+
if @cache_store.nil?
|
|
278
|
+
return analyze_files_sequentially_fallback(
|
|
279
|
+
files, reason: "pool mode requires a cache_store (--no-cache disables pool)"
|
|
280
|
+
)
|
|
281
|
+
end
|
|
282
|
+
prewarm_rbs_cache_for_pool
|
|
283
|
+
|
|
284
|
+
configuration = @configuration
|
|
285
|
+
cache_root = @cache_store&.root
|
|
286
|
+
blueprints = @plugin_registry.blueprints
|
|
287
|
+
explain = @explain
|
|
288
|
+
|
|
289
|
+
pool = Array.new(@workers) do
|
|
290
|
+
Ractor.new(configuration, cache_root, blueprints, explain) do |configuration, cache_root, blueprints, explain|
|
|
291
|
+
cache_store = cache_root ? Rigor::Cache::Store.new(root: cache_root) : nil
|
|
292
|
+
session = Rigor::Analysis::WorkerSession.new(
|
|
293
|
+
configuration: configuration,
|
|
294
|
+
cache_store: cache_store,
|
|
295
|
+
plugin_blueprints: blueprints,
|
|
296
|
+
explain: explain
|
|
297
|
+
)
|
|
298
|
+
main = Ractor.main
|
|
299
|
+
main.send([:prepare, session.prepare_diagnostics])
|
|
300
|
+
|
|
301
|
+
loop do
|
|
302
|
+
msg = Ractor.receive
|
|
303
|
+
break if msg.nil?
|
|
304
|
+
|
|
305
|
+
main.send([:file, msg, session.analyze(msg)])
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
main.send([:done, session.drain_reporters])
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
files.each_with_index { |path, index| pool[index % pool.size].send(path) }
|
|
313
|
+
pool.each { |worker| worker.send(nil) }
|
|
314
|
+
|
|
315
|
+
prepare_diagnostics = nil
|
|
316
|
+
results_by_path = {}
|
|
317
|
+
done_count = 0
|
|
318
|
+
|
|
319
|
+
while done_count < pool.size
|
|
320
|
+
message = Ractor.receive
|
|
321
|
+
case message.first
|
|
322
|
+
when :prepare
|
|
323
|
+
prepare_diagnostics ||= message.last
|
|
324
|
+
when :file
|
|
325
|
+
results_by_path[message[1]] = message[2]
|
|
326
|
+
when :done
|
|
327
|
+
merge_worker_reporters(message.last)
|
|
328
|
+
done_count += 1
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
pool.each(&:join)
|
|
333
|
+
|
|
334
|
+
Array(prepare_diagnostics) + files.flat_map { |path| results_by_path.fetch(path, []) }
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# End-of-run telemetry. Walks the cached
|
|
338
|
+
# `class_decl_paths` snapshot (sequential mode: from
|
|
339
|
+
# the coordinator's environment; pool mode: from the
|
|
340
|
+
# first worker's `:prepare` payload) and partitions the
|
|
341
|
+
# RBS class universe into "project sig/" (paths under
|
|
342
|
+
# `signature_paths`) vs "bundled" (everything else).
|
|
343
|
+
# Gem source-walk counts come from `dependency_source_index`
|
|
344
|
+
# which is already constructed regardless of pool mode.
|
|
345
|
+
# Wall + RSS are single syscalls; total cost is bounded
|
|
346
|
+
# by the snapshot size (~1000-2000 entries).
|
|
347
|
+
def build_run_stats(wall_started_at:, expansion:)
|
|
348
|
+
snapshot = @class_decl_paths_snapshot || {}.freeze
|
|
349
|
+
project_sig, bundled = RunStats.partition_classes(
|
|
350
|
+
class_decl_paths: snapshot,
|
|
351
|
+
signature_paths: @signature_paths_snapshot
|
|
352
|
+
)
|
|
353
|
+
RunStats.new(
|
|
354
|
+
wall_seconds: Process.clock_gettime(Process::CLOCK_MONOTONIC) - wall_started_at,
|
|
355
|
+
peak_rss_bytes: RunStats.peak_rss_bytes,
|
|
356
|
+
target_files: expansion.fetch(:files).size,
|
|
357
|
+
rbs_classes_total: snapshot.size,
|
|
358
|
+
rbs_classes_project_sig: project_sig,
|
|
359
|
+
rbs_classes_bundled: bundled,
|
|
360
|
+
rbs_attribution_available: RunStats.attribution_available?(class_decl_paths: snapshot),
|
|
361
|
+
gem_walk_classes: @dependency_source_index.class_to_gem.size,
|
|
362
|
+
gem_walk_gems: @dependency_source_index.resolved_gems.size
|
|
363
|
+
)
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# ADR-15 Phase 4b.x — drives every cached RBS producer
|
|
367
|
+
# on the main Ractor so each worker can serve all
|
|
368
|
+
# reflection queries from disk (Marshal-load only).
|
|
369
|
+
# Builds a single coordinator-side {Environment} for
|
|
370
|
+
# this purpose; the env object is discarded immediately
|
|
371
|
+
# after the cache is warm — workers build their own
|
|
372
|
+
# `Environment.for_project` inside the Ractor body,
|
|
373
|
+
# which then routes through `cached_env` instead of
|
|
374
|
+
# `RBS::EnvironmentLoader.new`.
|
|
375
|
+
def prewarm_rbs_cache_for_pool
|
|
376
|
+
warm_env = Environment.for_project(
|
|
377
|
+
libraries: @configuration.libraries,
|
|
378
|
+
signature_paths: @configuration.signature_paths,
|
|
379
|
+
cache_store: @cache_store,
|
|
380
|
+
bundler_bundle_path: @configuration.bundler_bundle_path,
|
|
381
|
+
bundler_auto_detect: @configuration.bundler_auto_detect,
|
|
382
|
+
bundler_lockfile: @configuration.bundler_lockfile,
|
|
383
|
+
rbs_collection_lockfile: @configuration.rbs_collection_lockfile,
|
|
384
|
+
rbs_collection_auto_detect: @configuration.rbs_collection_auto_detect
|
|
385
|
+
)
|
|
386
|
+
warm_env.rbs_loader&.prewarm
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# ADR-15 Phase 4b.x — pool-mode safety net. When pool
|
|
390
|
+
# mode is configured but a precondition fails (currently:
|
|
391
|
+
# `--no-cache` would force workers through
|
|
392
|
+
# `EnvironmentLoader.new`), degrade to sequential
|
|
393
|
+
# analysis with a `:warning` `pool-degraded` diagnostic
|
|
394
|
+
# at run start. The actual per-file analysis runs on
|
|
395
|
+
# the coordinator, identical to the default sequential
|
|
396
|
+
# path.
|
|
397
|
+
def analyze_files_sequentially_fallback(files, reason:)
|
|
398
|
+
environment = build_runner_environment
|
|
399
|
+
diagnostics = files.flat_map { |path| analyze_file(path, environment) }
|
|
400
|
+
loader = environment.rbs_loader
|
|
401
|
+
@class_decl_paths_snapshot = loader&.class_decl_paths || {}.freeze
|
|
402
|
+
@signature_paths_snapshot = loader&.signature_paths || [].freeze
|
|
403
|
+
diagnostics.unshift(
|
|
404
|
+
Diagnostic.new(
|
|
405
|
+
path: ".rigor.yml", line: 1, column: 1,
|
|
406
|
+
message: "pool mode degraded to sequential: #{reason}",
|
|
407
|
+
severity: :warning, rule: "pool-degraded", source_family: :builtin
|
|
408
|
+
)
|
|
409
|
+
)
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def merge_worker_reporters(drained)
|
|
413
|
+
rbs = drained.fetch(:rbs_extended)
|
|
414
|
+
rbs.fetch(:unresolved_payloads).each do |entry|
|
|
415
|
+
@rbs_extended_reporter.record_unresolved(
|
|
416
|
+
payload: entry.payload, source_location: entry.source_location
|
|
417
|
+
)
|
|
418
|
+
end
|
|
419
|
+
rbs.fetch(:lossy_projections).each do |entry|
|
|
420
|
+
@rbs_extended_reporter.record_lossy_projection(
|
|
421
|
+
head: entry.head, source_location: entry.source_location
|
|
422
|
+
)
|
|
423
|
+
end
|
|
424
|
+
drained.fetch(:boundary_cross).each do |entry|
|
|
425
|
+
@boundary_cross_reporter.record(
|
|
426
|
+
class_name: entry.class_name,
|
|
427
|
+
method_name: entry.method_name,
|
|
428
|
+
gem_name: entry.gem_name,
|
|
429
|
+
rbs_display: entry.rbs_display
|
|
430
|
+
)
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
|
|
123
434
|
# Loads project-configured plugins through {Rigor::Plugin::Loader}
|
|
124
435
|
# and returns the resulting {Rigor::Plugin::Registry}. Loader
|
|
125
436
|
# failures are isolated: each surfaces as a `:plugin_loader`
|
|
@@ -304,6 +615,70 @@ module Rigor
|
|
|
304
615
|
end
|
|
305
616
|
end
|
|
306
617
|
|
|
618
|
+
# O4 Layer 3 slice 3 — graceful-degradation coverage
|
|
619
|
+
# report. When the project has a `Gemfile.lock` (slice 1)
|
|
620
|
+
# and one or more locked gems are not covered by ANY of
|
|
621
|
+
# the four RBS resolution paths (`DEFAULT_LIBRARIES`,
|
|
622
|
+
# `data/vendored_gem_sigs/`, slice-1 bundle-shipped
|
|
623
|
+
# `sig/`, slice-2 `rbs_collection.lock.yaml`), emit a
|
|
624
|
+
# single `:info` diagnostic summarising the uncovered set
|
|
625
|
+
# so the user can act on it (run `rbs collection install`,
|
|
626
|
+
# opt the gem into `dependencies.source_inference:`, or
|
|
627
|
+
# accept the `Dynamic[T]` fallback).
|
|
628
|
+
#
|
|
629
|
+
# Suppressed when the lockfile is empty, when every gem
|
|
630
|
+
# is covered, or when slice 1's `bundler.lockfile`
|
|
631
|
+
# discovery returned nothing (no lockfile to read).
|
|
632
|
+
def rbs_coverage_diagnostics
|
|
633
|
+
locked = Environment::LockfileResolver.locked_gems(
|
|
634
|
+
lockfile_path: @configuration.bundler_lockfile,
|
|
635
|
+
project_root: Dir.pwd,
|
|
636
|
+
auto_detect: @configuration.bundler_auto_detect
|
|
637
|
+
)
|
|
638
|
+
return [] if locked.empty?
|
|
639
|
+
|
|
640
|
+
bundle_sig_paths = Environment::BundleSigDiscovery.discover(
|
|
641
|
+
bundle_path: @configuration.bundler_bundle_path,
|
|
642
|
+
project_root: Dir.pwd,
|
|
643
|
+
auto_detect: @configuration.bundler_auto_detect,
|
|
644
|
+
locked_gems: locked
|
|
645
|
+
)
|
|
646
|
+
collection_paths = Environment::RbsCollectionDiscovery.discover(
|
|
647
|
+
lockfile_path: @configuration.rbs_collection_lockfile,
|
|
648
|
+
project_root: Dir.pwd,
|
|
649
|
+
auto_detect: @configuration.rbs_collection_auto_detect
|
|
650
|
+
)
|
|
651
|
+
rows = Environment::RbsCoverageReport.classify(
|
|
652
|
+
locked_gems: locked,
|
|
653
|
+
default_libraries: Environment::DEFAULT_LIBRARIES,
|
|
654
|
+
bundle_sig_paths: bundle_sig_paths,
|
|
655
|
+
rbs_collection_paths: collection_paths
|
|
656
|
+
)
|
|
657
|
+
missing = Environment::RbsCoverageReport.missing(rows)
|
|
658
|
+
return [] if missing.empty?
|
|
659
|
+
|
|
660
|
+
[build_rbs_coverage_missing_diagnostic(missing)]
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
def build_rbs_coverage_missing_diagnostic(missing)
|
|
664
|
+
sample_size = 5
|
|
665
|
+
sample = missing.first(sample_size).map(&:gem_name)
|
|
666
|
+
suffix = missing.size > sample_size ? ", and #{missing.size - sample_size} more" : ""
|
|
667
|
+
Diagnostic.new(
|
|
668
|
+
path: ".rigor.yml",
|
|
669
|
+
line: 1,
|
|
670
|
+
column: 1,
|
|
671
|
+
message: "#{missing.size} gem(s) in Gemfile.lock have no RBS available: " \
|
|
672
|
+
"#{sample.join(', ')}#{suffix}. " \
|
|
673
|
+
"Consider `rbs collection install` to fetch community RBS from " \
|
|
674
|
+
"`ruby/gem_rbs_collection`, ship `sig/` in the gem itself, or " \
|
|
675
|
+
"opt the gem into `dependencies.source_inference:` in `.rigor.yml`.",
|
|
676
|
+
severity: :info,
|
|
677
|
+
rule: "rbs.coverage.missing-gem",
|
|
678
|
+
source_family: :builtin
|
|
679
|
+
)
|
|
680
|
+
end
|
|
681
|
+
|
|
307
682
|
# ADR-13 slice 3b — drains the per-run
|
|
308
683
|
# {RbsExtended::Reporter} into one diagnostic per accumulated
|
|
309
684
|
# event:
|