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
|
@@ -49,6 +49,13 @@ module Rigor
|
|
|
49
49
|
# miss without holding a loader instance, and the
|
|
50
50
|
# instance-side {#build_env} delegates here so the
|
|
51
51
|
# implementation stays single-rooted.
|
|
52
|
+
#
|
|
53
|
+
# Vendored gem stubs (`data/vendored_gem_sigs/<gem>/`) are
|
|
54
|
+
# loaded on top of `signature_paths` so the per-gem RBS
|
|
55
|
+
# bundled with Rigor itself is in scope for every analysis
|
|
56
|
+
# run. The gem stubs are intentionally read-only and
|
|
57
|
+
# appended LAST so user-supplied `signature_paths` win on
|
|
58
|
+
# name conflicts.
|
|
52
59
|
def build_env_for(libraries:, signature_paths:)
|
|
53
60
|
rbs_loader = RBS::EnvironmentLoader.new
|
|
54
61
|
libraries.each do |library|
|
|
@@ -60,8 +67,32 @@ module Rigor
|
|
|
60
67
|
path = Pathname(path) unless path.is_a?(Pathname)
|
|
61
68
|
rbs_loader.add(path: path) if path.directory?
|
|
62
69
|
end
|
|
70
|
+
vendored_gem_sig_paths.each do |path|
|
|
71
|
+
rbs_loader.add(path: path) if path.directory?
|
|
72
|
+
end
|
|
63
73
|
RBS::Environment.from_loader(rbs_loader).resolve_type_names
|
|
64
74
|
end
|
|
75
|
+
|
|
76
|
+
# Per-gem `data/vendored_gem_sigs/<gem>/` directories that
|
|
77
|
+
# ship with Rigor. Each subdirectory is one gem's RBS surface
|
|
78
|
+
# (the `<gem>.rbs` file is the typical content; `LICENSE.upstream`
|
|
79
|
+
# records provenance). Coverage is deliberately scoped to the
|
|
80
|
+
# native-extension and "everywhere in Rails" gems whose absence
|
|
81
|
+
# dominated `call.undefined-method` noise in the real-world
|
|
82
|
+
# survey at `docs/notes/20260515-real-world-rails-survey.md`.
|
|
83
|
+
VENDORED_GEM_SIGS_ROOT = File.expand_path(
|
|
84
|
+
"../../../data/vendored_gem_sigs",
|
|
85
|
+
__dir__
|
|
86
|
+
).freeze
|
|
87
|
+
private_constant :VENDORED_GEM_SIGS_ROOT
|
|
88
|
+
|
|
89
|
+
def vendored_gem_sig_paths
|
|
90
|
+
return [] unless File.directory?(VENDORED_GEM_SIGS_ROOT)
|
|
91
|
+
|
|
92
|
+
Dir.children(VENDORED_GEM_SIGS_ROOT).map do |gem_dir|
|
|
93
|
+
Pathname(File.join(VENDORED_GEM_SIGS_ROOT, gem_dir))
|
|
94
|
+
end
|
|
95
|
+
end
|
|
65
96
|
end
|
|
66
97
|
|
|
67
98
|
attr_reader :libraries, :signature_paths, :cache_store
|
|
@@ -87,7 +118,20 @@ module Rigor
|
|
|
87
118
|
@libraries = libraries.map(&:to_s).freeze
|
|
88
119
|
@signature_paths = signature_paths.map { |p| Pathname(p) }.freeze
|
|
89
120
|
@cache_store = cache_store
|
|
90
|
-
|
|
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
|
|
91
135
|
@instance_definition_cache = {}
|
|
92
136
|
@singleton_definition_cache = {}
|
|
93
137
|
@class_known_cache = {}
|
|
@@ -122,6 +166,7 @@ module Rigor
|
|
|
122
166
|
# it never recurses back through {#class_known?}.
|
|
123
167
|
def each_known_class_name
|
|
124
168
|
return enum_for(:each_known_class_name) unless block_given?
|
|
169
|
+
return if env.nil?
|
|
125
170
|
|
|
126
171
|
env.class_decls.each_key { |rbs_name| yield rbs_name.to_s }
|
|
127
172
|
env.class_alias_decls.each_key { |rbs_name| yield rbs_name.to_s }
|
|
@@ -133,6 +178,68 @@ module Rigor
|
|
|
133
178
|
# v0.0.9 cache `Cache::Descriptor` regression did.
|
|
134
179
|
end
|
|
135
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
|
+
|
|
211
|
+
# Returns a frozen `Hash<String, String>` mapping each loaded
|
|
212
|
+
# class / module name (top-level prefixed) to the file path of
|
|
213
|
+
# its FIRST declaration's RBS source. Used by
|
|
214
|
+
# {Rigor::Analysis::RunStats} to attribute the type universe
|
|
215
|
+
# between "project sig/" (paths under the configured
|
|
216
|
+
# `signature_paths`) and "bundled" (everything else — RBS
|
|
217
|
+
# core, stdlib libraries, gem-bundled RBS). Each value is a
|
|
218
|
+
# frozen `String` so the whole result is `Ractor.shareable?`
|
|
219
|
+
# — the Phase 4b worker pool ships a snapshot back to the
|
|
220
|
+
# coordinator on the first `:prepare` message.
|
|
221
|
+
def class_decl_paths
|
|
222
|
+
return {}.freeze if env.nil?
|
|
223
|
+
|
|
224
|
+
result = {}
|
|
225
|
+
env.class_decls.each do |rbs_name, entry|
|
|
226
|
+
decl = entry.primary_decl
|
|
227
|
+
next if decl.nil?
|
|
228
|
+
|
|
229
|
+
location = decl.location
|
|
230
|
+
next if location.nil?
|
|
231
|
+
|
|
232
|
+
buffer = location.buffer
|
|
233
|
+
name = buffer.respond_to?(:name) ? buffer.name : nil
|
|
234
|
+
next if name.nil?
|
|
235
|
+
|
|
236
|
+
result[rbs_name.to_s.dup.freeze] = name.to_s.dup.freeze
|
|
237
|
+
end
|
|
238
|
+
result.freeze
|
|
239
|
+
rescue ::RBS::BaseError
|
|
240
|
+
{}.freeze
|
|
241
|
+
end
|
|
242
|
+
|
|
136
243
|
# @return [RBS::Definition, nil] the resolved instance definition
|
|
137
244
|
# for `class_name`, or nil when the class is unknown or its
|
|
138
245
|
# definition cannot be built (RBS may raise on broken hierarchies;
|
|
@@ -250,6 +357,8 @@ module Rigor
|
|
|
250
357
|
# materialises the constant-type table; ordinary callers
|
|
251
358
|
# should keep using {#constant_type} for point lookups.
|
|
252
359
|
def constant_names
|
|
360
|
+
return [] if env.nil?
|
|
361
|
+
|
|
253
362
|
env.constant_decls.keys.map(&:to_s)
|
|
254
363
|
rescue ::RBS::BaseError
|
|
255
364
|
[]
|
|
@@ -262,6 +371,7 @@ module Rigor
|
|
|
262
371
|
# back into the cache when `cache_store` is set).
|
|
263
372
|
def each_constant_decl
|
|
264
373
|
return enum_for(:each_constant_decl) unless block_given?
|
|
374
|
+
return if env.nil?
|
|
265
375
|
|
|
266
376
|
env.constant_decls.each do |rbs_name, entry|
|
|
267
377
|
yield rbs_name.to_s, entry
|
|
@@ -299,26 +409,109 @@ module Rigor
|
|
|
299
409
|
nil
|
|
300
410
|
end
|
|
301
411
|
|
|
412
|
+
# ADR-15 Phase 4b.x — eagerly drives every cached
|
|
413
|
+
# producer so a subsequent worker Ractor can serve all
|
|
414
|
+
# of its RBS queries from the Marshal blob on disk
|
|
415
|
+
# without ever calling `RBS::EnvironmentLoader.new`.
|
|
416
|
+
# The loader path that calls `EnvironmentLoader.new`
|
|
417
|
+
# transitively reads a chain of non-`Ractor.shareable?`
|
|
418
|
+
# module constants
|
|
419
|
+
# (`RBS::EnvironmentLoader::DEFAULT_CORE_ROOT`,
|
|
420
|
+
# `RBS::Repository::DEFAULT_STDLIB_ROOT`,
|
|
421
|
+
# `Gem::Requirement::DefaultRequirement`, …) and trips
|
|
422
|
+
# `Ractor::IsolationError`. Pre-warming the cache on
|
|
423
|
+
# the main Ractor and letting workers consult ONLY the
|
|
424
|
+
# Marshal-loaded blob sidesteps the whole chain.
|
|
425
|
+
#
|
|
426
|
+
# No-op when `cache_store` is nil — without a Store the
|
|
427
|
+
# worker has no choice but to build env via the loader,
|
|
428
|
+
# so the caller MUST ensure pool mode runs with caching
|
|
429
|
+
# enabled. Returns `self` so the call chains cleanly
|
|
430
|
+
# from the `Runner` pre-spawn hook.
|
|
431
|
+
def prewarm
|
|
432
|
+
return self if cache_store.nil?
|
|
433
|
+
|
|
434
|
+
env
|
|
435
|
+
known_class_names_set
|
|
436
|
+
constant_type_table
|
|
437
|
+
type_param_names_table
|
|
438
|
+
ancestor_names_table
|
|
439
|
+
instance_definitions_table
|
|
440
|
+
singleton_definitions_table
|
|
441
|
+
self
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
# ADR-15 Phase 2b — return the loader's read-only
|
|
445
|
+
# query surface as a frozen, `Ractor.shareable?`
|
|
446
|
+
# {Reflection} value object. Built lazily on first
|
|
447
|
+
# access; the loader memoises so repeated calls return
|
|
448
|
+
# the same instance.
|
|
449
|
+
#
|
|
450
|
+
# The Reflection consumes the loader's already-warmed
|
|
451
|
+
# cache producers (or, when no `cache_store` is set,
|
|
452
|
+
# eagerly walks the env). Once constructed, the
|
|
453
|
+
# Reflection carries the derived tables independently
|
|
454
|
+
# and never re-consults the loader — making it safe to
|
|
455
|
+
# share across Ractors while the loader stays per-
|
|
456
|
+
# process / per-Ractor for write-path operations.
|
|
457
|
+
def reflection
|
|
458
|
+
@state[:reflection] ||= begin
|
|
459
|
+
require_relative "reflection"
|
|
460
|
+
Environment::Reflection.new(
|
|
461
|
+
known_class_names: known_class_names_set,
|
|
462
|
+
instance_definitions: instance_definitions_table,
|
|
463
|
+
singleton_definitions: singleton_definitions_table,
|
|
464
|
+
type_param_names: type_param_names_table,
|
|
465
|
+
constant_types: constant_type_table,
|
|
466
|
+
ancestor_names: ancestor_names_table
|
|
467
|
+
)
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
|
|
302
471
|
private
|
|
303
472
|
|
|
304
473
|
def constant_type_table
|
|
305
474
|
@constant_type_table ||= begin
|
|
306
475
|
require_relative "../cache/rbs_constant_table"
|
|
307
|
-
Cache::RbsConstantTable
|
|
476
|
+
fetch_or_compute_producer(Cache::RbsConstantTable)
|
|
308
477
|
end
|
|
309
478
|
end
|
|
310
479
|
|
|
311
480
|
def known_class_names_set
|
|
312
481
|
@known_class_names_set ||= begin
|
|
313
482
|
require_relative "../cache/rbs_known_class_names"
|
|
314
|
-
Cache::RbsKnownClassNames
|
|
483
|
+
fetch_or_compute_producer(Cache::RbsKnownClassNames)
|
|
315
484
|
end
|
|
316
485
|
end
|
|
317
486
|
|
|
318
487
|
def type_param_names_table
|
|
319
488
|
@type_param_names_table ||= begin
|
|
320
489
|
require_relative "../cache/rbs_class_type_param_names"
|
|
321
|
-
Cache::RbsClassTypeParamNames
|
|
490
|
+
fetch_or_compute_producer(Cache::RbsClassTypeParamNames)
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
# ADR-15 Phase 2b — the `Reflection` build path
|
|
495
|
+
# consumes these tables even when `cache_store` is nil
|
|
496
|
+
# (e.g. tests that build a `Reflection` without a
|
|
497
|
+
# persistent cache). The helper routes through the
|
|
498
|
+
# producer's `.fetch` when a store IS available, and
|
|
499
|
+
# falls back to the producer's `.compute` otherwise.
|
|
500
|
+
def fetch_or_compute_producer(producer)
|
|
501
|
+
return producer.fetch(loader: self, store: cache_store) if cache_store
|
|
502
|
+
|
|
503
|
+
producer.send(:compute, self)
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
# ADR-15 Phase 2b — `Hash<String, Array<String>>` of
|
|
507
|
+
# normalised ancestor chains per class. Consumes the
|
|
508
|
+
# existing `RbsClassAncestorTable` producer when
|
|
509
|
+
# `cache_store` is set; falls back to the producer's
|
|
510
|
+
# `compute` otherwise. Used by {#reflection}.
|
|
511
|
+
def ancestor_names_table
|
|
512
|
+
@ancestor_names_table ||= begin
|
|
513
|
+
require_relative "../cache/rbs_class_ancestor_table"
|
|
514
|
+
fetch_or_compute_producer(Cache::RbsClassAncestorTable)
|
|
322
515
|
end
|
|
323
516
|
end
|
|
324
517
|
|
|
@@ -332,6 +525,8 @@ module Rigor
|
|
|
332
525
|
end
|
|
333
526
|
|
|
334
527
|
def translate_constant_decl(rbs_name)
|
|
528
|
+
return nil if env.nil?
|
|
529
|
+
|
|
335
530
|
entry = env.constant_decls[rbs_name]
|
|
336
531
|
return nil unless entry
|
|
337
532
|
|
|
@@ -339,8 +534,41 @@ module Rigor
|
|
|
339
534
|
translated unless translated.is_a?(Type::Bot)
|
|
340
535
|
end
|
|
341
536
|
|
|
537
|
+
# The RBS environment for this loader. Memoised both on
|
|
538
|
+
# success AND on failure: when the env build raises
|
|
539
|
+
# (typically `RBS::DuplicatedDeclarationError` because a
|
|
540
|
+
# `signature_paths:` entry redeclares a constant or class
|
|
541
|
+
# already shipped by stdlib RBS), retrying on every
|
|
542
|
+
# subsequent `env` call would re-parse and re-resolve the
|
|
543
|
+
# whole sig set per AST node touched during analysis,
|
|
544
|
+
# multiplying per-file analysis cost by ~100x. Failures
|
|
545
|
+
# short-circuit to `nil` here and are surfaced to the user
|
|
546
|
+
# via `warn_about_env_build_failure_once` so the broken
|
|
547
|
+
# `signature_paths:` entry is identifiable.
|
|
342
548
|
def env
|
|
343
|
-
@state[:env]
|
|
549
|
+
return @state[:env] if @state[:env_loaded]
|
|
550
|
+
|
|
551
|
+
@state[:env_loaded] = true
|
|
552
|
+
@state[:env] = cache_store ? cached_env : build_env
|
|
553
|
+
rescue ::RBS::BaseError => e
|
|
554
|
+
warn_about_env_build_failure_once(e)
|
|
555
|
+
@state[:env] = nil
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
def warn_about_env_build_failure_once(error)
|
|
559
|
+
return if @state[:env_build_warned]
|
|
560
|
+
|
|
561
|
+
@state[:env_build_warned] = true
|
|
562
|
+
first_line = error.message.to_s.lines.first.to_s.strip
|
|
563
|
+
warn(
|
|
564
|
+
"rigor: RBS environment build failed: #{error.class}: #{first_line}\n " \
|
|
565
|
+
"Likely cause: a `signature_paths:` entry redeclares a constant or class\n " \
|
|
566
|
+
"already shipped by Rigor's bundled RBS (Ruby core / stdlib / gem-bundled\n " \
|
|
567
|
+
"RBS / `data/vendored_gem_sigs/`). Rigor will continue analyzing with no\n " \
|
|
568
|
+
"RBS env in scope, so most type-of queries will return `Dynamic[top]` and\n " \
|
|
569
|
+
"most rule diagnostics will not fire. Remove the conflicting `.rbs` from\n " \
|
|
570
|
+
"your `signature_paths:` to restore type coverage."
|
|
571
|
+
)
|
|
344
572
|
end
|
|
345
573
|
|
|
346
574
|
def cached_env
|
|
@@ -362,7 +590,7 @@ module Rigor
|
|
|
362
590
|
def instance_definitions_table
|
|
363
591
|
@state[:instance_definitions_table] ||= begin
|
|
364
592
|
require_relative "../cache/rbs_instance_definitions"
|
|
365
|
-
Cache::RbsInstanceDefinitions
|
|
593
|
+
fetch_or_compute_producer(Cache::RbsInstanceDefinitions)
|
|
366
594
|
end
|
|
367
595
|
end
|
|
368
596
|
|
|
@@ -373,7 +601,7 @@ module Rigor
|
|
|
373
601
|
def singleton_definitions_table
|
|
374
602
|
@state[:singleton_definitions_table] ||= begin
|
|
375
603
|
require_relative "../cache/rbs_instance_definitions"
|
|
376
|
-
Cache::RbsSingletonDefinitions
|
|
604
|
+
fetch_or_compute_producer(Cache::RbsSingletonDefinitions)
|
|
377
605
|
end
|
|
378
606
|
end
|
|
379
607
|
|
|
@@ -398,6 +626,7 @@ module Rigor
|
|
|
398
626
|
def build_instance_definition(class_name)
|
|
399
627
|
rbs_name = parse_type_name(class_name)
|
|
400
628
|
return nil unless rbs_name
|
|
629
|
+
return nil if env.nil?
|
|
401
630
|
return nil unless env.class_decls.key?(rbs_name)
|
|
402
631
|
|
|
403
632
|
builder.build_instance(rbs_name)
|
|
@@ -408,6 +637,7 @@ module Rigor
|
|
|
408
637
|
def build_singleton_definition(class_name)
|
|
409
638
|
rbs_name = parse_type_name(class_name)
|
|
410
639
|
return nil unless rbs_name
|
|
640
|
+
return nil if env.nil?
|
|
411
641
|
return nil unless env.class_decls.key?(rbs_name)
|
|
412
642
|
|
|
413
643
|
builder.build_singleton(rbs_name)
|
|
@@ -428,6 +658,7 @@ module Rigor
|
|
|
428
658
|
def compute_class_known(name)
|
|
429
659
|
rbs_name = parse_type_name(name)
|
|
430
660
|
return false unless rbs_name
|
|
661
|
+
return false if env.nil?
|
|
431
662
|
|
|
432
663
|
# `RBS::Environment#class_decls` after `resolve_type_names`
|
|
433
664
|
# holds entries for both classes AND modules; the gem unifies
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rigor
|
|
4
|
+
class Environment
|
|
5
|
+
# Frozen, `Ractor.shareable?` read-only RBS query facade.
|
|
6
|
+
# [ADR-15](../../../docs/adr/15-ractor-concurrency.md)
|
|
7
|
+
# Phase 2b extracts the read-only surface of {RbsLoader}
|
|
8
|
+
# into this carrier so future Ractor-isolated workers
|
|
9
|
+
# can share one Reflection across the pool while keeping
|
|
10
|
+
# the per-Ractor mutable accelerator state (per-process
|
|
11
|
+
# memo Hashes) where it belongs.
|
|
12
|
+
#
|
|
13
|
+
# Backing tables (all frozen at construction):
|
|
14
|
+
#
|
|
15
|
+
# - `known_class_names` — `Set<String>` of every
|
|
16
|
+
# class / module / alias name in the loaded RBS
|
|
17
|
+
# environment. Top-level prefixed (`"::Hash"`); plain
|
|
18
|
+
# queries normalise via {#normalise}.
|
|
19
|
+
# - `instance_definitions` —
|
|
20
|
+
# `Hash<String, RBS::Definition>` keyed on
|
|
21
|
+
# `RBS::TypeName#to_s` (top-level prefixed).
|
|
22
|
+
# - `singleton_definitions` — same shape, singleton side.
|
|
23
|
+
# - `type_param_names` —
|
|
24
|
+
# `Hash<String, Array<Symbol>>` of declared type
|
|
25
|
+
# parameters per class.
|
|
26
|
+
# - `constant_types` — `Hash<String, Rigor::Type>` of
|
|
27
|
+
# translated constant declarations.
|
|
28
|
+
# - `ancestor_names` — `Hash<String, Array<String>>` of
|
|
29
|
+
# normalised ancestor chains per class.
|
|
30
|
+
#
|
|
31
|
+
# Each `Reflection` instance is `frozen?` at construction
|
|
32
|
+
# — every cached table is frozen, `self` is frozen.
|
|
33
|
+
# **NOT** `Ractor.shareable?`: the `instance_definitions`
|
|
34
|
+
# / `singleton_definitions` tables hold upstream
|
|
35
|
+
# `RBS::Definition` objects that transitively reference
|
|
36
|
+
# `RBS::Location` (C-extension state that
|
|
37
|
+
# `Ractor.make_shareable` rejects).
|
|
38
|
+
#
|
|
39
|
+
# The Ractor worker pool (ADR-15 Phase 4) sidesteps this
|
|
40
|
+
# by having each worker build ITS OWN `Reflection` from
|
|
41
|
+
# the shared `Cache::Store`. The cross-Ractor sharing
|
|
42
|
+
# point is the Store's on-disk + in-process memo layer,
|
|
43
|
+
# NOT the Reflection itself. Each Reflection is a per-
|
|
44
|
+
# Ractor immutable read-side view; this carrier exists
|
|
45
|
+
# to GUARANTEE the per-worker view never mutates after
|
|
46
|
+
# construction.
|
|
47
|
+
#
|
|
48
|
+
# If a future RBS release makes `RBS::Location`
|
|
49
|
+
# Ractor-shareable, swapping the `freeze` call below for
|
|
50
|
+
# `Ractor.make_shareable(self)` makes the whole carrier
|
|
51
|
+
# cross-Ractor-shareable in one line. Until then, the
|
|
52
|
+
# frozen-read-only contract is the deliverable.
|
|
53
|
+
class Reflection
|
|
54
|
+
attr_reader :known_class_names, :instance_definitions, :singleton_definitions,
|
|
55
|
+
:type_param_names, :constant_types, :ancestor_names
|
|
56
|
+
|
|
57
|
+
def initialize(known_class_names:, instance_definitions:, singleton_definitions:,
|
|
58
|
+
type_param_names:, constant_types:, ancestor_names:)
|
|
59
|
+
@known_class_names = freeze_set(known_class_names)
|
|
60
|
+
@instance_definitions = freeze_hash(instance_definitions)
|
|
61
|
+
@singleton_definitions = freeze_hash(singleton_definitions)
|
|
62
|
+
@type_param_names = freeze_hash(type_param_names)
|
|
63
|
+
@constant_types = freeze_hash(constant_types)
|
|
64
|
+
@ancestor_names = freeze_hash(ancestor_names)
|
|
65
|
+
freeze
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def class_known?(name)
|
|
69
|
+
@known_class_names.include?(rooted(name))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def instance_definition(name)
|
|
73
|
+
@instance_definitions[rooted(name)]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def singleton_definition(name)
|
|
77
|
+
@singleton_definitions[rooted(name)]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def class_type_param_names(name)
|
|
81
|
+
@type_param_names.fetch(unrooted(name), [])
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def constant_type(name)
|
|
85
|
+
@constant_types[rooted(name)]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Three-valued `(lhs, rhs)` relation:
|
|
89
|
+
# `:equal` / `:subclass` / `:superclass` / `:disjoint` /
|
|
90
|
+
# `:unknown`. Mirrors {RbsHierarchy#class_ordering}'s
|
|
91
|
+
# contract; the Reflection's frozen ancestor table
|
|
92
|
+
# supports the same queries without any in-process
|
|
93
|
+
# mutation.
|
|
94
|
+
def class_ordering(lhs, rhs)
|
|
95
|
+
lhs = unrooted(lhs)
|
|
96
|
+
rhs = unrooted(rhs)
|
|
97
|
+
return :equal if lhs == rhs
|
|
98
|
+
|
|
99
|
+
lhs_ancestors = @ancestor_names[lhs]
|
|
100
|
+
rhs_ancestors = @ancestor_names[rhs]
|
|
101
|
+
return :unknown if lhs_ancestors.nil? || rhs_ancestors.nil? || lhs_ancestors.empty? || rhs_ancestors.empty?
|
|
102
|
+
|
|
103
|
+
if lhs_ancestors.include?(rhs)
|
|
104
|
+
:subclass
|
|
105
|
+
elsif rhs_ancestors.include?(lhs)
|
|
106
|
+
:superclass
|
|
107
|
+
else
|
|
108
|
+
:disjoint
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Yields every known class / module / alias name in
|
|
113
|
+
# the loader's canonical rooted form (`"::Hash"`).
|
|
114
|
+
def each_known_class_name(&)
|
|
115
|
+
@known_class_names.each(&)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
# The cached tables use mixed key conventions inherited
|
|
121
|
+
# from the underlying RBS::TypeName surface: the
|
|
122
|
+
# name-set / definition tables / constant table store
|
|
123
|
+
# rooted `"::Foo"` keys; the type-param / ancestor
|
|
124
|
+
# tables store unrooted `"Foo"`. Reflection's queries
|
|
125
|
+
# normalise per-lookup so callers can pass either form.
|
|
126
|
+
def rooted(name)
|
|
127
|
+
s = name.to_s
|
|
128
|
+
s.start_with?("::") ? s : "::#{s}"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def unrooted(name)
|
|
132
|
+
name.to_s.delete_prefix("::")
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def freeze_set(value)
|
|
136
|
+
return value if value.is_a?(Set) && value.frozen?
|
|
137
|
+
|
|
138
|
+
case value
|
|
139
|
+
when Set then value.dup.freeze
|
|
140
|
+
when Array, Hash then Set.new(value).freeze
|
|
141
|
+
else raise ArgumentError, "expected Set / Array / Hash, got #{value.class}"
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def freeze_hash(value)
|
|
146
|
+
return value if value.frozen?
|
|
147
|
+
|
|
148
|
+
value.dup.freeze
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -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
|