rigortype 0.1.4 → 0.1.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 +69 -56
- data/lib/rigor/analysis/buffer_binding.rb +36 -0
- data/lib/rigor/analysis/check_rules.rb +11 -1
- data/lib/rigor/analysis/dependency_source_inference/index.rb +14 -1
- data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +105 -0
- data/lib/rigor/analysis/dependency_source_inference/walker.rb +32 -12
- data/lib/rigor/analysis/fact_store.rb +15 -3
- data/lib/rigor/analysis/project_scan.rb +39 -0
- data/lib/rigor/analysis/result.rb +11 -3
- data/lib/rigor/analysis/run_stats.rb +193 -0
- data/lib/rigor/analysis/runner.rb +681 -19
- data/lib/rigor/analysis/worker_session.rb +339 -0
- data/lib/rigor/builtins/hkt_builtins.rb +342 -0
- data/lib/rigor/builtins/imported_refinements.rb +6 -2
- data/lib/rigor/builtins/regex_refinement.rb +17 -12
- data/lib/rigor/builtins/static_return_refinements.rb +120 -0
- data/lib/rigor/cache/rbs_descriptor.rb +3 -1
- data/lib/rigor/cache/store.rb +72 -9
- data/lib/rigor/cli/lsp_command.rb +129 -0
- data/lib/rigor/cli/type_of_command.rb +44 -5
- data/lib/rigor/cli.rb +122 -10
- data/lib/rigor/configuration.rb +168 -7
- data/lib/rigor/environment/bundle_sig_discovery.rb +198 -0
- data/lib/rigor/environment/class_registry.rb +12 -3
- data/lib/rigor/environment/hkt_registry_holder.rb +33 -0
- 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 +238 -7
- data/lib/rigor/environment/reflection.rb +152 -0
- data/lib/rigor/environment/reporters.rb +40 -0
- data/lib/rigor/environment.rb +179 -10
- data/lib/rigor/inference/acceptance.rb +83 -4
- 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 +59 -2
- data/lib/rigor/inference/hkt_body.rb +171 -0
- data/lib/rigor/inference/hkt_body_parser.rb +363 -0
- data/lib/rigor/inference/hkt_reducer.rb +256 -0
- data/lib/rigor/inference/hkt_registry.rb +223 -0
- 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 +126 -31
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +87 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +46 -40
- data/lib/rigor/inference/method_dispatcher.rb +282 -6
- data/lib/rigor/inference/method_parameter_binder.rb +21 -11
- data/lib/rigor/inference/narrowing.rb +127 -8
- data/lib/rigor/inference/project_patched_methods.rb +70 -0
- data/lib/rigor/inference/project_patched_scanner.rb +210 -0
- data/lib/rigor/inference/scope_indexer.rb +156 -12
- data/lib/rigor/inference/statement_evaluator.rb +106 -6
- 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 +599 -0
- data/lib/rigor/language_server/buffer_table.rb +63 -0
- data/lib/rigor/language_server/completion_provider.rb +438 -0
- data/lib/rigor/language_server/debouncer.rb +86 -0
- data/lib/rigor/language_server/diagnostic_publisher.rb +167 -0
- data/lib/rigor/language_server/document_symbol_provider.rb +142 -0
- data/lib/rigor/language_server/folding_range_provider.rb +75 -0
- data/lib/rigor/language_server/hover_provider.rb +74 -0
- data/lib/rigor/language_server/hover_renderer.rb +312 -0
- data/lib/rigor/language_server/loop.rb +71 -0
- data/lib/rigor/language_server/project_context.rb +145 -0
- data/lib/rigor/language_server/selection_range_provider.rb +93 -0
- data/lib/rigor/language_server/server.rb +384 -0
- data/lib/rigor/language_server/signature_help_provider.rb +249 -0
- data/lib/rigor/language_server/synchronized_writer.rb +28 -0
- data/lib/rigor/language_server/uri.rb +40 -0
- data/lib/rigor/language_server.rb +29 -0
- data/lib/rigor/plugin/base.rb +63 -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 +315 -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 +127 -9
- data/lib/rigor/plugin/registry.rb +51 -2
- data/lib/rigor/plugin.rb +1 -0
- data/lib/rigor/rbs_extended/hkt_directives.rb +326 -0
- data/lib/rigor/rbs_extended.rb +82 -2
- data/lib/rigor/sig_gen/generator.rb +12 -3
- data/lib/rigor/trinary.rb +15 -11
- data/lib/rigor/type/app.rb +107 -0
- 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.rb +1 -0
- 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 +11 -4
- data/sig/rigor/inference.rbs +2 -0
- 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 +37 -2
- metadata +92 -1
data/lib/rigor/cli.rb
CHANGED
|
@@ -25,7 +25,8 @@ module Rigor
|
|
|
25
25
|
"type-scan" => :run_type_scan,
|
|
26
26
|
"explain" => :run_explain,
|
|
27
27
|
"diff" => :run_diff,
|
|
28
|
-
"sig-gen" => :run_sig_gen
|
|
28
|
+
"sig-gen" => :run_sig_gen,
|
|
29
|
+
"lsp" => :run_lsp
|
|
29
30
|
}.freeze
|
|
30
31
|
|
|
31
32
|
def self.start(argv = ARGV, out: $stdout, err: $stderr)
|
|
@@ -69,29 +70,91 @@ module Rigor
|
|
|
69
70
|
|
|
70
71
|
def run_check
|
|
71
72
|
require_relative "analysis/runner"
|
|
73
|
+
require_relative "analysis/buffer_binding"
|
|
72
74
|
require_relative "cache/store"
|
|
73
75
|
|
|
74
76
|
options = parse_check_options
|
|
77
|
+
buffer = resolve_buffer_binding(options)
|
|
78
|
+
return EXIT_USAGE if buffer == :usage_error
|
|
75
79
|
|
|
76
80
|
configuration = Configuration.load(options.fetch(:config))
|
|
77
81
|
cache_root = configuration.cache_path
|
|
78
82
|
handle_clear_cache(cache_root) if options.fetch(:clear_cache)
|
|
79
|
-
cache_store = options.fetch(:no_cache) ? nil : Cache::Store.new(root: cache_root)
|
|
80
83
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
explain: options.fetch(:explain),
|
|
85
|
-
cache_store: cache_store
|
|
84
|
+
runner = build_check_runner(
|
|
85
|
+
configuration: configuration, options: options,
|
|
86
|
+
buffer: buffer, cache_root: cache_root
|
|
86
87
|
)
|
|
87
|
-
result = runner.run(paths)
|
|
88
|
+
result = runner.run(@argv.empty? ? configuration.paths : @argv)
|
|
88
89
|
|
|
89
90
|
write_result(result, options.fetch(:format))
|
|
91
|
+
write_run_stats(result.stats) if result.stats
|
|
90
92
|
write_cache_stats(cache_root, runner.cache_store) if options.fetch(:cache_stats)
|
|
91
93
|
result.success? ? 0 : 1
|
|
92
94
|
end
|
|
93
95
|
|
|
94
|
-
def
|
|
96
|
+
def build_check_runner(configuration:, options:, buffer:, cache_root:)
|
|
97
|
+
cache_store = options.fetch(:no_cache) ? nil : Cache::Store.new(root: cache_root)
|
|
98
|
+
Analysis::Runner.new(
|
|
99
|
+
configuration: configuration,
|
|
100
|
+
explain: options.fetch(:explain),
|
|
101
|
+
cache_store: cache_store,
|
|
102
|
+
collect_stats: options.fetch(:stats),
|
|
103
|
+
workers: resolve_workers(options, configuration),
|
|
104
|
+
buffer: buffer
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Editor-mode CLI envelope. The `--tmp-file=PATH` /
|
|
109
|
+
# `--instead-of=PATH` pair binds an in-flight buffer file to
|
|
110
|
+
# the logical project path it represents (see
|
|
111
|
+
# `docs/design/20260516-editor-mode.md`). Both flags must
|
|
112
|
+
# appear together; either alone is a usage error. The
|
|
113
|
+
# physical file must be readable; missing-file is a usage
|
|
114
|
+
# error too so editors get one consistent failure shape.
|
|
115
|
+
#
|
|
116
|
+
# Returns:
|
|
117
|
+
# - `nil` when neither flag was supplied (legacy path).
|
|
118
|
+
# - `Rigor::Analysis::BufferBinding` when the pair is valid.
|
|
119
|
+
# - `:usage_error` after writing one diagnostic to stderr;
|
|
120
|
+
# the caller MUST translate this to `EXIT_USAGE`.
|
|
121
|
+
def resolve_buffer_binding(options)
|
|
122
|
+
tmp = options[:tmp_file]
|
|
123
|
+
instead = options[:instead_of]
|
|
124
|
+
return nil if tmp.nil? && instead.nil?
|
|
125
|
+
|
|
126
|
+
if tmp.nil? || instead.nil?
|
|
127
|
+
@err.puts("--tmp-file and --instead-of must appear together")
|
|
128
|
+
return :usage_error
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
unless File.file?(tmp)
|
|
132
|
+
@err.puts("--tmp-file #{tmp.inspect}: no such file or not readable")
|
|
133
|
+
return :usage_error
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
Analysis::BufferBinding.new(logical_path: instead, physical_path: tmp)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# ADR-15 Phase 4c — resolves the worker count by
|
|
140
|
+
# precedence: CLI `--workers=N` (most explicit) > env
|
|
141
|
+
# `RIGOR_RACTOR_WORKERS` > config `.rigor.yml`
|
|
142
|
+
# `parallel.workers:` > 0 (sequential default). Returns
|
|
143
|
+
# an Integer; non-numeric values raise so typos fail
|
|
144
|
+
# loudly. CLI / env may pass a negative value — clamped
|
|
145
|
+
# to 0 (sequential) so a stray `-1` doesn't crash the
|
|
146
|
+
# pool spawn loop.
|
|
147
|
+
def resolve_workers(options, configuration)
|
|
148
|
+
cli_value = options[:workers]
|
|
149
|
+
return [Integer(cli_value), 0].max if cli_value
|
|
150
|
+
|
|
151
|
+
env_value = ENV.fetch("RIGOR_RACTOR_WORKERS", nil)
|
|
152
|
+
return [Integer(env_value), 0].max if env_value && !env_value.empty?
|
|
153
|
+
|
|
154
|
+
configuration.parallel_workers
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def parse_check_options # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
|
95
158
|
options = {
|
|
96
159
|
# `nil` triggers `Configuration.discover` (`.rigor.yml` then
|
|
97
160
|
# `.rigor.dist.yml`); an explicit `--config=PATH` overrides.
|
|
@@ -100,7 +163,24 @@ module Rigor
|
|
|
100
163
|
explain: false,
|
|
101
164
|
cache_stats: false,
|
|
102
165
|
clear_cache: false,
|
|
103
|
-
no_cache: false
|
|
166
|
+
no_cache: false,
|
|
167
|
+
# Run-stats summary (target files, RBS class universe
|
|
168
|
+
# breakdown, wall time, peak RSS) is on by default
|
|
169
|
+
# because collection is ~free (single syscall for RSS,
|
|
170
|
+
# one walk of `class_decl_paths` for the breakdown).
|
|
171
|
+
# `--no-stats` suppresses it for callers that want a
|
|
172
|
+
# diagnostic-only output stream.
|
|
173
|
+
stats: true,
|
|
174
|
+
# ADR-15 Phase 4c — when nil, falls back to
|
|
175
|
+
# `RIGOR_RACTOR_WORKERS` then `.rigor.yml`
|
|
176
|
+
# `parallel.workers:` then 0 (sequential). See
|
|
177
|
+
# `resolve_workers` for the precedence chain.
|
|
178
|
+
workers: nil,
|
|
179
|
+
# Editor mode (`docs/design/20260516-editor-mode.md`).
|
|
180
|
+
# Both must appear together; the runner uses the pair
|
|
181
|
+
# to bind an in-flight buffer file to its logical path.
|
|
182
|
+
tmp_file: nil,
|
|
183
|
+
instead_of: nil
|
|
104
184
|
}
|
|
105
185
|
parser = OptionParser.new do |opts|
|
|
106
186
|
opts.banner = "Usage: rigor check [options] [paths]"
|
|
@@ -110,6 +190,22 @@ module Rigor
|
|
|
110
190
|
opts.on("--cache-stats", "Print on-disk cache inventory at end of run") { options[:cache_stats] = true }
|
|
111
191
|
opts.on("--clear-cache", "Remove the .rigor/cache directory before running") { options[:clear_cache] = true }
|
|
112
192
|
opts.on("--no-cache", "Disable the persistent cache for this run") { options[:no_cache] = true }
|
|
193
|
+
opts.on("--[no-]stats",
|
|
194
|
+
"Print run summary (files, classes, memory, wall time) to stderr (default: on)") do |value|
|
|
195
|
+
options[:stats] = value
|
|
196
|
+
end
|
|
197
|
+
opts.on("--workers=N", Integer,
|
|
198
|
+
"Dispatch per-file analysis across N Ractor workers (default: 0; sequential)") do |value|
|
|
199
|
+
options[:workers] = value
|
|
200
|
+
end
|
|
201
|
+
opts.on("--tmp-file=PATH",
|
|
202
|
+
"Editor mode: read source bytes from PATH instead of --instead-of (paired)") do |value|
|
|
203
|
+
options[:tmp_file] = value
|
|
204
|
+
end
|
|
205
|
+
opts.on("--instead-of=PATH",
|
|
206
|
+
"Editor mode: the logical project path the buffer represents (paired with --tmp-file)") do |value|
|
|
207
|
+
options[:instead_of] = value
|
|
208
|
+
end
|
|
113
209
|
end
|
|
114
210
|
parser.parse!(@argv)
|
|
115
211
|
options
|
|
@@ -124,6 +220,15 @@ module Rigor
|
|
|
124
220
|
end
|
|
125
221
|
end
|
|
126
222
|
|
|
223
|
+
# Emits the {Analysis::RunStats} summary to STDERR so it
|
|
224
|
+
# doesn't interleave with the diagnostic stream (text or
|
|
225
|
+
# JSON) on STDOUT. JSON consumers can pipe stdout cleanly;
|
|
226
|
+
# interactive users still see the summary on their tty.
|
|
227
|
+
def write_run_stats(stats)
|
|
228
|
+
@err.puts("")
|
|
229
|
+
stats.format(@err)
|
|
230
|
+
end
|
|
231
|
+
|
|
127
232
|
def write_cache_stats(cache_root, runtime_store)
|
|
128
233
|
inv = Cache::Store.disk_inventory(root: cache_root)
|
|
129
234
|
|
|
@@ -286,6 +391,12 @@ module Rigor
|
|
|
286
391
|
SigGenCommand.new(argv: @argv, out: @out, err: @err).run
|
|
287
392
|
end
|
|
288
393
|
|
|
394
|
+
def run_lsp
|
|
395
|
+
require_relative "cli/lsp_command"
|
|
396
|
+
|
|
397
|
+
LspCommand.new(argv: @argv, out: @out, err: @err).run
|
|
398
|
+
end
|
|
399
|
+
|
|
289
400
|
def write_result(result, format)
|
|
290
401
|
case format
|
|
291
402
|
when "json"
|
|
@@ -326,6 +437,7 @@ module Rigor
|
|
|
326
437
|
explain Print the description of one or all CheckRules
|
|
327
438
|
diff Compare current diagnostics to a saved baseline JSON
|
|
328
439
|
sig-gen Emit RBS skeletons inferred from .rb sources (ADR-14)
|
|
440
|
+
lsp Run the Rigor Language Server (LSP) over stdio
|
|
329
441
|
version Print the Rigor version
|
|
330
442
|
help Print this help
|
|
331
443
|
HELP
|
data/lib/rigor/configuration.rb
CHANGED
|
@@ -49,6 +49,16 @@ module Rigor
|
|
|
49
49
|
"disable" => [],
|
|
50
50
|
"libraries" => [],
|
|
51
51
|
"signature_paths" => nil,
|
|
52
|
+
# ADR-17 — project-side monkey-patch pre-evaluation.
|
|
53
|
+
# Empty by default; users opt in by listing explicit files
|
|
54
|
+
# that the analyzer walks before per-file inference so
|
|
55
|
+
# patched-method declarations are visible across the
|
|
56
|
+
# project (e.g. `lib/core_ext/string_extensions.rb`). Slice 1
|
|
57
|
+
# plumbing only — listed files are validated at config-load
|
|
58
|
+
# time (`pre-eval.file-not-found` on a missing path), but
|
|
59
|
+
# the dispatcher tier consuming the registry lands in
|
|
60
|
+
# slice 2.
|
|
61
|
+
"pre_eval" => [],
|
|
52
62
|
"fold_platform_specific_paths" => false,
|
|
53
63
|
"cache" => {
|
|
54
64
|
"path" => ".rigor/cache"
|
|
@@ -63,6 +73,81 @@ module Rigor
|
|
|
63
73
|
"dependencies" => {
|
|
64
74
|
"source_inference" => [],
|
|
65
75
|
"budget_per_gem" => Configuration::Dependencies::DEFAULT_BUDGET_PER_GEM
|
|
76
|
+
},
|
|
77
|
+
"parallel" => {
|
|
78
|
+
# ADR-15 Phase 4c — when greater than zero, `rigor check`
|
|
79
|
+
# dispatches per-file analysis across N Ractor workers
|
|
80
|
+
# built around {Rigor::Analysis::WorkerSession}.
|
|
81
|
+
# `0` (default) keeps the sequential coordinator path
|
|
82
|
+
# bit-for-bit unchanged. The CLI's `--workers=N` flag
|
|
83
|
+
# and the `RIGOR_RACTOR_WORKERS` env var both override
|
|
84
|
+
# this setting; precedence is CLI > env > config > 0.
|
|
85
|
+
"workers" => 0
|
|
86
|
+
},
|
|
87
|
+
"bundler" => {
|
|
88
|
+
# Open item O4 — target-project Bundler awareness.
|
|
89
|
+
# When `bundle_path:` is set (or auto-detected), Rigor
|
|
90
|
+
# walks `<bundle_path>/ruby/*/gems/*/sig/` and adds each
|
|
91
|
+
# gem-shipped sig directory to `signature_paths:`. With
|
|
92
|
+
# O7's failure-memo in place, conflicts (a vendored sig
|
|
93
|
+
# already declares the same constant) degrade gracefully
|
|
94
|
+
# to "no RBS env" with a single-line warning naming the
|
|
95
|
+
# offending file, rather than hanging.
|
|
96
|
+
#
|
|
97
|
+
# `bundle_path:` (String, optional): explicit path to the
|
|
98
|
+
# bundler install root (e.g., "vendor/bundle" or an
|
|
99
|
+
# absolute path). Resolved relative to the project root
|
|
100
|
+
# (`paths:`'s base) when relative.
|
|
101
|
+
#
|
|
102
|
+
# `auto_detect:` (Boolean, default true): when no
|
|
103
|
+
# explicit `bundle_path:` is set, try `.bundle/config`'s
|
|
104
|
+
# `BUNDLE_PATH:` first; fall back to `vendor/bundle/`
|
|
105
|
+
# under the project root if it exists. When neither is
|
|
106
|
+
# found, no extra sigs are added — the analyzer sees
|
|
107
|
+
# only rigor's vendored RBS and the user's
|
|
108
|
+
# `signature_paths:`.
|
|
109
|
+
#
|
|
110
|
+
# O4 Layer 3 keys:
|
|
111
|
+
#
|
|
112
|
+
# `lockfile:` (String, optional): explicit path to a
|
|
113
|
+
# `Gemfile.lock`. Resolved relative to the project root
|
|
114
|
+
# when relative. When set (or auto-detected via the
|
|
115
|
+
# `auto_detect:` flag below) Rigor parses the lockfile
|
|
116
|
+
# and uses it to FILTER the bundle-discovered `sig/`
|
|
117
|
+
# directories: only gems whose `(name, version,
|
|
118
|
+
# platform)` matches a lockfile entry are admitted to
|
|
119
|
+
# `signature_paths:`. Stale or out-of-band gems sitting
|
|
120
|
+
# in the bundle install tree are silently dropped.
|
|
121
|
+
#
|
|
122
|
+
# `auto_detect:` (Boolean, also gates the lockfile
|
|
123
|
+
# search): when true and `lockfile:` is nil, look for
|
|
124
|
+
# `<project_root>/Gemfile.lock`.
|
|
125
|
+
"bundle_path" => nil,
|
|
126
|
+
"auto_detect" => true,
|
|
127
|
+
"lockfile" => nil
|
|
128
|
+
},
|
|
129
|
+
"rbs_collection" => {
|
|
130
|
+
# Open item O4 Layer 3 slice 2 — `rbs collection
|
|
131
|
+
# install` awareness. When the target project has been
|
|
132
|
+
# set up with `rbs collection install`, the resulting
|
|
133
|
+
# `rbs_collection.lock.yaml` carries the resolved (gem,
|
|
134
|
+
# version, source) triples and `.gem_rbs_collection/`
|
|
135
|
+
# holds the downloaded `.rbs` files. Rigor parses the
|
|
136
|
+
# lockfile and auto-feeds each gem's
|
|
137
|
+
# `<collection_root>/<name>/<version>/` directory into
|
|
138
|
+
# `RbsLoader`'s `signature_paths:`. Sources of type
|
|
139
|
+
# `stdlib` are skipped because rigor's bundled
|
|
140
|
+
# `DEFAULT_LIBRARIES` already covers that surface.
|
|
141
|
+
#
|
|
142
|
+
# `lockfile:` (String, optional): explicit path to
|
|
143
|
+
# `rbs_collection.lock.yaml`. Resolved relative to the
|
|
144
|
+
# project root when relative.
|
|
145
|
+
#
|
|
146
|
+
# `auto_detect:` (Boolean, default true): when no
|
|
147
|
+
# explicit `lockfile:` is set, look for
|
|
148
|
+
# `<project_root>/rbs_collection.lock.yaml`.
|
|
149
|
+
"lockfile" => nil,
|
|
150
|
+
"auto_detect" => true
|
|
66
151
|
}
|
|
67
152
|
}.freeze
|
|
68
153
|
|
|
@@ -70,7 +155,7 @@ module Rigor
|
|
|
70
155
|
# MUST be resolved relative to the config file's directory.
|
|
71
156
|
# `exclude:` is intentionally NOT in this list — its entries
|
|
72
157
|
# are glob patterns (`**/vendor/**`), not paths.
|
|
73
|
-
PATH_KEYS = %w[paths signature_paths].freeze
|
|
158
|
+
PATH_KEYS = %w[paths signature_paths pre_eval].freeze
|
|
74
159
|
private_constant :PATH_KEYS
|
|
75
160
|
|
|
76
161
|
attr_reader :target_ruby, :paths, :exclude_patterns, :plugins, :cache_path, :disabled_rules,
|
|
@@ -78,7 +163,10 @@ module Rigor
|
|
|
78
163
|
:plugins_io_network, :plugins_io_allowed_paths,
|
|
79
164
|
:plugins_io_allowed_url_hosts,
|
|
80
165
|
:severity_profile, :severity_overrides,
|
|
81
|
-
:dependencies
|
|
166
|
+
:dependencies, :parallel_workers,
|
|
167
|
+
:bundler_bundle_path, :bundler_auto_detect, :bundler_lockfile,
|
|
168
|
+
:rbs_collection_lockfile, :rbs_collection_auto_detect,
|
|
169
|
+
:pre_eval
|
|
82
170
|
|
|
83
171
|
# Loads a configuration file.
|
|
84
172
|
#
|
|
@@ -214,13 +302,13 @@ module Rigor
|
|
|
214
302
|
private_class_method :load_with_includes, :merge_includes, :resolve_paths_in, :deep_merge,
|
|
215
303
|
:merge_value, :merge_dependencies_hash
|
|
216
304
|
|
|
217
|
-
# rubocop:disable Metrics/AbcSize
|
|
305
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
218
306
|
def initialize(data = DEFAULTS)
|
|
219
307
|
cache = DEFAULTS.fetch("cache").merge(data.fetch("cache", {}))
|
|
220
308
|
plugins_io = DEFAULTS.fetch("plugins_io").merge(data.fetch("plugins_io", {}))
|
|
221
309
|
|
|
222
310
|
@target_ruby = coerce_target_ruby(data.fetch("target_ruby", DEFAULTS.fetch("target_ruby")))
|
|
223
|
-
@paths = Array(data.fetch("paths", DEFAULTS.fetch("paths"))).map(&:to_s)
|
|
311
|
+
@paths = Array(data.fetch("paths", DEFAULTS.fetch("paths"))).map(&:to_s).freeze
|
|
224
312
|
user_excludes = Array(data.fetch("exclude", DEFAULTS.fetch("exclude"))).map(&:to_s)
|
|
225
313
|
@exclude_patterns = (BUILTIN_EXCLUDES + user_excludes).uniq.freeze
|
|
226
314
|
@plugins = Array(data.fetch("plugins", DEFAULTS.fetch("plugins"))).map do |entry|
|
|
@@ -230,6 +318,9 @@ module Rigor
|
|
|
230
318
|
@libraries = Array(data.fetch("libraries", DEFAULTS.fetch("libraries"))).map(&:to_s).freeze
|
|
231
319
|
sig_paths = data.fetch("signature_paths", DEFAULTS.fetch("signature_paths"))
|
|
232
320
|
@signature_paths = sig_paths.nil? ? nil : Array(sig_paths).map(&:to_s).freeze
|
|
321
|
+
@pre_eval = expand_pre_eval_entries(
|
|
322
|
+
Array(data.fetch("pre_eval", DEFAULTS.fetch("pre_eval"))).map(&:to_s)
|
|
323
|
+
)
|
|
233
324
|
@fold_platform_specific_paths = data.fetch(
|
|
234
325
|
"fold_platform_specific_paths", DEFAULTS.fetch("fold_platform_specific_paths")
|
|
235
326
|
) == true
|
|
@@ -246,10 +337,32 @@ module Rigor
|
|
|
246
337
|
@dependencies = Dependencies.from_h(
|
|
247
338
|
data.fetch("dependencies", DEFAULTS.fetch("dependencies"))
|
|
248
339
|
)
|
|
340
|
+
parallel = DEFAULTS.fetch("parallel").merge(data.fetch("parallel", {}))
|
|
341
|
+
@parallel_workers = coerce_parallel_workers(parallel.fetch("workers"))
|
|
342
|
+
bundler = DEFAULTS.fetch("bundler").merge(data.fetch("bundler", {}))
|
|
343
|
+
bp = bundler.fetch("bundle_path")
|
|
344
|
+
@bundler_bundle_path = bp.nil? ? nil : bp.to_s.dup.freeze
|
|
345
|
+
@bundler_auto_detect = bundler.fetch("auto_detect") == true
|
|
346
|
+
lf = bundler.fetch("lockfile")
|
|
347
|
+
@bundler_lockfile = lf.nil? ? nil : lf.to_s.dup.freeze
|
|
348
|
+
rbs_collection = DEFAULTS.fetch("rbs_collection").merge(data.fetch("rbs_collection", {}))
|
|
349
|
+
rclf = rbs_collection.fetch("lockfile")
|
|
350
|
+
@rbs_collection_lockfile = rclf.nil? ? nil : rclf.to_s.dup.freeze
|
|
351
|
+
@rbs_collection_auto_detect = rbs_collection.fetch("auto_detect") == true
|
|
352
|
+
# Ractor migration Phase 2a: deep-freeze the
|
|
353
|
+
# Configuration so it is `Ractor.shareable?`. Every
|
|
354
|
+
# ivar above is now either a frozen value (Symbol /
|
|
355
|
+
# nil / Boolean) or an explicitly frozen
|
|
356
|
+
# collection / value object; freezing `self` makes the
|
|
357
|
+
# whole carrier safe to send across Ractor boundaries
|
|
358
|
+
# (and catches accidental post-init mutation in any
|
|
359
|
+
# caller). See
|
|
360
|
+
# `docs/design/20260514-ractor-migration.md`.
|
|
361
|
+
freeze
|
|
249
362
|
end
|
|
250
|
-
# rubocop:enable Metrics/AbcSize
|
|
363
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
251
364
|
|
|
252
|
-
def to_h
|
|
365
|
+
def to_h # rubocop:disable Metrics/MethodLength
|
|
253
366
|
{
|
|
254
367
|
"target_ruby" => target_ruby,
|
|
255
368
|
"paths" => paths,
|
|
@@ -258,6 +371,7 @@ module Rigor
|
|
|
258
371
|
"disable" => disabled_rules,
|
|
259
372
|
"libraries" => libraries,
|
|
260
373
|
"signature_paths" => signature_paths,
|
|
374
|
+
"pre_eval" => pre_eval,
|
|
261
375
|
"fold_platform_specific_paths" => fold_platform_specific_paths,
|
|
262
376
|
"cache" => {
|
|
263
377
|
"path" => cache_path
|
|
@@ -269,12 +383,45 @@ module Rigor
|
|
|
269
383
|
},
|
|
270
384
|
"severity_profile" => severity_profile.to_s,
|
|
271
385
|
"severity_overrides" => severity_overrides.to_h { |k, v| [k, v.to_s] },
|
|
272
|
-
"dependencies" => dependencies.to_h
|
|
386
|
+
"dependencies" => dependencies.to_h,
|
|
387
|
+
"parallel" => {
|
|
388
|
+
"workers" => parallel_workers
|
|
389
|
+
},
|
|
390
|
+
"bundler" => {
|
|
391
|
+
"bundle_path" => bundler_bundle_path,
|
|
392
|
+
"auto_detect" => bundler_auto_detect,
|
|
393
|
+
"lockfile" => bundler_lockfile
|
|
394
|
+
},
|
|
395
|
+
"rbs_collection" => {
|
|
396
|
+
"lockfile" => rbs_collection_lockfile,
|
|
397
|
+
"auto_detect" => rbs_collection_auto_detect
|
|
398
|
+
}
|
|
273
399
|
}
|
|
274
400
|
end
|
|
275
401
|
|
|
276
402
|
private
|
|
277
403
|
|
|
404
|
+
# ADR-17 slice 4 — `pre_eval:` glob expansion. Each entry is
|
|
405
|
+
# accepted as either a literal path (slice 1 contract) OR a
|
|
406
|
+
# `File.fnmatch?`-shaped glob pattern (`lib/core_ext/**/*.rb`).
|
|
407
|
+
# Glob meta characters (`*`, `?`, `[`) trigger `Dir.glob`
|
|
408
|
+
# expansion; the resulting file list is folded into the
|
|
409
|
+
# `pre_eval:` set with `uniq`. Literal entries that don't
|
|
410
|
+
# exist on disk continue to surface as `pre-eval.file-not-found`
|
|
411
|
+
# `:error` (slice 1 behaviour); glob entries that match
|
|
412
|
+
# nothing degrade silently to "no contribution from this
|
|
413
|
+
# entry" so a templated `**` pattern in a fresh project
|
|
414
|
+
# doesn't generate an error per match-less pattern.
|
|
415
|
+
def expand_pre_eval_entries(entries)
|
|
416
|
+
entries.flat_map do |entry|
|
|
417
|
+
glob_pattern?(entry) ? Dir.glob(entry, sort: true) : [entry]
|
|
418
|
+
end.uniq.freeze
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def glob_pattern?(path)
|
|
422
|
+
path.include?("*") || path.include?("?") || path.include?("[")
|
|
423
|
+
end
|
|
424
|
+
|
|
278
425
|
# Accepts either `"rigor-foo"` (gem-name shorthand) or
|
|
279
426
|
# `{ "gem" => "rigor-foo", "id" => "foo", "config" => {...} }`
|
|
280
427
|
# (full form). Returns the canonical hash form so the loader
|
|
@@ -327,6 +474,20 @@ module Rigor
|
|
|
327
474
|
VALID_NETWORK_POLICIES = %i[disabled allowlist].freeze
|
|
328
475
|
private_constant :VALID_NETWORK_POLICIES
|
|
329
476
|
|
|
477
|
+
# ADR-15 Phase 4c — accepts a non-negative Integer (or a
|
|
478
|
+
# string-shaped one from YAML files that miss type
|
|
479
|
+
# annotations). Negative / non-integer values raise so
|
|
480
|
+
# typos / bad YAML fail loudly rather than silently
|
|
481
|
+
# disabling parallelism.
|
|
482
|
+
def coerce_parallel_workers(value)
|
|
483
|
+
integer = Integer(value)
|
|
484
|
+
raise ArgumentError, "parallel.workers must be >= 0, got #{value.inspect}" if integer.negative?
|
|
485
|
+
|
|
486
|
+
integer
|
|
487
|
+
rescue TypeError, ArgumentError => e
|
|
488
|
+
raise ArgumentError, "parallel.workers must be a non-negative Integer, got #{value.inspect} (#{e.message})"
|
|
489
|
+
end
|
|
490
|
+
|
|
330
491
|
def coerce_network_policy(value)
|
|
331
492
|
sym = value.to_sym
|
|
332
493
|
unless VALID_NETWORK_POLICIES.include?(sym)
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Rigor
|
|
6
|
+
class Environment
|
|
7
|
+
# Open item O4 — target-project Bundler awareness.
|
|
8
|
+
#
|
|
9
|
+
# Walks a Bundler-installed gem tree (e.g., the project's
|
|
10
|
+
# `vendor/bundle` or a Docker-mounted bundle root) and
|
|
11
|
+
# returns the per-gem `sig/` directories to feed into
|
|
12
|
+
# `RbsLoader`'s `signature_paths:`. Of the ~3% of gems that
|
|
13
|
+
# ship `sig/` in their gem package today (per the four-project
|
|
14
|
+
# Mastodon Docker bundle-install measurement on 2026-05-15:
|
|
15
|
+
# 10 of 343 gems shipped sig — `prism`, `aws-sdk-s3`,
|
|
16
|
+
# `aws-sdk-kms`, `aws-sdk-core`, `playwright-ruby-client`,
|
|
17
|
+
# `mutex_m`, `webrick`, `base64`, `stoplight`, `ffi`), this
|
|
18
|
+
# discovery surfaces the typed contract the gem author
|
|
19
|
+
# explicitly published.
|
|
20
|
+
#
|
|
21
|
+
# Conflicts with rigor's bundled stdlib RBS (the prism case
|
|
22
|
+
# was the motivating example) degrade gracefully via O7's
|
|
23
|
+
# failure-memo in `RbsLoader#env`: a single warning naming
|
|
24
|
+
# the offending file is emitted and analysis continues with
|
|
25
|
+
# `Dynamic[top]` everywhere rather than hanging.
|
|
26
|
+
#
|
|
27
|
+
# The discovery is intentionally a pure file-system walk —
|
|
28
|
+
# no `Bundler` API call, no `Gemfile.lock` parse — so rigor
|
|
29
|
+
# doesn't need the target project's Bundler context.
|
|
30
|
+
module BundleSigDiscovery
|
|
31
|
+
# Gems already covered by rigor's `DEFAULT_LIBRARIES`
|
|
32
|
+
# (stdlib RBS) plus the `data/vendored_gem_sigs/` bundle.
|
|
33
|
+
# Skipping these from bundle discovery prevents
|
|
34
|
+
# `RBS::DuplicatedDeclarationError` (the prism case was the
|
|
35
|
+
# motivating example — Ruby 4.0 ships prism's RBS in
|
|
36
|
+
# stdlib, and the gem also ships its own `sig/`, so loading
|
|
37
|
+
# both raises on `Prism::BACKEND` etc.).
|
|
38
|
+
#
|
|
39
|
+
# The list is hard-coded for the MVP because it tracks
|
|
40
|
+
# rigor's bundled coverage 1:1. When a new gem is vendored
|
|
41
|
+
# under `data/vendored_gem_sigs/` or added to
|
|
42
|
+
# `DEFAULT_LIBRARIES`, add its name here.
|
|
43
|
+
SKIPPED_GEMS_BY_DEFAULT = Set[
|
|
44
|
+
# DEFAULT_LIBRARIES (lib/rigor/environment.rb)
|
|
45
|
+
"pathname", "optparse", "json", "yaml", "fileutils",
|
|
46
|
+
"tempfile", "tmpdir", "stringio", "forwardable",
|
|
47
|
+
"digest", "securerandom", "uri", "logger", "date",
|
|
48
|
+
"pp", "delegate", "singleton", "observable", "abbrev",
|
|
49
|
+
"find", "tsort", "shellwords", "benchmark", "base64",
|
|
50
|
+
"did_you_mean", "monitor", "mutex_m", "timeout",
|
|
51
|
+
"open3", "erb", "etc", "ipaddr", "bigdecimal",
|
|
52
|
+
"bigdecimal-math", "prettyprint",
|
|
53
|
+
"random-formatter", "time", "open-uri", "resolv",
|
|
54
|
+
"csv", "pstore", "objspace", "io-console", "cgi", "cgi-escape",
|
|
55
|
+
"strscan",
|
|
56
|
+
"prism", "rbs",
|
|
57
|
+
# data/vendored_gem_sigs/
|
|
58
|
+
"pg", "mysql2", "nokogiri", "bcrypt", "redis", "idn-ruby"
|
|
59
|
+
].freeze
|
|
60
|
+
|
|
61
|
+
# @param bundle_path [String, Pathname, nil] explicit path
|
|
62
|
+
# to the bundler install root. When `nil`, falls back to
|
|
63
|
+
# `auto_detect` if `auto_detect:` is true.
|
|
64
|
+
# @param project_root [String] resolution base for relative
|
|
65
|
+
# `bundle_path:` and the auto-detect search.
|
|
66
|
+
# @param auto_detect [Boolean] when true and `bundle_path:`
|
|
67
|
+
# is nil, try `.bundle/config`'s `BUNDLE_PATH:` and
|
|
68
|
+
# `vendor/bundle/` under `project_root`.
|
|
69
|
+
# @param skip_gems [Set<String>] gem names to exclude from
|
|
70
|
+
# discovery. Defaults to {SKIPPED_GEMS_BY_DEFAULT}.
|
|
71
|
+
# @param locked_gems [Hash{String => LockfileResolver::LockedGem}, nil]
|
|
72
|
+
# Optional O4-Layer-3 filter. When non-nil and non-empty,
|
|
73
|
+
# only `sig/` directories whose gem `(name, version,
|
|
74
|
+
# platform)` tuple matches a lockfile entry are returned.
|
|
75
|
+
# Bundle entries absent from the lockfile (or at a drifted
|
|
76
|
+
# version) are silently dropped — the lockfile is treated
|
|
77
|
+
# as the source of truth for "what gems this project
|
|
78
|
+
# actually declares". Pass `nil` (the default) to keep
|
|
79
|
+
# the pre-Layer-3 behaviour of returning every non-skipped
|
|
80
|
+
# `sig/` under the bundle.
|
|
81
|
+
# @return [Array<Pathname>] every `<gem-dir>/sig` directory
|
|
82
|
+
# under the resolved bundle path, minus any whose gem
|
|
83
|
+
# name is in `skip_gems` and (when `locked_gems` is
|
|
84
|
+
# supplied) minus any whose `(name, version, platform)`
|
|
85
|
+
# does not match a lockfile entry.
|
|
86
|
+
def self.discover(bundle_path:, project_root: Dir.pwd, auto_detect: true,
|
|
87
|
+
skip_gems: SKIPPED_GEMS_BY_DEFAULT, locked_gems: nil)
|
|
88
|
+
resolved = resolve_bundle_path(
|
|
89
|
+
bundle_path: bundle_path,
|
|
90
|
+
project_root: project_root,
|
|
91
|
+
auto_detect: auto_detect
|
|
92
|
+
)
|
|
93
|
+
return [] if resolved.nil?
|
|
94
|
+
|
|
95
|
+
# `<bundle>/ruby/X.Y.Z/gems/<name>-<ver>/sig/` is the
|
|
96
|
+
# canonical bundler layout. `*` on the ruby version dir
|
|
97
|
+
# picks up whichever Ruby the bundle was installed for.
|
|
98
|
+
all = Dir.glob(resolved.join("ruby", "*", "gems", "*", "sig")).map { |d| Pathname.new(d) }
|
|
99
|
+
filtered = all.reject { |sig_dir| skip_gems.include?(gem_name_from_sig_path(sig_dir)) }
|
|
100
|
+
return filtered if locked_gems.nil? || locked_gems.empty?
|
|
101
|
+
|
|
102
|
+
expected_dirs = expected_gem_dirs(locked_gems)
|
|
103
|
+
filtered.select { |sig_dir| expected_dirs.include?(sig_dir.parent.basename.to_s) }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# `{name => LockedGem}` → set of canonical bundler gem
|
|
107
|
+
# directory basenames. Pure-Ruby gems install as
|
|
108
|
+
# `<name>-<version>`; platform-specific gems install as
|
|
109
|
+
# `<name>-<version>-<platform>` (e.g. `ffi-1.17.4-aarch64-linux-gnu`).
|
|
110
|
+
# Lockfile platform `"ruby"` is the pure-Ruby case; any
|
|
111
|
+
# other value is treated as a platform tag.
|
|
112
|
+
def self.expected_gem_dirs(locked_gems)
|
|
113
|
+
locked_gems.each_value.with_object(Set.new) do |locked, set|
|
|
114
|
+
base = "#{locked.name}-#{locked.version}"
|
|
115
|
+
set << if locked.platform == "ruby" || locked.platform.empty?
|
|
116
|
+
base
|
|
117
|
+
else
|
|
118
|
+
"#{base}-#{locked.platform}"
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
private_class_method :expected_gem_dirs
|
|
123
|
+
|
|
124
|
+
# `<bundle>/ruby/X.Y.Z/gems/<name>-<ver>/sig` → `<name>`.
|
|
125
|
+
# The gem directory follows the canonical
|
|
126
|
+
# `<name>-<version>` pattern; we strip everything from the
|
|
127
|
+
# last hyphen onwards to recover the name. (Platform-tagged
|
|
128
|
+
# variants like `ffi-1.17.4-aarch64-linux-gnu/` keep their
|
|
129
|
+
# platform suffix in the version part, so the first hyphen
|
|
130
|
+
# from the right is still the name boundary.)
|
|
131
|
+
#
|
|
132
|
+
# Public so the O4 Layer 3 slice-3 coverage report
|
|
133
|
+
# (`RbsCoverageReport`) can classify discovered bundle sigs
|
|
134
|
+
# against locked gem names without re-running discovery.
|
|
135
|
+
def self.gem_name_from_sig_path(sig_dir)
|
|
136
|
+
gem_dir = sig_dir.parent.basename.to_s
|
|
137
|
+
# Strip `-<version>` and any platform suffix. The version
|
|
138
|
+
# always starts with a digit, so split at the first
|
|
139
|
+
# `-` followed by a digit.
|
|
140
|
+
gem_dir.sub(/-\d.*\z/, "")
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Returns `Pathname` resolved bundle path, or `nil` when
|
|
144
|
+
# neither explicit nor auto-detected. Public for the stats
|
|
145
|
+
# banner so end users can see what rigor picked up.
|
|
146
|
+
def self.resolve_bundle_path(bundle_path:, project_root: Dir.pwd, auto_detect: true)
|
|
147
|
+
if bundle_path
|
|
148
|
+
path = Pathname.new(File.expand_path(bundle_path.to_s, project_root))
|
|
149
|
+
return path if path.directory?
|
|
150
|
+
|
|
151
|
+
return nil
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
return nil unless auto_detect
|
|
155
|
+
|
|
156
|
+
detected = auto_detect(project_root: project_root)
|
|
157
|
+
Pathname.new(detected) if detected
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Auto-detection order:
|
|
161
|
+
# 1. `<project_root>/.bundle/config` carries `BUNDLE_PATH:`
|
|
162
|
+
# set by `bundle config set --local path <dir>`.
|
|
163
|
+
# 2. `<project_root>/vendor/bundle/` — the conventional
|
|
164
|
+
# in-tree install location when a developer ran
|
|
165
|
+
# `bundle install --path vendor/bundle`.
|
|
166
|
+
# 3. `nil` — let the caller proceed without bundle sig
|
|
167
|
+
# discovery (rigor's vendored RBS still loads).
|
|
168
|
+
def self.auto_detect(project_root:)
|
|
169
|
+
from_config = read_bundle_config_path(project_root)
|
|
170
|
+
return File.expand_path(from_config, project_root) if from_config
|
|
171
|
+
|
|
172
|
+
vendor = File.join(project_root, "vendor", "bundle")
|
|
173
|
+
return vendor if File.directory?(vendor)
|
|
174
|
+
|
|
175
|
+
nil
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def self.read_bundle_config_path(project_root)
|
|
179
|
+
config_path = File.join(project_root, ".bundle", "config")
|
|
180
|
+
return nil unless File.exist?(config_path)
|
|
181
|
+
|
|
182
|
+
# `.bundle/config` is YAML with all-caps env-style keys.
|
|
183
|
+
# `BUNDLE_PATH:` is the canonical key (Bundler 2.x); the
|
|
184
|
+
# `--path` flag sets it.
|
|
185
|
+
data = YAML.safe_load_file(config_path)
|
|
186
|
+
return nil unless data.is_a?(Hash)
|
|
187
|
+
|
|
188
|
+
data["BUNDLE_PATH"]
|
|
189
|
+
rescue StandardError
|
|
190
|
+
# Malformed `.bundle/config` should not break analysis;
|
|
191
|
+
# silently skip auto-detection.
|
|
192
|
+
nil
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
private_class_method :read_bundle_config_path
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
@@ -67,10 +67,19 @@ module Rigor
|
|
|
67
67
|
|
|
68
68
|
private
|
|
69
69
|
|
|
70
|
+
# ADR-15 Phase 4b — the default registry MUST be
|
|
71
|
+
# `Ractor.shareable?` so worker Ractors that consult
|
|
72
|
+
# `Environment.for_project`'s default `class_registry:`
|
|
73
|
+
# don't trip `Ractor::IsolationError`. The internal
|
|
74
|
+
# `@nominals` / `@class_objects` Hashes are populated
|
|
75
|
+
# via `register`, then `Ractor.make_shareable`
|
|
76
|
+
# recursively freezes the registry, the two Hashes,
|
|
77
|
+
# and confirms every entry (Type::Nominal carriers +
|
|
78
|
+
# core Ruby classes) is itself shareable.
|
|
70
79
|
def build_default
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
80
|
+
registry = new
|
|
81
|
+
CORE_BUILT_INS.each { |klass| registry.register(klass) }
|
|
82
|
+
Ractor.make_shareable(registry)
|
|
74
83
|
end
|
|
75
84
|
end
|
|
76
85
|
|