rigortype 0.1.5 → 0.1.7
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 +76 -79
- data/lib/rigor/analysis/baseline.rb +347 -0
- data/lib/rigor/analysis/buffer_binding.rb +36 -0
- data/lib/rigor/analysis/check_rules.rb +68 -3
- 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/project_scan.rb +39 -0
- data/lib/rigor/analysis/runner.rb +309 -22
- data/lib/rigor/analysis/worker_session.rb +14 -2
- data/lib/rigor/builtins/hkt_builtins.rb +342 -0
- data/lib/rigor/builtins/static_return_refinements.rb +142 -0
- data/lib/rigor/cache/store.rb +33 -3
- data/lib/rigor/cli/baseline_command.rb +377 -0
- data/lib/rigor/cli/lsp_command.rb +129 -0
- data/lib/rigor/cli/type_of_command.rb +44 -5
- data/lib/rigor/cli.rb +142 -13
- data/lib/rigor/configuration.rb +58 -2
- data/lib/rigor/environment/hkt_registry_holder.rb +33 -0
- data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
- data/lib/rigor/environment/rbs_loader.rb +67 -2
- data/lib/rigor/environment/reporters.rb +40 -0
- data/lib/rigor/environment.rb +119 -9
- data/lib/rigor/flow_contribution/fact.rb +20 -10
- data/lib/rigor/inference/acceptance.rb +48 -3
- data/lib/rigor/inference/expression_typer.rb +64 -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/method_dispatcher/overload_selector.rb +125 -30
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +32 -11
- data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +87 -0
- data/lib/rigor/inference/method_dispatcher.rb +174 -6
- data/lib/rigor/inference/narrowing.rb +103 -1
- 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 +209 -19
- data/lib/rigor/inference/statement_evaluator.rb +172 -11
- data/lib/rigor/inference/synthetic_method_scanner.rb +94 -16
- 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/macro/heredoc_template.rb +127 -13
- data/lib/rigor/plugin/macro/trait_registry.rb +1 -1
- data/lib/rigor/plugin/manifest.rb +54 -7
- data/lib/rigor/plugin/registry.rb +19 -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/type/app.rb +107 -0
- data/lib/rigor/type.rb +1 -0
- data/lib/rigor/version.rb +1 -1
- data/sig/rigor/environment.rbs +10 -4
- data/sig/rigor/inference.rbs +2 -0
- data/sig/rigor.rbs +4 -1
- metadata +56 -1
data/lib/rigor/cli.rb
CHANGED
|
@@ -25,7 +25,9 @@ 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,
|
|
30
|
+
"baseline" => :run_baseline
|
|
29
31
|
}.freeze
|
|
30
32
|
|
|
31
33
|
def self.start(argv = ARGV, out: $stdout, err: $stderr)
|
|
@@ -69,24 +71,24 @@ module Rigor
|
|
|
69
71
|
|
|
70
72
|
def run_check
|
|
71
73
|
require_relative "analysis/runner"
|
|
74
|
+
require_relative "analysis/buffer_binding"
|
|
75
|
+
require_relative "analysis/baseline"
|
|
72
76
|
require_relative "cache/store"
|
|
73
77
|
|
|
74
78
|
options = parse_check_options
|
|
79
|
+
buffer = resolve_buffer_binding(options)
|
|
80
|
+
return EXIT_USAGE if buffer == :usage_error
|
|
75
81
|
|
|
76
82
|
configuration = Configuration.load(options.fetch(:config))
|
|
77
83
|
cache_root = configuration.cache_path
|
|
78
84
|
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
85
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
explain: options.fetch(:explain),
|
|
85
|
-
cache_store: cache_store,
|
|
86
|
-
collect_stats: options.fetch(:stats),
|
|
87
|
-
workers: resolve_workers(options, configuration)
|
|
86
|
+
runner = build_check_runner(
|
|
87
|
+
configuration: configuration, options: options,
|
|
88
|
+
buffer: buffer, cache_root: cache_root
|
|
88
89
|
)
|
|
89
|
-
result = runner.run(paths)
|
|
90
|
+
result = runner.run(@argv.empty? ? configuration.paths : @argv)
|
|
91
|
+
result = apply_baseline_filter(result, configuration, options)
|
|
90
92
|
|
|
91
93
|
write_result(result, options.fetch(:format))
|
|
92
94
|
write_run_stats(result.stats) if result.stats
|
|
@@ -94,6 +96,94 @@ module Rigor
|
|
|
94
96
|
result.success? ? 0 : 1
|
|
95
97
|
end
|
|
96
98
|
|
|
99
|
+
# ADR-22 — apply the baseline filter as the LAST step of
|
|
100
|
+
# the diagnostic pipeline (after `# rigor:disable`,
|
|
101
|
+
# `severity_profile`, etc. — WD6). Resolution order
|
|
102
|
+
# follows WD2 (b):
|
|
103
|
+
#
|
|
104
|
+
# 1. --no-baseline on the CLI → no baseline.
|
|
105
|
+
# 2. --baseline=PATH on the CLI → load that path.
|
|
106
|
+
# 3. .rigor.yml's `baseline: <path>` → load that path.
|
|
107
|
+
# 4. otherwise → no baseline.
|
|
108
|
+
#
|
|
109
|
+
# When the path resolves and loads successfully, the filter
|
|
110
|
+
# replaces `result.diagnostics` with the surfaced set and
|
|
111
|
+
# writes a one-line summary to stderr (WD7) when any
|
|
112
|
+
# diagnostics were silenced. Load failures emit a warning
|
|
113
|
+
# to stderr and fall through to "no baseline" (graceful
|
|
114
|
+
# degradation).
|
|
115
|
+
def apply_baseline_filter(result, configuration, options)
|
|
116
|
+
path = resolve_baseline_path(configuration, options)
|
|
117
|
+
return result if path.nil?
|
|
118
|
+
|
|
119
|
+
baseline = Analysis::Baseline.load(path)
|
|
120
|
+
return result if baseline.nil?
|
|
121
|
+
|
|
122
|
+
surfaced, silenced_count = baseline.filter(result.diagnostics)
|
|
123
|
+
report_baseline_summary(silenced_count, path) if silenced_count.positive?
|
|
124
|
+
Analysis::Result.new(diagnostics: surfaced, stats: result.stats)
|
|
125
|
+
rescue Analysis::Baseline::LoadError => e
|
|
126
|
+
@err.puts("rigor: baseline load failed: #{e.message} (continuing without baseline)")
|
|
127
|
+
result
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# WD2 (b) — resolve effective baseline path.
|
|
131
|
+
def resolve_baseline_path(configuration, options)
|
|
132
|
+
cli_value = options.fetch(:baseline)
|
|
133
|
+
case cli_value
|
|
134
|
+
when false then nil # --no-baseline
|
|
135
|
+
when :unset then configuration.baseline_path # fall through to config
|
|
136
|
+
else cli_value # --baseline=PATH
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def report_baseline_summary(silenced_count, baseline_path)
|
|
141
|
+
@err.puts("rigor: #{silenced_count} diagnostic(s) silenced by baseline #{baseline_path}")
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def build_check_runner(configuration:, options:, buffer:, cache_root:)
|
|
145
|
+
cache_store = options.fetch(:no_cache) ? nil : Cache::Store.new(root: cache_root)
|
|
146
|
+
Analysis::Runner.new(
|
|
147
|
+
configuration: configuration,
|
|
148
|
+
explain: options.fetch(:explain),
|
|
149
|
+
cache_store: cache_store,
|
|
150
|
+
collect_stats: options.fetch(:stats),
|
|
151
|
+
workers: resolve_workers(options, configuration),
|
|
152
|
+
buffer: buffer
|
|
153
|
+
)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Editor-mode CLI envelope. The `--tmp-file=PATH` /
|
|
157
|
+
# `--instead-of=PATH` pair binds an in-flight buffer file to
|
|
158
|
+
# the logical project path it represents (see
|
|
159
|
+
# `docs/design/20260516-editor-mode.md`). Both flags must
|
|
160
|
+
# appear together; either alone is a usage error. The
|
|
161
|
+
# physical file must be readable; missing-file is a usage
|
|
162
|
+
# error too so editors get one consistent failure shape.
|
|
163
|
+
#
|
|
164
|
+
# Returns:
|
|
165
|
+
# - `nil` when neither flag was supplied (legacy path).
|
|
166
|
+
# - `Rigor::Analysis::BufferBinding` when the pair is valid.
|
|
167
|
+
# - `:usage_error` after writing one diagnostic to stderr;
|
|
168
|
+
# the caller MUST translate this to `EXIT_USAGE`.
|
|
169
|
+
def resolve_buffer_binding(options)
|
|
170
|
+
tmp = options[:tmp_file]
|
|
171
|
+
instead = options[:instead_of]
|
|
172
|
+
return nil if tmp.nil? && instead.nil?
|
|
173
|
+
|
|
174
|
+
if tmp.nil? || instead.nil?
|
|
175
|
+
@err.puts("--tmp-file and --instead-of must appear together")
|
|
176
|
+
return :usage_error
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
unless File.file?(tmp)
|
|
180
|
+
@err.puts("--tmp-file #{tmp.inspect}: no such file or not readable")
|
|
181
|
+
return :usage_error
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
Analysis::BufferBinding.new(logical_path: instead, physical_path: tmp)
|
|
185
|
+
end
|
|
186
|
+
|
|
97
187
|
# ADR-15 Phase 4c — resolves the worker count by
|
|
98
188
|
# precedence: CLI `--workers=N` (most explicit) > env
|
|
99
189
|
# `RIGOR_RACTOR_WORKERS` > config `.rigor.yml`
|
|
@@ -112,7 +202,7 @@ module Rigor
|
|
|
112
202
|
configuration.parallel_workers
|
|
113
203
|
end
|
|
114
204
|
|
|
115
|
-
def parse_check_options
|
|
205
|
+
def parse_check_options # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
|
116
206
|
options = {
|
|
117
207
|
# `nil` triggers `Configuration.discover` (`.rigor.yml` then
|
|
118
208
|
# `.rigor.dist.yml`); an explicit `--config=PATH` overrides.
|
|
@@ -133,9 +223,19 @@ module Rigor
|
|
|
133
223
|
# `RIGOR_RACTOR_WORKERS` then `.rigor.yml`
|
|
134
224
|
# `parallel.workers:` then 0 (sequential). See
|
|
135
225
|
# `resolve_workers` for the precedence chain.
|
|
136
|
-
workers: nil
|
|
226
|
+
workers: nil,
|
|
227
|
+
# Editor mode (`docs/design/20260516-editor-mode.md`).
|
|
228
|
+
# Both must appear together; the runner uses the pair
|
|
229
|
+
# to bind an in-flight buffer file to its logical path.
|
|
230
|
+
tmp_file: nil,
|
|
231
|
+
instead_of: nil,
|
|
232
|
+
# ADR-22 — baseline filter. `:unset` means "fall through
|
|
233
|
+
# to `.rigor.yml`'s `baseline:` key"; a String overrides
|
|
234
|
+
# the config; `false` (from `--no-baseline`) suppresses
|
|
235
|
+
# any baseline that the config might name.
|
|
236
|
+
baseline: :unset
|
|
137
237
|
}
|
|
138
|
-
parser = OptionParser.new do |opts|
|
|
238
|
+
parser = OptionParser.new do |opts| # rubocop:disable Metrics/BlockLength
|
|
139
239
|
opts.banner = "Usage: rigor check [options] [paths]"
|
|
140
240
|
opts.on("--config=PATH", "Path to the Rigor configuration file") { |value| options[:config] = value }
|
|
141
241
|
opts.on("--format=FORMAT", "Output format: text or json") { |value| options[:format] = value }
|
|
@@ -151,6 +251,22 @@ module Rigor
|
|
|
151
251
|
"Dispatch per-file analysis across N Ractor workers (default: 0; sequential)") do |value|
|
|
152
252
|
options[:workers] = value
|
|
153
253
|
end
|
|
254
|
+
opts.on("--tmp-file=PATH",
|
|
255
|
+
"Editor mode: read source bytes from PATH instead of --instead-of (paired)") do |value|
|
|
256
|
+
options[:tmp_file] = value
|
|
257
|
+
end
|
|
258
|
+
opts.on("--instead-of=PATH",
|
|
259
|
+
"Editor mode: the logical project path the buffer represents (paired with --tmp-file)") do |value|
|
|
260
|
+
options[:instead_of] = value
|
|
261
|
+
end
|
|
262
|
+
opts.on("--baseline=PATH",
|
|
263
|
+
"ADR-22: load baseline from PATH (overrides .rigor.yml `baseline:`)") do |value|
|
|
264
|
+
options[:baseline] = value
|
|
265
|
+
end
|
|
266
|
+
opts.on("--no-baseline",
|
|
267
|
+
"ADR-22: ignore any configured baseline for this run") do
|
|
268
|
+
options[:baseline] = false
|
|
269
|
+
end
|
|
154
270
|
end
|
|
155
271
|
parser.parse!(@argv)
|
|
156
272
|
options
|
|
@@ -336,6 +452,18 @@ module Rigor
|
|
|
336
452
|
SigGenCommand.new(argv: @argv, out: @out, err: @err).run
|
|
337
453
|
end
|
|
338
454
|
|
|
455
|
+
def run_lsp
|
|
456
|
+
require_relative "cli/lsp_command"
|
|
457
|
+
|
|
458
|
+
LspCommand.new(argv: @argv, out: @out, err: @err).run
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
def run_baseline
|
|
462
|
+
require_relative "cli/baseline_command"
|
|
463
|
+
|
|
464
|
+
BaselineCommand.new(argv: @argv, out: @out, err: @err).run
|
|
465
|
+
end
|
|
466
|
+
|
|
339
467
|
def write_result(result, format)
|
|
340
468
|
case format
|
|
341
469
|
when "json"
|
|
@@ -376,6 +504,7 @@ module Rigor
|
|
|
376
504
|
explain Print the description of one or all CheckRules
|
|
377
505
|
diff Compare current diagnostics to a saved baseline JSON
|
|
378
506
|
sig-gen Emit RBS skeletons inferred from .rb sources (ADR-14)
|
|
507
|
+
lsp Run the Rigor Language Server (LSP) over stdio
|
|
379
508
|
version Print the Rigor version
|
|
380
509
|
help Print this help
|
|
381
510
|
HELP
|
data/lib/rigor/configuration.rb
CHANGED
|
@@ -49,6 +49,24 @@ 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" => [],
|
|
62
|
+
# ADR-22 — baseline file path. nil (default) means no
|
|
63
|
+
# baseline is loaded; the `false` literal is treated as
|
|
64
|
+
# the explicit-disable form for `.rigor.yml`-side override
|
|
65
|
+
# of an upstream `.rigor.dist.yml` `baseline:` declaration.
|
|
66
|
+
# The presence of `.rigor-baseline.yml` on disk alone does
|
|
67
|
+
# NOT activate filtering — the path must be named here
|
|
68
|
+
# (WD2 (b) of ADR-22).
|
|
69
|
+
"baseline" => nil,
|
|
52
70
|
"fold_platform_specific_paths" => false,
|
|
53
71
|
"cache" => {
|
|
54
72
|
"path" => ".rigor/cache"
|
|
@@ -145,7 +163,7 @@ module Rigor
|
|
|
145
163
|
# MUST be resolved relative to the config file's directory.
|
|
146
164
|
# `exclude:` is intentionally NOT in this list — its entries
|
|
147
165
|
# are glob patterns (`**/vendor/**`), not paths.
|
|
148
|
-
PATH_KEYS = %w[paths signature_paths].freeze
|
|
166
|
+
PATH_KEYS = %w[paths signature_paths pre_eval].freeze
|
|
149
167
|
private_constant :PATH_KEYS
|
|
150
168
|
|
|
151
169
|
attr_reader :target_ruby, :paths, :exclude_patterns, :plugins, :cache_path, :disabled_rules,
|
|
@@ -155,7 +173,8 @@ module Rigor
|
|
|
155
173
|
:severity_profile, :severity_overrides,
|
|
156
174
|
:dependencies, :parallel_workers,
|
|
157
175
|
:bundler_bundle_path, :bundler_auto_detect, :bundler_lockfile,
|
|
158
|
-
:rbs_collection_lockfile, :rbs_collection_auto_detect
|
|
176
|
+
:rbs_collection_lockfile, :rbs_collection_auto_detect,
|
|
177
|
+
:pre_eval, :baseline_path
|
|
159
178
|
|
|
160
179
|
# Loads a configuration file.
|
|
161
180
|
#
|
|
@@ -307,6 +326,10 @@ module Rigor
|
|
|
307
326
|
@libraries = Array(data.fetch("libraries", DEFAULTS.fetch("libraries"))).map(&:to_s).freeze
|
|
308
327
|
sig_paths = data.fetch("signature_paths", DEFAULTS.fetch("signature_paths"))
|
|
309
328
|
@signature_paths = sig_paths.nil? ? nil : Array(sig_paths).map(&:to_s).freeze
|
|
329
|
+
@pre_eval = expand_pre_eval_entries(
|
|
330
|
+
Array(data.fetch("pre_eval", DEFAULTS.fetch("pre_eval"))).map(&:to_s)
|
|
331
|
+
)
|
|
332
|
+
@baseline_path = coerce_baseline_path(data.fetch("baseline", DEFAULTS.fetch("baseline")))
|
|
310
333
|
@fold_platform_specific_paths = data.fetch(
|
|
311
334
|
"fold_platform_specific_paths", DEFAULTS.fetch("fold_platform_specific_paths")
|
|
312
335
|
) == true
|
|
@@ -357,6 +380,7 @@ module Rigor
|
|
|
357
380
|
"disable" => disabled_rules,
|
|
358
381
|
"libraries" => libraries,
|
|
359
382
|
"signature_paths" => signature_paths,
|
|
383
|
+
"pre_eval" => pre_eval,
|
|
360
384
|
"fold_platform_specific_paths" => fold_platform_specific_paths,
|
|
361
385
|
"cache" => {
|
|
362
386
|
"path" => cache_path
|
|
@@ -386,6 +410,27 @@ module Rigor
|
|
|
386
410
|
|
|
387
411
|
private
|
|
388
412
|
|
|
413
|
+
# ADR-17 slice 4 — `pre_eval:` glob expansion. Each entry is
|
|
414
|
+
# accepted as either a literal path (slice 1 contract) OR a
|
|
415
|
+
# `File.fnmatch?`-shaped glob pattern (`lib/core_ext/**/*.rb`).
|
|
416
|
+
# Glob meta characters (`*`, `?`, `[`) trigger `Dir.glob`
|
|
417
|
+
# expansion; the resulting file list is folded into the
|
|
418
|
+
# `pre_eval:` set with `uniq`. Literal entries that don't
|
|
419
|
+
# exist on disk continue to surface as `pre-eval.file-not-found`
|
|
420
|
+
# `:error` (slice 1 behaviour); glob entries that match
|
|
421
|
+
# nothing degrade silently to "no contribution from this
|
|
422
|
+
# entry" so a templated `**` pattern in a fresh project
|
|
423
|
+
# doesn't generate an error per match-less pattern.
|
|
424
|
+
def expand_pre_eval_entries(entries)
|
|
425
|
+
entries.flat_map do |entry|
|
|
426
|
+
glob_pattern?(entry) ? Dir.glob(entry, sort: true) : [entry]
|
|
427
|
+
end.uniq.freeze
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
def glob_pattern?(path)
|
|
431
|
+
path.include?("*") || path.include?("?") || path.include?("[")
|
|
432
|
+
end
|
|
433
|
+
|
|
389
434
|
# Accepts either `"rigor-foo"` (gem-name shorthand) or
|
|
390
435
|
# `{ "gem" => "rigor-foo", "id" => "foo", "config" => {...} }`
|
|
391
436
|
# (full form). Returns the canonical hash form so the loader
|
|
@@ -452,6 +497,17 @@ module Rigor
|
|
|
452
497
|
raise ArgumentError, "parallel.workers must be a non-negative Integer, got #{value.inspect} (#{e.message})"
|
|
453
498
|
end
|
|
454
499
|
|
|
500
|
+
# ADR-22 WD2 (b) — `baseline: <path>` activates the file;
|
|
501
|
+
# `baseline: false` is the explicit-disable form (useful in
|
|
502
|
+
# `.rigor.yml` to override an upstream `.rigor.dist.yml`
|
|
503
|
+
# that names a baseline). `nil` (default / absent key) is
|
|
504
|
+
# also "no baseline".
|
|
505
|
+
def coerce_baseline_path(value)
|
|
506
|
+
return nil if value.nil? || value == false
|
|
507
|
+
|
|
508
|
+
value.to_s
|
|
509
|
+
end
|
|
510
|
+
|
|
455
511
|
def coerce_network_policy(value)
|
|
456
512
|
sym = value.to_sym
|
|
457
513
|
unless VALID_NETWORK_POLICIES.include?(sym)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
class Environment
|
|
5
|
+
# ADR-20 slice 2e — mutable single-slot memoization
|
|
6
|
+
# container for the per-Environment HKT registry. Held by
|
|
7
|
+
# {Environment} so the otherwise-frozen instance can
|
|
8
|
+
# still cache a computed value on first access.
|
|
9
|
+
#
|
|
10
|
+
# Concurrent {#fetch} calls from multiple threads against
|
|
11
|
+
# one Environment are NOT serialised here — the LSP
|
|
12
|
+
# single-publish-at-a-time discipline and the Ractor
|
|
13
|
+
# pool's per-worker Environment shape already prevent
|
|
14
|
+
# cross-thread races. If a future caller introduces a
|
|
15
|
+
# multi-threaded reader path against a shared
|
|
16
|
+
# Environment, the synchronisation belongs at that
|
|
17
|
+
# caller's seam, not here.
|
|
18
|
+
class HktRegistryHolder
|
|
19
|
+
def initialize
|
|
20
|
+
@loaded = false
|
|
21
|
+
@value = nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def fetch
|
|
25
|
+
return @value if @loaded
|
|
26
|
+
|
|
27
|
+
@value = yield
|
|
28
|
+
@loaded = true
|
|
29
|
+
@value
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -42,7 +42,7 @@ module Rigor
|
|
|
42
42
|
# enough that hard-coding is acceptable; a directory walk
|
|
43
43
|
# at every call would add stat-cost to no benefit.)
|
|
44
44
|
VENDORED_GEM_NAMES = Set[
|
|
45
|
-
"bcrypt", "idn-ruby", "mysql2", "nokogiri", "pg", "redis"
|
|
45
|
+
"bcrypt", "bundler", "cgi", "did_you_mean", "idn-ruby", "mysql2", "nokogiri", "pg", "prism", "redis", "rubygems"
|
|
46
46
|
].freeze
|
|
47
47
|
|
|
48
48
|
# @param locked_gems [Hash{String => LockfileResolver::LockedGem}]
|
|
@@ -83,7 +83,7 @@ module Rigor
|
|
|
83
83
|
VENDORED_GEM_SIGS_ROOT = File.expand_path(
|
|
84
84
|
"../../../data/vendored_gem_sigs",
|
|
85
85
|
__dir__
|
|
86
|
-
)
|
|
86
|
+
).freeze
|
|
87
87
|
private_constant :VENDORED_GEM_SIGS_ROOT
|
|
88
88
|
|
|
89
89
|
def vendored_gem_sig_paths
|
|
@@ -118,7 +118,20 @@ module Rigor
|
|
|
118
118
|
@libraries = libraries.map(&:to_s).freeze
|
|
119
119
|
@signature_paths = signature_paths.map { |p| Pathname(p) }.freeze
|
|
120
120
|
@cache_store = cache_store
|
|
121
|
-
|
|
121
|
+
# Per-loader memoization bucket. Held as a single
|
|
122
|
+
# mutable Hash so the loader instance itself can be
|
|
123
|
+
# `.freeze`d (per ADR-15 reflection-facade contract)
|
|
124
|
+
# without losing the lazy-memo behaviour. Slot names
|
|
125
|
+
# currently consulted: `:env`, `:env_loaded`,
|
|
126
|
+
# `:env_build_warned`, `:builder`, `:reflection`,
|
|
127
|
+
# `:instance_definitions_table`,
|
|
128
|
+
# `:singleton_definitions_table`. Constructed via
|
|
129
|
+
# `Hash.new` (NOT a `{ ... }` literal) so Rigor's
|
|
130
|
+
# `HashShape` narrowing doesn't infer a fixed key set
|
|
131
|
+
# from the initial state and fold post-initial slot
|
|
132
|
+
# reads (e.g. `@state[:env_loaded]`) to a constant
|
|
133
|
+
# `nil`.
|
|
134
|
+
@state = Hash.new # rubocop:disable Style/EmptyLiteral
|
|
122
135
|
@instance_definition_cache = {}
|
|
123
136
|
@singleton_definition_cache = {}
|
|
124
137
|
@class_known_cache = {}
|
|
@@ -147,6 +160,28 @@ module Rigor
|
|
|
147
160
|
end
|
|
148
161
|
end
|
|
149
162
|
|
|
163
|
+
# Returns true when the named RBS declaration is a Module
|
|
164
|
+
# (`RBS::AST::Declarations::Module`) rather than a Class. The
|
|
165
|
+
# `user_class_fallback_receiver` tier consults this to route
|
|
166
|
+
# `Nominal[M].some_kernel_method` (where M is a module mixin
|
|
167
|
+
# like `PP::ObjectMixin`) through the `Nominal[Object]`
|
|
168
|
+
# fallback, because every concrete includer of M sees Kernel
|
|
169
|
+
# / Object instance methods as part of its own ancestor chain.
|
|
170
|
+
#
|
|
171
|
+
# Returns false for classes, for unknown names, and when the
|
|
172
|
+
# RBS environment failed to build (fail-soft).
|
|
173
|
+
def rbs_module?(name)
|
|
174
|
+
return false if env.nil?
|
|
175
|
+
|
|
176
|
+
rbs_name = parse_type_name(name)
|
|
177
|
+
return false if rbs_name.nil?
|
|
178
|
+
|
|
179
|
+
entry = env.class_decls[rbs_name]
|
|
180
|
+
entry.is_a?(::RBS::Environment::ModuleEntry)
|
|
181
|
+
rescue ::RBS::BaseError
|
|
182
|
+
false
|
|
183
|
+
end
|
|
184
|
+
|
|
150
185
|
# Yields every known class / module / alias name (top-level
|
|
151
186
|
# prefixed) currently loaded into the environment. The cache
|
|
152
187
|
# producer that materialises the known-name set uses this so
|
|
@@ -165,6 +200,36 @@ module Rigor
|
|
|
165
200
|
# v0.0.9 cache `Cache::Descriptor` regression did.
|
|
166
201
|
end
|
|
167
202
|
|
|
203
|
+
# ADR-20 slice 2e — iterates over every `%a{...}`
|
|
204
|
+
# annotation attached to a class- or module-level
|
|
205
|
+
# declaration in the loaded RBS environment, yielding
|
|
206
|
+
# `(annotation_string, source_location)` pairs. Used by
|
|
207
|
+
# {Rigor::Inference::HktRegistry.scan_rbs_loader} to
|
|
208
|
+
# find `rigor:v1:hkt_register` / `rigor:v1:hkt_define`
|
|
209
|
+
# directives in user-authored overlays and merge them
|
|
210
|
+
# into the per-`Environment` HKT registry. Yields nothing
|
|
211
|
+
# when the env failed to build (fail-soft, same shape as
|
|
212
|
+
# {#each_known_class_name}).
|
|
213
|
+
def each_class_decl_annotation
|
|
214
|
+
return enum_for(:each_class_decl_annotation) unless block_given?
|
|
215
|
+
return if env.nil?
|
|
216
|
+
|
|
217
|
+
env.class_decls.each_value do |entry|
|
|
218
|
+
entry.each_decl do |decl|
|
|
219
|
+
next unless decl.respond_to?(:annotations)
|
|
220
|
+
|
|
221
|
+
decl.annotations.each { |a| yield a.string, a.location }
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
rescue ::RBS::BaseError, ::Ractor::IsolationError
|
|
225
|
+
# fail-soft: matches each_known_class_name's policy.
|
|
226
|
+
# Ractor::IsolationError surfaces when the scan is
|
|
227
|
+
# invoked from a non-main Ractor pool worker before
|
|
228
|
+
# ADR-15's full deep-freeze migration completes — the
|
|
229
|
+
# worker falls back to the base (builtins-only)
|
|
230
|
+
# registry rather than crashing.
|
|
231
|
+
end
|
|
232
|
+
|
|
168
233
|
# Returns a frozen `Hash<String, String>` mapping each loaded
|
|
169
234
|
# class / module name (top-level prefixed) to the file path of
|
|
170
235
|
# its FIRST declaration's RBS source. Used by
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
class Environment
|
|
5
|
+
# Mutable container for the per-run analysis reporters
|
|
6
|
+
# ({Rigor::RbsExtended::Reporter} and
|
|
7
|
+
# {Rigor::Analysis::DependencySourceInference::BoundaryCrossReporter}).
|
|
8
|
+
# Held by {Environment} as a single attr; the reporters can be
|
|
9
|
+
# swapped through {Environment#attach_reporters!} so long-lived
|
|
10
|
+
# integrations (the LSP `ProjectContext`, future editor-mode
|
|
11
|
+
# daemons) can share one Environment across many `Runner.run`
|
|
12
|
+
# calls without each call's diagnostic events accumulating into
|
|
13
|
+
# a single reporter pair.
|
|
14
|
+
#
|
|
15
|
+
# Per-publish reset is the contract: at the start of every
|
|
16
|
+
# `Runner.run` in sequential mode, the runner stamps the
|
|
17
|
+
# environment's `Reporters` slot with the runner's own
|
|
18
|
+
# freshly-built reporter pair. Dispatchers / `RbsExtended`
|
|
19
|
+
# consumers continue to write through
|
|
20
|
+
# `environment.rbs_extended_reporter` /
|
|
21
|
+
# `environment.boundary_cross_reporter` — the lookup just hops
|
|
22
|
+
# through the `Reporters` slot rather than reading a frozen
|
|
23
|
+
# ivar.
|
|
24
|
+
#
|
|
25
|
+
# Construction default is `nil` on both slots so existing
|
|
26
|
+
# callers that don't care about reporters (project-default
|
|
27
|
+
# `Environment.default`, test scopes that don't drive
|
|
28
|
+
# dispatch) keep their current behaviour: reporter lookups
|
|
29
|
+
# return nil, and the consumer sites short-circuit on
|
|
30
|
+
# `reporter.nil?`.
|
|
31
|
+
class Reporters
|
|
32
|
+
attr_accessor :rbs_extended, :boundary_cross
|
|
33
|
+
|
|
34
|
+
def initialize(rbs_extended: nil, boundary_cross: nil)
|
|
35
|
+
@rbs_extended = rbs_extended
|
|
36
|
+
@boundary_cross = boundary_cross
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|