rigortype 0.1.5 → 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 +36 -50
- 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/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 +120 -0
- data/lib/rigor/cache/store.rb +33 -3
- data/lib/rigor/cli/lsp_command.rb +129 -0
- data/lib/rigor/cli/type_of_command.rb +44 -5
- data/lib/rigor/cli.rb +74 -12
- data/lib/rigor/configuration.rb +38 -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 +45 -2
- data/lib/rigor/environment/reporters.rb +40 -0
- data/lib/rigor/environment.rb +106 -9
- data/lib/rigor/inference/acceptance.rb +48 -3
- data/lib/rigor/inference/expression_typer.rb +47 -0
- 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/receiver_affinity.rb +87 -0
- data/lib/rigor/inference/method_dispatcher.rb +154 -3
- 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_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 +125 -11
- 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 +8 -4
- data/sig/rigor/inference.rbs +2 -0
- data/sig/rigor.rbs +3 -1
- metadata +54 -1
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "optionparser"
|
|
4
4
|
require "prism"
|
|
5
5
|
|
|
6
|
+
require_relative "../analysis/buffer_binding"
|
|
6
7
|
require_relative "../configuration"
|
|
7
8
|
require_relative "../environment"
|
|
8
9
|
require_relative "../scope"
|
|
@@ -38,35 +39,73 @@ module Rigor
|
|
|
38
39
|
# @return [Integer] CLI exit status.
|
|
39
40
|
def run
|
|
40
41
|
options = parse_options
|
|
42
|
+
buffer = resolve_buffer_binding(options)
|
|
43
|
+
return CLI::EXIT_USAGE if buffer == :usage_error
|
|
41
44
|
|
|
42
45
|
target = parse_position_argument(@argv)
|
|
43
46
|
return CLI::EXIT_USAGE if target.nil?
|
|
44
47
|
|
|
45
|
-
execute(target: target, options: options)
|
|
48
|
+
execute(target: target, options: options, buffer: buffer)
|
|
46
49
|
end
|
|
47
50
|
|
|
48
51
|
private
|
|
49
52
|
|
|
50
53
|
def parse_options
|
|
51
|
-
options = { format: "text", trace: false, config: nil }
|
|
54
|
+
options = { format: "text", trace: false, config: nil, tmp_file: nil, instead_of: nil }
|
|
52
55
|
|
|
53
56
|
parser = OptionParser.new do |opts|
|
|
54
57
|
opts.banner = USAGE
|
|
55
58
|
opts.on("--format=FORMAT", "Output format: text or json") { |value| options[:format] = value }
|
|
56
59
|
opts.on("--trace", "Record fail-soft fallbacks via FallbackTracer") { options[:trace] = true }
|
|
57
60
|
opts.on("--config=PATH", "Path to the Rigor configuration file") { |value| options[:config] = value }
|
|
61
|
+
opts.on("--tmp-file=PATH",
|
|
62
|
+
"Editor mode: read source bytes from PATH instead of --instead-of (paired)") do |value|
|
|
63
|
+
options[:tmp_file] = value
|
|
64
|
+
end
|
|
65
|
+
opts.on("--instead-of=PATH",
|
|
66
|
+
"Editor mode: the logical project path the buffer represents (paired with --tmp-file)") do |value|
|
|
67
|
+
options[:instead_of] = value
|
|
68
|
+
end
|
|
58
69
|
end
|
|
59
70
|
parser.parse!(@argv)
|
|
60
71
|
|
|
61
72
|
options
|
|
62
73
|
end
|
|
63
74
|
|
|
64
|
-
|
|
75
|
+
# Mirrors `Rigor::CLI#resolve_buffer_binding` (the `check`
|
|
76
|
+
# path). Returns nil / BufferBinding / :usage_error. The
|
|
77
|
+
# symbol return path lets the caller translate to
|
|
78
|
+
# `CLI::EXIT_USAGE` without raising.
|
|
79
|
+
def resolve_buffer_binding(options)
|
|
80
|
+
tmp = options[:tmp_file]
|
|
81
|
+
instead = options[:instead_of]
|
|
82
|
+
return nil if tmp.nil? && instead.nil?
|
|
83
|
+
|
|
84
|
+
if tmp.nil? || instead.nil?
|
|
85
|
+
@err.puts("--tmp-file and --instead-of must appear together")
|
|
86
|
+
return :usage_error
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
unless File.file?(tmp)
|
|
90
|
+
@err.puts("--tmp-file #{tmp.inspect}: no such file or not readable")
|
|
91
|
+
return :usage_error
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
Rigor::Analysis::BufferBinding.new(logical_path: instead, physical_path: tmp)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def execute(target:, options:, buffer: nil)
|
|
65
98
|
file, line, column = target
|
|
66
|
-
|
|
99
|
+
# Under editor mode the logical `file` may not exist on disk
|
|
100
|
+
# (user editing a new file); the runtime check is only that
|
|
101
|
+
# the BUFFER is readable, which `resolve_buffer_binding`
|
|
102
|
+
# has already enforced. For non-editor mode `file` must
|
|
103
|
+
# exist.
|
|
104
|
+
physical = buffer ? buffer.resolve(file) : file
|
|
105
|
+
return 1 unless file_exists?(buffer ? physical : file)
|
|
67
106
|
|
|
68
107
|
configuration = Configuration.load(options.fetch(:config))
|
|
69
|
-
source = File.read(
|
|
108
|
+
source = File.read(physical)
|
|
70
109
|
parse_result = Prism.parse(source, filepath: file, version: configuration.target_ruby)
|
|
71
110
|
return 1 if parse_errors?(parse_result, file)
|
|
72
111
|
|
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,24 +70,22 @@ 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,
|
|
86
|
-
collect_stats: options.fetch(:stats),
|
|
87
|
-
workers: resolve_workers(options, configuration)
|
|
84
|
+
runner = build_check_runner(
|
|
85
|
+
configuration: configuration, options: options,
|
|
86
|
+
buffer: buffer, cache_root: cache_root
|
|
88
87
|
)
|
|
89
|
-
result = runner.run(paths)
|
|
88
|
+
result = runner.run(@argv.empty? ? configuration.paths : @argv)
|
|
90
89
|
|
|
91
90
|
write_result(result, options.fetch(:format))
|
|
92
91
|
write_run_stats(result.stats) if result.stats
|
|
@@ -94,6 +93,49 @@ module Rigor
|
|
|
94
93
|
result.success? ? 0 : 1
|
|
95
94
|
end
|
|
96
95
|
|
|
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
|
+
|
|
97
139
|
# ADR-15 Phase 4c — resolves the worker count by
|
|
98
140
|
# precedence: CLI `--workers=N` (most explicit) > env
|
|
99
141
|
# `RIGOR_RACTOR_WORKERS` > config `.rigor.yml`
|
|
@@ -112,7 +154,7 @@ module Rigor
|
|
|
112
154
|
configuration.parallel_workers
|
|
113
155
|
end
|
|
114
156
|
|
|
115
|
-
def parse_check_options
|
|
157
|
+
def parse_check_options # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
|
|
116
158
|
options = {
|
|
117
159
|
# `nil` triggers `Configuration.discover` (`.rigor.yml` then
|
|
118
160
|
# `.rigor.dist.yml`); an explicit `--config=PATH` overrides.
|
|
@@ -133,7 +175,12 @@ module Rigor
|
|
|
133
175
|
# `RIGOR_RACTOR_WORKERS` then `.rigor.yml`
|
|
134
176
|
# `parallel.workers:` then 0 (sequential). See
|
|
135
177
|
# `resolve_workers` for the precedence chain.
|
|
136
|
-
workers: nil
|
|
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
|
|
137
184
|
}
|
|
138
185
|
parser = OptionParser.new do |opts|
|
|
139
186
|
opts.banner = "Usage: rigor check [options] [paths]"
|
|
@@ -151,6 +198,14 @@ module Rigor
|
|
|
151
198
|
"Dispatch per-file analysis across N Ractor workers (default: 0; sequential)") do |value|
|
|
152
199
|
options[:workers] = value
|
|
153
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
|
|
154
209
|
end
|
|
155
210
|
parser.parse!(@argv)
|
|
156
211
|
options
|
|
@@ -336,6 +391,12 @@ module Rigor
|
|
|
336
391
|
SigGenCommand.new(argv: @argv, out: @out, err: @err).run
|
|
337
392
|
end
|
|
338
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
|
+
|
|
339
400
|
def write_result(result, format)
|
|
340
401
|
case format
|
|
341
402
|
when "json"
|
|
@@ -376,6 +437,7 @@ module Rigor
|
|
|
376
437
|
explain Print the description of one or all CheckRules
|
|
377
438
|
diff Compare current diagnostics to a saved baseline JSON
|
|
378
439
|
sig-gen Emit RBS skeletons inferred from .rb sources (ADR-14)
|
|
440
|
+
lsp Run the Rigor Language Server (LSP) over stdio
|
|
379
441
|
version Print the Rigor version
|
|
380
442
|
help Print this help
|
|
381
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"
|
|
@@ -145,7 +155,7 @@ module Rigor
|
|
|
145
155
|
# MUST be resolved relative to the config file's directory.
|
|
146
156
|
# `exclude:` is intentionally NOT in this list — its entries
|
|
147
157
|
# are glob patterns (`**/vendor/**`), not paths.
|
|
148
|
-
PATH_KEYS = %w[paths signature_paths].freeze
|
|
158
|
+
PATH_KEYS = %w[paths signature_paths pre_eval].freeze
|
|
149
159
|
private_constant :PATH_KEYS
|
|
150
160
|
|
|
151
161
|
attr_reader :target_ruby, :paths, :exclude_patterns, :plugins, :cache_path, :disabled_rules,
|
|
@@ -155,7 +165,8 @@ module Rigor
|
|
|
155
165
|
:severity_profile, :severity_overrides,
|
|
156
166
|
:dependencies, :parallel_workers,
|
|
157
167
|
:bundler_bundle_path, :bundler_auto_detect, :bundler_lockfile,
|
|
158
|
-
:rbs_collection_lockfile, :rbs_collection_auto_detect
|
|
168
|
+
:rbs_collection_lockfile, :rbs_collection_auto_detect,
|
|
169
|
+
:pre_eval
|
|
159
170
|
|
|
160
171
|
# Loads a configuration file.
|
|
161
172
|
#
|
|
@@ -307,6 +318,9 @@ module Rigor
|
|
|
307
318
|
@libraries = Array(data.fetch("libraries", DEFAULTS.fetch("libraries"))).map(&:to_s).freeze
|
|
308
319
|
sig_paths = data.fetch("signature_paths", DEFAULTS.fetch("signature_paths"))
|
|
309
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
|
+
)
|
|
310
324
|
@fold_platform_specific_paths = data.fetch(
|
|
311
325
|
"fold_platform_specific_paths", DEFAULTS.fetch("fold_platform_specific_paths")
|
|
312
326
|
) == true
|
|
@@ -357,6 +371,7 @@ module Rigor
|
|
|
357
371
|
"disable" => disabled_rules,
|
|
358
372
|
"libraries" => libraries,
|
|
359
373
|
"signature_paths" => signature_paths,
|
|
374
|
+
"pre_eval" => pre_eval,
|
|
360
375
|
"fold_platform_specific_paths" => fold_platform_specific_paths,
|
|
361
376
|
"cache" => {
|
|
362
377
|
"path" => cache_path
|
|
@@ -386,6 +401,27 @@ module Rigor
|
|
|
386
401
|
|
|
387
402
|
private
|
|
388
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
|
+
|
|
389
425
|
# Accepts either `"rigor-foo"` (gem-name shorthand) or
|
|
390
426
|
# `{ "gem" => "rigor-foo", "id" => "foo", "config" => {...} }`
|
|
391
427
|
# (full form). Returns the canonical hash form so the loader
|
|
@@ -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", "idn-ruby", "mysql2", "nokogiri", "pg", "prism", "redis"
|
|
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 = {}
|
|
@@ -165,6 +178,36 @@ module Rigor
|
|
|
165
178
|
# v0.0.9 cache `Cache::Descriptor` regression did.
|
|
166
179
|
end
|
|
167
180
|
|
|
181
|
+
# ADR-20 slice 2e — iterates over every `%a{...}`
|
|
182
|
+
# annotation attached to a class- or module-level
|
|
183
|
+
# declaration in the loaded RBS environment, yielding
|
|
184
|
+
# `(annotation_string, source_location)` pairs. Used by
|
|
185
|
+
# {Rigor::Inference::HktRegistry.scan_rbs_loader} to
|
|
186
|
+
# find `rigor:v1:hkt_register` / `rigor:v1:hkt_define`
|
|
187
|
+
# directives in user-authored overlays and merge them
|
|
188
|
+
# into the per-`Environment` HKT registry. Yields nothing
|
|
189
|
+
# when the env failed to build (fail-soft, same shape as
|
|
190
|
+
# {#each_known_class_name}).
|
|
191
|
+
def each_class_decl_annotation
|
|
192
|
+
return enum_for(:each_class_decl_annotation) unless block_given?
|
|
193
|
+
return if env.nil?
|
|
194
|
+
|
|
195
|
+
env.class_decls.each_value do |entry|
|
|
196
|
+
entry.each_decl do |decl|
|
|
197
|
+
next unless decl.respond_to?(:annotations)
|
|
198
|
+
|
|
199
|
+
decl.annotations.each { |a| yield a.string, a.location }
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
rescue ::RBS::BaseError, ::Ractor::IsolationError
|
|
203
|
+
# fail-soft: matches each_known_class_name's policy.
|
|
204
|
+
# Ractor::IsolationError surfaces when the scan is
|
|
205
|
+
# invoked from a non-main Ractor pool worker before
|
|
206
|
+
# ADR-15's full deep-freeze migration completes — the
|
|
207
|
+
# worker falls back to the base (builtins-only)
|
|
208
|
+
# registry rather than crashing.
|
|
209
|
+
end
|
|
210
|
+
|
|
168
211
|
# Returns a frozen `Hash<String, String>` mapping each loaded
|
|
169
212
|
# class / module name (top-level prefixed) to the file path of
|
|
170
213
|
# 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
|
data/lib/rigor/environment.rb
CHANGED
|
@@ -3,11 +3,16 @@
|
|
|
3
3
|
require_relative "environment/class_registry"
|
|
4
4
|
require_relative "environment/rbs_loader"
|
|
5
5
|
require_relative "environment/reflection"
|
|
6
|
+
require_relative "environment/reporters"
|
|
7
|
+
require_relative "environment/hkt_registry_holder"
|
|
6
8
|
require_relative "environment/bundle_sig_discovery"
|
|
7
9
|
require_relative "environment/lockfile_resolver"
|
|
8
10
|
require_relative "environment/rbs_collection_discovery"
|
|
9
11
|
require_relative "environment/rbs_coverage_report"
|
|
10
12
|
require_relative "inference/synthetic_method_index"
|
|
13
|
+
require_relative "inference/project_patched_methods"
|
|
14
|
+
require_relative "inference/hkt_registry"
|
|
15
|
+
require_relative "builtins/hkt_builtins"
|
|
11
16
|
require_relative "type_node/name_scope"
|
|
12
17
|
require_relative "type_node/resolver_chain"
|
|
13
18
|
|
|
@@ -57,8 +62,8 @@ module Rigor
|
|
|
57
62
|
].freeze
|
|
58
63
|
|
|
59
64
|
attr_reader :class_registry, :rbs_loader, :plugin_registry, :dependency_source_index,
|
|
60
|
-
:
|
|
61
|
-
:synthetic_method_index
|
|
65
|
+
:reporters, :name_scope,
|
|
66
|
+
:synthetic_method_index, :project_patched_methods
|
|
62
67
|
|
|
63
68
|
# @param class_registry [Rigor::Environment::ClassRegistry]
|
|
64
69
|
# @param rbs_loader [Rigor::Environment::RbsLoader, nil] when nil the
|
|
@@ -79,24 +84,105 @@ module Rigor
|
|
|
79
84
|
# sources the dispatcher consults BELOW RBS dispatch.
|
|
80
85
|
# When nil (the default), no dep-source contribution
|
|
81
86
|
# participates and the dispatcher tier is a no-op.
|
|
82
|
-
def initialize(class_registry: ClassRegistry.default, rbs_loader: nil,
|
|
87
|
+
def initialize(class_registry: ClassRegistry.default, rbs_loader: nil, # rubocop:disable Metrics/ParameterLists
|
|
83
88
|
plugin_registry: nil, dependency_source_index: nil,
|
|
84
89
|
rbs_extended_reporter: nil, boundary_cross_reporter: nil,
|
|
85
|
-
synthetic_method_index: nil
|
|
90
|
+
synthetic_method_index: nil, project_patched_methods: nil,
|
|
91
|
+
hkt_registry: nil)
|
|
86
92
|
@class_registry = class_registry
|
|
87
93
|
@rbs_loader = rbs_loader
|
|
88
94
|
@plugin_registry = plugin_registry
|
|
89
95
|
@dependency_source_index = dependency_source_index
|
|
90
|
-
|
|
91
|
-
|
|
96
|
+
# ADR-pending — reporters live in a mutable container so
|
|
97
|
+
# long-lived integrations (LSP `ProjectContext`) can swap
|
|
98
|
+
# them per `Runner.run` without rebuilding the env. The
|
|
99
|
+
# existing `#rbs_extended_reporter` / `#boundary_cross_reporter`
|
|
100
|
+
# accessors below preserve the public lookup shape.
|
|
101
|
+
@reporters = Reporters.new(
|
|
102
|
+
rbs_extended: rbs_extended_reporter,
|
|
103
|
+
boundary_cross: boundary_cross_reporter
|
|
104
|
+
)
|
|
92
105
|
@synthetic_method_index = synthetic_method_index || Inference::SyntheticMethodIndex::EMPTY
|
|
106
|
+
@project_patched_methods = project_patched_methods || Inference::ProjectPatchedMethods::EMPTY
|
|
107
|
+
# ADR-20 slice 2c + 2e — the per-env HKT registry
|
|
108
|
+
# consulted by the reducer when resolving `Type::App`
|
|
109
|
+
# carriers. Defaults to {Inference::HktRegistry::EMPTY};
|
|
110
|
+
# the {.default} / {.for_project} class methods seed it
|
|
111
|
+
# with the bundled builtins (`json::value`, …) plus any
|
|
112
|
+
# `%a{rigor:v1:hkt_register / hkt_define}` annotations
|
|
113
|
+
# the RBS loader exposes. The hkt_registry getter
|
|
114
|
+
# (defined below) MEMOIZES the result of merging the
|
|
115
|
+
# base with the RBS scan so the scan is paid at most
|
|
116
|
+
# once per Environment lifetime — and only when first
|
|
117
|
+
# consulted, leaving fast paths like `rigor check
|
|
118
|
+
# --cache-stats --no-stats` from doing the RBS env
|
|
119
|
+
# build at all.
|
|
120
|
+
@hkt_registry_base = hkt_registry || Inference::HktRegistry::EMPTY
|
|
121
|
+
@hkt_registry_holder = HktRegistryHolder.new
|
|
93
122
|
@name_scope = build_name_scope
|
|
94
123
|
freeze
|
|
95
124
|
end
|
|
96
125
|
|
|
126
|
+
# ADR-20 slices 2e + 6 — lazy HKT registry getter.
|
|
127
|
+
# Merge order on first call: builtins (base) ← plugin
|
|
128
|
+
# manifest aggregation ← RBS env scan. Last-write-wins on
|
|
129
|
+
# URI collisions so user-authored `.rbs` overlays beat
|
|
130
|
+
# plugin entries, which beat the bundled JSON_VALUE.
|
|
131
|
+
# Memoised; single-threaded use only (under the Ractor
|
|
132
|
+
# pool path each worker has its own Environment so
|
|
133
|
+
# cross-worker mutation is impossible; the LSP
|
|
134
|
+
# single-publish-at-a-time invariant serialises here).
|
|
135
|
+
def hkt_registry
|
|
136
|
+
@hkt_registry_holder.fetch do
|
|
137
|
+
with_plugin_overlay = if @plugin_registry.respond_to?(:hkt_overlay_registry)
|
|
138
|
+
@hkt_registry_base.merge(@plugin_registry.hkt_overlay_registry)
|
|
139
|
+
else
|
|
140
|
+
@hkt_registry_base
|
|
141
|
+
end
|
|
142
|
+
Inference::HktRegistry.scan_rbs_loader(
|
|
143
|
+
@rbs_loader,
|
|
144
|
+
base: with_plugin_overlay,
|
|
145
|
+
reporter: rbs_extended_reporter
|
|
146
|
+
)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Backwards-compatible reporter accessors — every existing
|
|
151
|
+
# consumer (rbs_extended, method_dispatcher) calls these. The
|
|
152
|
+
# frozen `@reporters` container is mutable for slot reassignment
|
|
153
|
+
# via {#attach_reporters!} below.
|
|
154
|
+
def rbs_extended_reporter
|
|
155
|
+
@reporters.rbs_extended
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def boundary_cross_reporter
|
|
159
|
+
@reporters.boundary_cross
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Replaces the env's per-run reporter slots. Intended for
|
|
163
|
+
# long-lived integrations (LSP `ProjectContext`) that share one
|
|
164
|
+
# Environment instance across many `Runner.run` calls: each call
|
|
165
|
+
# attaches its own fresh reporter pair so per-call diagnostic
|
|
166
|
+
# events stay scoped to that call rather than accumulating
|
|
167
|
+
# across publishes.
|
|
168
|
+
#
|
|
169
|
+
# Single-threaded use only. Concurrent publishes against one
|
|
170
|
+
# Environment must serialise — the LSP `Server` debouncer +
|
|
171
|
+
# synchronized writer already enforces this for the editor
|
|
172
|
+
# path. The Ractor pool path builds a per-worker Environment
|
|
173
|
+
# and does not reach this surface.
|
|
174
|
+
def attach_reporters!(rbs_extended_reporter:, boundary_cross_reporter:)
|
|
175
|
+
@reporters.rbs_extended = rbs_extended_reporter
|
|
176
|
+
@reporters.boundary_cross = boundary_cross_reporter
|
|
177
|
+
nil
|
|
178
|
+
end
|
|
179
|
+
|
|
97
180
|
class << self
|
|
98
181
|
def default
|
|
99
|
-
@default ||= new(
|
|
182
|
+
@default ||= new(
|
|
183
|
+
rbs_loader: RbsLoader.default,
|
|
184
|
+
hkt_registry: Builtins::HktBuiltins.registry
|
|
185
|
+
).freeze
|
|
100
186
|
end
|
|
101
187
|
|
|
102
188
|
# Builds an Environment that consults the project's local
|
|
@@ -127,7 +213,7 @@ module Rigor
|
|
|
127
213
|
bundler_bundle_path: nil, bundler_auto_detect: false,
|
|
128
214
|
bundler_lockfile: nil,
|
|
129
215
|
rbs_collection_lockfile: nil, rbs_collection_auto_detect: false,
|
|
130
|
-
synthetic_method_index: nil)
|
|
216
|
+
synthetic_method_index: nil, project_patched_methods: nil)
|
|
131
217
|
resolved_paths = signature_paths || default_signature_paths(root)
|
|
132
218
|
# O4 MVP — append per-gem `sig/` directories discovered
|
|
133
219
|
# under the target project's bundler install root. Empty
|
|
@@ -173,13 +259,24 @@ module Rigor
|
|
|
173
259
|
signature_paths: loader_signature_paths,
|
|
174
260
|
cache_store: cache_store
|
|
175
261
|
)
|
|
262
|
+
# ADR-20 slice 2c + 2e — seed hkt_registry with the
|
|
263
|
+
# bundled builtins. The Environment's `#hkt_registry`
|
|
264
|
+
# getter then LAZILY merges in the RBS env scan on
|
|
265
|
+
# first call so fast paths that don't consult HKT
|
|
266
|
+
# (e.g. `rigor check --cache-stats --no-stats`) don't
|
|
267
|
+
# pay the eager env-build cost up front. URI
|
|
268
|
+
# collisions let the user-authored overlay win over
|
|
269
|
+
# the bundled builtin (last-write-wins per ADR-20
|
|
270
|
+
# OQ3 tentative).
|
|
176
271
|
new(
|
|
177
272
|
rbs_loader: loader,
|
|
178
273
|
plugin_registry: plugin_registry,
|
|
179
274
|
dependency_source_index: dependency_source_index,
|
|
180
275
|
rbs_extended_reporter: rbs_extended_reporter,
|
|
181
276
|
boundary_cross_reporter: boundary_cross_reporter,
|
|
182
|
-
synthetic_method_index: synthetic_method_index
|
|
277
|
+
synthetic_method_index: synthetic_method_index,
|
|
278
|
+
project_patched_methods: project_patched_methods,
|
|
279
|
+
hkt_registry: Builtins::HktBuiltins.registry
|
|
183
280
|
)
|
|
184
281
|
end
|
|
185
282
|
# rubocop:enable Metrics/MethodLength, Metrics/ParameterLists
|