Class: Rigor::Environment::RbsLoader
- Inherits:
-
Object
- Object
- Rigor::Environment::RbsLoader
- Defined in:
- lib/rigor/environment/rbs_loader.rb,
sig/rigor/environment.rbs
Overview
Loads RBS class declarations and method definitions from disk and exposes them to the inference engine in a small, stable surface.
Slice 4 phase 1 only enabled the RBS core signatures shipped with the rbs gem (Object, Integer,
String, Array, ...). Phase 2a adds opt-in stdlib library loading (pathname, json, tempfile,
...) and arbitrary-directory signature loading (typically the project's local sig/ tree). Both are
off by default on RbsLoader.default so the core-only fast path stays cheap; project-aware loading is
opted into through for_project or by constructing a custom loader.
The default instance is shared across the process: building the core RBS environment costs hundreds of
milliseconds and the data is read-only. The shared instance is frozen, but holds a mutable state hash
for lazy memoization of the heavy RBS::Environment and RBS::DefinitionBuilder -- the user-visible
API stays purely functional.
See docs/internal-spec/inference-engine.md for the binding contract. rubocop:disable Metrics/ClassLength
Constant Summary collapse
- SYNTHETIC_NAMESPACE_BUFFER =
Buffer name stamped on the
moduledeclarations synthesized by synthesize_missing_namespaces. Re-read off the built env by #synthesized_namespaces so the analysis layer can surface an:infodiagnostic naming the project's malformed-RBS namespaces — robust across the marshalled env cache, since the sentinel rides along on each synthetic declaration's location. "(rigor: synthesized namespaces)"- SYNTHETIC_STUB_BUFFER =
Buffer name stamped on the stub
class/moduledeclarations synthesized by stub_missing_referenced_types for types the project's RBS references but no loaded signature declares. #synthesized_stub_types reads them back off the built env (so the answer survives the marshalled env cache), and #synthesized_type_names folds them together with the namespace stubs into the set MethodDispatcher resolves toDynamic[Top](no falsecall.undefined-method). "(rigor: synthesized stub types)"- QUARANTINE_WARN_LIMIT =
Cap on how many quarantined
signature_paths:files #warn_about_quarantined_signatures lists by name before collapsing the tail to "… and N more" — a broken generator can emit many, and a wall of parse errors buries the signal. 10- MAX_STUB_PASSES =
ADR-5 robustness, second tier. A project
signature_paths:RBS that references a type no loaded signature declares —def x: () -> DRb::DRbServerwhen thedrbRBS is not available, or a stale reference to its own removedTextbringer::EditorError— makesRBS::DefinitionBuilder#build_instanceraiseNoTypeFoundError, and (per RBS's all-or-nothing per-class build) that single unresolved reference takes down EVERY method on the class, not just the one signature. Observed on shugo/textbringer: oneDRb::DRbServerreference left the wholeTextbringer::Commandsmodule — including its 186-call-sitedefine_commandDSL — resolving asDynamic[Top].We synthesize an empty stub for each such referenced-but- undeclared type so the rest of the class builds. A leaf type is stubbed as
class, its enclosing namespaces asmodule. Stubbed types carry no methods, so a call against a value of a stubbed type would otherwise mis-firecall.undefined-method; MethodDispatcher consults #synthesized_type_names and resolves such calls toDynamic[Top]instead (the same no-false-positive contract as the dependency-source tier).Detection re-uses RBS's own builder (correct by construction): build every PROJECT class and read the missing name out of the raised error. Bounded to
signature_pathsclasses (stdlib / vendored RBS is well-formed) and to MAX_STUB_PASSES iterations — a fresh stub can expose a deeper reference the first build error hid, but empty stubs reference nothing, so the fixpoint converges quickly. 5- CORE_OVERLAY_SIGS_ROOT =
Rigor-owned core-overlay RBS (
data/core_overlay/). Reopens Ruby core classes to add methods upstreamruby/rbsomits but which every concrete value answers at runtime — loaded last so upstream always wins on conflict. Public so the cache descriptor can digest these files into the env-blob key. File.( "../../../data/core_overlay", __dir__ ).freeze
- GEM_OVERLAY_SIGS_ROOT =
Rigor-owned per-gem RBS overlays (
data/gem_overlay/<gem>/), ADR-72. Unlike the unconditionalcore_overlay, each gem's overlay is loaded ONLY when that gem is locked in the project's Gemfile.lock but ships no RBS of its own — Rigor::Environment.for_project decides eligibility and passes the already-filtered gem-name set here. One directory per gem name keeps the membership check a cheapFile.directory?. File.( "../../../data/gem_overlay", __dir__ ).freeze
Instance Attribute Summary collapse
-
#cache_store ⇒ Object?
readonly
Returns the value of attribute cache_store.
-
#libraries ⇒ Array[String]
readonly
Returns the value of attribute libraries.
-
#signature_paths ⇒ Array[String | _ToPath]
readonly
Returns the value of attribute signature_paths.
-
#virtual_rbs ⇒ Object
readonly
Returns the value of attribute virtual_rbs.
Class Method Summary collapse
-
.add_parsed_decls(env, buffer, directives, decls) ⇒ Object
Appends freshly-parsed declarations to an
RBS::Environmentacross the gemspec's supported RBS range (rbs >= 3.0, < 5.0). -
.add_project_signatures(env, signature_paths) ⇒ Object
Load the project's
signature_paths:RBS files intoenvONE FILE AT A TIME, quarantining any file that fails to parse rather than letting it collapse the whole env (thefrom_loaderbatch parse is all-or-nothing — see RbsLoader.build_env_for). -
.add_virtual_rbs(env, virtual_rbs) ⇒ Object
ADR-32 WD4 — merge synthesised-from-source RBS strings into the freshly-built environment.
-
.append_stub_declarations(base_env, missing) ⇒ Object
Adds empty stub declarations for the missing referenced types (and any enclosing namespace they need) to the pre-resolve env, tagged with SYNTHETIC_STUB_BUFFER.
-
.build_env_for(libraries:, signature_paths:, virtual_rbs: []) ⇒ Object
Builds an
RBS::Environmentfrom explicitlibrariesandsignature_paths. -
.collect_missing_namespaces(env) ⇒ Object
Returns the
::-stripped names of every enclosing namespace that some declaration references but no declaration defines, shallowest-first so the synthesized source declaresFoobeforeFoo::Bar. - .core_overlay_sig_paths ⇒ Object
-
.declared_type_names(env) ⇒ Object
The
::-stripped names of every class / module / class-alias declaration already present inenv, so the synthesis paths never re-declare (and thereby duplicate) a real declaration. - .default ⇒ RbsLoader
-
.gem_overlay_sig_paths(gem_names) ⇒ Array<Pathname>
The bundled overlay directory for each gem that ships one; empty when none match or the overlay root is absent.
-
.parse_signature_file(file) ⇒ Object
Parse one project
.rbsinto[buffer, directives, decls], or nil when it is unparseable / unreadable. -
.parseable_rbs?(content) ⇒ Boolean
True when
contentparses as an RBS signature. -
.primary_decl_for(entry) ⇒ Object
Normalises a
class_declsentry's representative declaration across the gemspec's supported RBS range (rbs >= 3.0, < 5.0). -
.project_entry?(entry, project_files) ⇒ Boolean
True when a
class_declsentry was declared in one of the project's own signature files (by declaration location), so the sweep skips the bundled stdlib / vendored universe. -
.project_sig_files(signature_paths) ⇒ Object
The absolute paths of every
.rbsfile under the project'ssignature_paths:(NOT vendored / stdlib RBS — those are well-formed, so attempting to build them would only waste time). -
.quarantined_project_signatures(signature_paths) ⇒ Object
The project
signature_paths:files that FAIL to parse, as[absolute_path, first_error_line]pairs (sorted, deterministic). -
.reset_default! ⇒ void
Used by tests to discard the cached default loader; production code MUST NOT call this.
-
.resolve_quarantining_virtual_collisions(env, virtual_rbs) ⇒ Object
Backstop for a virtual-vs-anything
RBS::DuplicatedDeclarationErrorthat only materialises atresolve_type_names(which rebuilds the env fromsources). - .stub_missing_referenced_types(base_env, resolved, project_files) ⇒ Object
-
.synthesize_missing_namespaces(env) ⇒ Object
Robustness (ADR-5): a project whose RBS declares qualified names (
class Foo::Bar) without ever declaring the enclosing namespace (module Foo) is invalid by upstream RBS rules —RBS::DefinitionBuilder#build_instanceraisesNoTypeFoundError: Could not find ::Foo, which the loader's fail-soft rescue turns into a silent dispatch miss (every method on every such class degrades toDynamic[Top]). -
.unresolved_referenced_types(env, project_files) ⇒ Object
Builds every project class (instance + singleton side) and returns the
::-stripped names of the types whose absence raisedNoTypeFoundError. -
.vendored_gem_names ⇒ Array[String]
Gem names whose RBS ships under
data/vendored_gem_sigs/<gem>/. - .vendored_gem_sig_paths ⇒ Array[Pathname]
Instance Method Summary collapse
-
#class_decl_paths ⇒ Object
Returns a frozen
Hash<String, String>mapping each loaded class / module name (top-level prefixed) to the file path of its FIRST declaration's RBS source. -
#class_known?(name) ⇒ Boolean
Returns true when an RBS class or module declaration with the given name is loaded.
- #class_ordering(lhs, rhs) ⇒ ordering
-
#class_type_param_names(class_name) ⇒ Array[Symbol]
Slice 4 phase 2d.
-
#constant_names ⇒ Array<String>
Every RBS-declared constant name (top-level prefixed, e.g.,
"::Math::PI") currently loaded into the environment. -
#constant_type(name) ⇒ Type::t?
Slice A constant-value lookup.
-
#each_class_decl_annotation ⇒ Object
ADR-20 slice 2e — iterates over every
%a{...}annotation attached to a class- or module-level declaration in the loaded RBS environment, yielding(annotation_string, source_location)pairs. -
#each_class_decl_annotation_with_name ⇒ Object
Like #each_class_decl_annotation, but also yields the owning class / module's RBS name as the first block argument:
(class_name, annotation_string, location). -
#each_constant_decl ⇒ Object
Yields
(name, entry)for every RBS constant declaration currently loaded into the environment. -
#each_known_class_name {|name| ... } ⇒ Object
Yields every known class / module / alias name (top-level prefixed) currently loaded into the environment.
-
#env_build_failure ⇒ Array(String, String, Array<String>)?
The total RBS-environment build failure captured this run, or nil when the env built.
-
#expand_type_alias(rbs_alias) ⇒ RBS::Types::t?
The alias's aliased type one level out, with type arguments substituted for a generic alias (
string→::String | ::_ToStr;range[int?]→::Range[int?] | ::_Range[int?]), or nil for an unresolved name. -
#initialize(libraries: [], signature_paths: [], cache_store: nil, virtual_rbs: []) ⇒ RbsLoader
constructor
A new instance of RbsLoader.
-
#instance_definition(class_name) ⇒ RBS::Definition?
Built on demand from the (possibly cache-loaded) env; the in-memory
@instance_definition_cachekeeps the per-process short-circuit. - #instance_method(class_name:, method_name:) ⇒ RBS::Definition::Method?
-
#instance_method_names(class_name) ⇒ Array<Symbol>?
Every instance-method name on
class_name— own, inherited, and included — as resolved byRBS::DefinitionBuilder. -
#interface_definition(interface_name) ⇒ RBS::Definition?
The built definition for the RBS interface
interface_name(_RewindableStream), whose.methodsare the required members (including interface-ancestor members). -
#interface_method_names(interface_name) ⇒ Array<Symbol>?
Every method name required by the RBS interface
interface_name, or nil when it does not resolve. -
#prewarm ⇒ Object
ADR-15 Phase 4b.x — eagerly drives every cached producer (plus the eager definitions tables, computed from the cached env since ADR-54 WD1) so a subsequent worker Ractor can serve all of its RBS queries without ever calling
RBS::EnvironmentLoader.new. -
#quarantined_signatures ⇒ Array<Array(String, String)>
The project
signature_paths:files that were QUARANTINED this run (they do not parse, so RbsLoader.add_project_signatures skipped them to keep the rest of the env alive), as[absolute_path, first_error_line]pairs. -
#rbs_cache_descriptor ⇒ Object
ADR-54 WD4 — the shared cache descriptor for every RBS-derived producer consulting this loader.
-
#rbs_module?(name) ⇒ Boolean
Returns true when the named RBS declaration is a Module (
RBS::AST::Declarations::Module) rather than a Class. -
#reflection ⇒ Object
ADR-15 Phase 2b — return the loader's read-only query surface as a frozen,
Ractor.shareable?Reflection value object. -
#singleton_definition(class_name) ⇒ RBS::Definition?
Built on demand from the env with a per-process memo; the same on-demand discipline as #instance_definition (ADR-54 WD1).
-
#singleton_method(class_name:, method_name:) ⇒ RBS::Definition::Method?
The class method on
class_name. -
#synthesized_namespaces ⇒ Object
The enclosing namespaces RbsLoader.synthesize_missing_namespaces had to invent because the project's
signature_paths:RBS declared qualified names (class Foo::Bar) without ever declaringFoo. -
#synthesized_stub_types ⇒ Object
The referenced-but-undeclared types RbsLoader.stub_missing_referenced_types stubbed so the project classes that mention them could build (e.g. an unavailable
DRb::DRbServer, or a staleTextbringer::EditorError). -
#synthesized_type_names ⇒ Object
Every type name Rigor invented to make an otherwise-inert / unbuildable project signature set resolve — both the namespace stubs and the referenced-type stubs.
-
#virtual_rbs_collision_quarantined ⇒ Array<String>
Virtual (inline-synthesized) contributions dropped by the collision quarantine (RbsLoader.resolve_quarantining_virtual_collisions): buffer names absent from the built env even though the entry's content is non-empty and parses (a parse failure is the synthesizer's own WD6 skip, reported through the synthesis reporter instead).
Constructor Details
#initialize(libraries: [], signature_paths: [], cache_store: nil, virtual_rbs: []) ⇒ RbsLoader
Returns a new instance of RbsLoader.
502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 |
# File 'lib/rigor/environment/rbs_loader.rb', line 502 def initialize(libraries: [], signature_paths: [], cache_store: nil, virtual_rbs: []) @libraries = libraries.map(&:to_s).freeze @signature_paths = signature_paths.map { |p| Pathname(p) }.freeze @cache_store = cache_store @virtual_rbs = virtual_rbs.map { |name, content| [name.to_s.dup.freeze, content.to_s.dup.freeze].freeze }.freeze # Per-loader memoization bucket. Held as a single mutable Hash so the loader instance itself can be # `.freeze`d (per ADR-15 reflection-facade contract) without losing the lazy-memo behaviour. Slot # names currently consulted: `:env`, `:env_loaded`, `:env_build_warned`, `:builder`, `:reflection`, # `:instance_definitions_table`, `:singleton_definitions_table`. Constructed via `Hash.new` (NOT a # `{ ... }` literal) so Rigor's `HashShape` narrowing doesn't infer a fixed key set from the initial # state and fold post-initial slot reads (e.g. `@state[:env_loaded]`) to a constant `nil`. @state = Hash.new # rubocop:disable Style/EmptyLiteral @instance_definition_cache = {} @singleton_definition_cache = {} @class_known_cache = {} @hierarchy = RbsHierarchy.new(self) end |
Instance Attribute Details
#cache_store ⇒ Object? (readonly)
Returns the value of attribute cache_store.
485 486 487 |
# File 'lib/rigor/environment/rbs_loader.rb', line 485 def cache_store @cache_store end |
#libraries ⇒ Array[String] (readonly)
Returns the value of attribute libraries.
485 486 487 |
# File 'lib/rigor/environment/rbs_loader.rb', line 485 def libraries @libraries end |
#signature_paths ⇒ Array[String | _ToPath] (readonly)
Returns the value of attribute signature_paths.
485 486 487 |
# File 'lib/rigor/environment/rbs_loader.rb', line 485 def signature_paths @signature_paths end |
#virtual_rbs ⇒ Object (readonly)
Returns the value of attribute virtual_rbs.
485 486 487 |
# File 'lib/rigor/environment/rbs_loader.rb', line 485 def virtual_rbs @virtual_rbs end |
Class Method Details
.add_parsed_decls(env, buffer, directives, decls) ⇒ Object
Appends freshly-parsed declarations to an RBS::Environment across the gemspec's supported RBS
range (rbs >= 3.0, < 5.0). RBS 4.x wraps the declarations in an RBS::Source::RBS and takes them
through env.add_source; RBS 3.x has neither RBS::Source nor add_source and instead registers
them with env.add_signature(buffer:, directives:, decls:) (a bare env << decl is NOT enough —
it skips the signatures table that resolve_type_names rebuilds from, so the synthesized
declarations silently vanish on resolve). Without this guard the synthesis paths
(synthesize_missing_namespaces, append_stub_declarations, add_virtual_rbs) crashed under RBS
3.x with uninitialized constant RBS::Source.
315 316 317 318 319 320 321 322 323 324 325 |
# File 'lib/rigor/environment/rbs_loader.rb', line 315 def add_parsed_decls(env, buffer, directives, decls) decls ||= [] directives ||= [] if env.respond_to?(:add_source) env.add_source(::RBS::Source::RBS.new(buffer, directives, decls)) elsif env.respond_to?(:add_signature) env.add_signature(buffer: buffer, directives: directives, decls: decls) else decls.each { |decl| env << decl } end end |
.add_project_signatures(env, signature_paths) ⇒ Object
Load the project's signature_paths: RBS files into env ONE FILE AT A TIME, quarantining any file
that fails to parse rather than letting it collapse the whole env (the from_loader batch parse is
all-or-nothing — see build_env_for). A quarantined file's declarations are simply absent, so calls
into the types it would have declared read Dynamic[top]; the rest of the project's (and all
bundled) RBS still loads. The user is told which files were skipped via
#warn_about_quarantined_signatures, so this is a graceful degrade, not a silent one.
Buffer names are the file's absolute path (matching project_sig_files) so project_entry? — which
attributes a class_decls entry to the project by buffer name — still recognises these declarations.
Sorted for a deterministic add order (the env feeds the cache, ADR-54).
233 234 235 236 237 238 239 240 241 |
# File 'lib/rigor/environment/rbs_loader.rb', line 233 def add_project_signatures(env, signature_paths) project_sig_files(signature_paths).sort.each do |file| parsed = parse_signature_file(file) next if parsed.nil? # quarantined (unparseable) or unreadable — skip so the env survives buffer, directives, decls = parsed add_parsed_decls(env, buffer, directives, decls) end end |
.add_virtual_rbs(env, virtual_rbs) ⇒ Object
ADR-32 WD4 — merge synthesised-from-source RBS strings into the freshly-built environment. Each
entry is a [virtual_filename, rbs_source] pair. virtual_filename is purely for diagnostic
provenance (RBS parse errors cite it) — it is not a real file path. Per WD6 the synthesizer-emit
path is responsible for catching its own parse errors and returning nil rather than garbage; this
method assumes its input is parseable and only rescues RBS::ParsingError as a fail-soft.
387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 |
# File 'lib/rigor/environment/rbs_loader.rb', line 387 def add_virtual_rbs(env, virtual_rbs) return if virtual_rbs.nil? || virtual_rbs.empty? virtual_rbs.each do |filename, content| next if content.nil? || content.empty? buffer = ::RBS::Buffer.new(name: filename.to_s, content: content.to_s) _, directives, decls = ::RBS::Parser.parse_signature(buffer) add_parsed_decls(env, buffer, directives, decls) rescue ::RBS::BaseError # WD6 fail-soft: a single broken virtual RBS contribution does not pull the whole env down — for # a parse error, skipping the entry is enough. But `RBS::Environment#add_source` appends to # `env.sources` BEFORE inserting decls, so when the raise is a mid-insert # `RBS::DuplicatedDeclarationError` (the entry declares a constant the project's own `sig/` # already declares — the expected state for a project migrating between `sig/` and inline # annotations, not an authoring error), the POISONED SOURCE is left behind, and # `resolve_type_names` — which rebuilds the env from `sources` — re-raises the same error outside # this rescue and collapses the WHOLE env to nil (measured on herb: 1,490 classes → 0, `require` # itself stopped resolving, 74 false `call.unresolved-toplevel`). Make the skip transactional: # drop the poisoned source. The explicit `.rbs` declaration wins — the spec keeps standalone # `.rbs` files "the preferred place for complete type definitions" (`overview.md`) — and # {#warn_about_virtual_rbs_collisions} names the dropped file. `sources` is the rbs 4.x shape; # under the 3.x API this degrades to today's behaviour. env.sources.reject! { |source| source.buffer.name == buffer.name } if env.respond_to?(:sources) end end |
.append_stub_declarations(base_env, missing) ⇒ Object
Adds empty stub declarations for the missing referenced types (and any enclosing namespace they
need) to the pre-resolve env, tagged with SYNTHETIC_STUB_BUFFER. A name that is a prefix of
another name is declared module (it is a namespace); a leaf is declared class (referenced types
appear in instance position far more often than as mixins).
Names already declared in base_env are skipped — exactly the declared.include? guard
collect_missing_namespaces applies. Without it, stubbing a nested reference (Foo::Bar::Baz)
re-emits its enclosing prefix (Foo::Bar) as a module, and when that prefix is already a class
in the project's own sig/ the kind mismatch makes resolve_type_names raise
RBS::DuplicatedDeclarationError, collapsing the WHOLE env to nil (every type-of query then
degrades to Dynamic[Top]). One malformed .rbs must not disproportionately blind the analysis: a
subclass sig that references an inherited nested type (class GitAdapter; def x: () -> GitAdapter::Revision) was the real-world trigger — see the 2026-07-04 redmine onboarding note.
351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 |
# File 'lib/rigor/environment/rbs_loader.rb', line 351 def append_stub_declarations(base_env, missing) declared = declared_type_names(base_env) names = missing.to_set missing.each do |name| parts = name.split("::") (1...parts.length).each { |i| names << parts[0, i].join("::") } end names = names.reject { |name| declared.include?(name) }.to_set return if names.empty? source = names.sort_by { |n| n.count(":") }.map do |name| keyword = names.any? { |other| other != name && other.start_with?("#{name}::") } ? "module" : "class" "#{keyword} #{name}\nend\n" end.join buffer = ::RBS::Buffer.new(name: SYNTHETIC_STUB_BUFFER, content: source) _, directives, decls = ::RBS::Parser.parse_signature(buffer) add_parsed_decls(base_env, buffer, directives, decls) rescue ::RBS::BaseError nil end |
.build_env_for(libraries:, signature_paths:, virtual_rbs: []) ⇒ Object
Builds an RBS::Environment from explicit libraries and signature_paths. Stateless surface so
the v0.0.9 Cache::RbsEnvironment producer can build an env on cache miss without holding a loader
instance, and the instance-side #build_env delegates here so the implementation stays
single-rooted.
Vendored gem stubs (data/vendored_gem_sigs/<gem>/) are loaded on top of signature_paths so the
per-gem RBS bundled with Rigor itself is in scope for every analysis run. The gem stubs are
intentionally read-only and appended LAST so user-supplied signature_paths win on name conflicts.
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
# File 'lib/rigor/environment/rbs_loader.rb', line 66 def build_env_for(libraries:, signature_paths:, virtual_rbs: []) rbs_loader = RBS::EnvironmentLoader.new libraries.each do |library| next unless rbs_loader.has_library?(library: library, version: nil) rbs_loader.add(library: library, version: nil) end # Project `signature_paths:` are loaded per-file by {.add_project_signatures} AFTER `from_loader`, # NOT added to the loader here: `RBS::Environment.from_loader` parses every added file all-or-nothing, # so one unparseable user `.rbs` raises `RBS::ParsingError` and collapses the WHOLE env to nil (every # type-of query then degrades to `Dynamic[top]` — the "sig looks harmful" failure of the 2026-07-06 # mastodon coverage note). Per-file loading quarantines the broken file instead. Vendored / core-overlay # sigs are Rigor-shipped and trusted, so they stay on the loader's fast batch path. vendored_gem_sig_paths.each do |path| rbs_loader.add(path: path) if path.directory? end # Rigor-owned core overlay — loaded LAST so an upstream declaration always wins on conflict; these # reopenings only fill genuine holes (e.g. `Numeric#to_f`/`to_i`/`to_r`, which upstream RBS # declares on the concrete subclasses but not on the abstract `Numeric` that Rigor's # arithmetic-chain widening produces). .each do |path| rbs_loader.add(path: path) if path.directory? end env = RBS::Environment.from_loader(rbs_loader) add_project_signatures(env, signature_paths) add_virtual_rbs(env, virtual_rbs) synthesize_missing_namespaces(env) env, resolved = resolve_quarantining_virtual_collisions(env, virtual_rbs) stub_missing_referenced_types(env, resolved, project_sig_files(signature_paths)) end |
.collect_missing_namespaces(env) ⇒ Object
Returns the ::-stripped names of every enclosing namespace that some declaration references but
no declaration defines, shallowest-first so the synthesized source declares Foo before
Foo::Bar.
197 198 199 200 201 202 203 204 205 206 207 208 209 |
# File 'lib/rigor/environment/rbs_loader.rb', line 197 def collect_missing_namespaces(env) declared = env.class_decls.keys.to_set missing = {} env.class_decls.each_key do |type_name| path = type_name.namespace.path path.each_index do |i| prefix = path[0..i] full = ::RBS::TypeName.parse("::#{prefix.join('::')}") missing[prefix.join("::")] = prefix.length unless declared.include?(full) end end missing.sort_by { |_name, depth| depth }.map(&:first) end |
.core_overlay_sig_paths ⇒ Object
434 435 436 437 438 |
# File 'lib/rigor/environment/rbs_loader.rb', line 434 def return [] unless File.directory?(CORE_OVERLAY_SIGS_ROOT) [Pathname(CORE_OVERLAY_SIGS_ROOT)] end |
.declared_type_names(env) ⇒ Object
The ::-stripped names of every class / module / class-alias declaration already present in
env, so the synthesis paths never re-declare (and thereby duplicate) a real declaration.
374 375 376 377 378 379 380 |
# File 'lib/rigor/environment/rbs_loader.rb', line 374 def declared_type_names(env) names = env.class_decls.keys.map { |n| n.to_s.sub(/\A::/, "") } if env.respond_to?(:class_alias_decls) names.concat(env.class_alias_decls.keys.map { |n| n.to_s.sub(/\A::/, "") }) end names.to_set end |
.default ⇒ RbsLoader
47 48 49 |
# File 'lib/rigor/environment/rbs_loader.rb', line 47 def default @default ||= new.freeze end |
.gem_overlay_sig_paths(gem_names) ⇒ Array<Pathname>
Returns the bundled overlay directory for each gem that ships one; empty when none match or the overlay root is absent.
454 455 456 457 458 459 460 461 |
# File 'lib/rigor/environment/rbs_loader.rb', line 454 def (gem_names) return [] unless File.directory?(GEM_OVERLAY_SIGS_ROOT) gem_names.filter_map do |name| dir = File.join(GEM_OVERLAY_SIGS_ROOT, name.to_s) Pathname(dir) if File.directory?(dir) end end |
.parse_signature_file(file) ⇒ Object
Parse one project .rbs into [buffer, directives, decls], or nil when it is unparseable /
unreadable. Mirrors RBS::EnvironmentLoader#each_signature's per-file parse so the decls register
identically to the loader's batch path.
246 247 248 249 250 251 252 |
# File 'lib/rigor/environment/rbs_loader.rb', line 246 def parse_signature_file(file) buffer = ::RBS::Buffer.new(name: file, content: File.read(file, encoding: "UTF-8")) _buffer, directives, decls = ::RBS::Parser.parse_signature(buffer) [buffer, directives, decls] rescue ::RBS::ParsingError, Errno::ENOENT, Errno::EISDIR, Errno::EACCES nil end |
.parseable_rbs?(content) ⇒ Boolean
True when content parses as an RBS signature. #virtual_rbs_collision_quarantined uses this to
tell a collision-dropped virtual entry (parses, but absent from the env) from a parse-failed one
(the synthesizer's own WD6 skip, reported separately).
100 101 102 103 104 105 |
# File 'lib/rigor/environment/rbs_loader.rb', line 100 def parseable_rbs?(content) ::RBS::Parser.parse_signature(::RBS::Buffer.new(name: "(rigor: virtual parse check)", content: content)) true rescue ::RBS::BaseError false end |
.primary_decl_for(entry) ⇒ Object
Normalises a class_decls entry's representative declaration across the gemspec's supported RBS
range (rbs >= 3.0, < 5.0). RBS 4.x exposes it as entry.primary_decl (the AST declaration
directly); RBS 3.x exposes entry.primary (a wrapper whose #decl is the AST declaration).
Returns the AST declaration, or nil when neither accessor is present. Without this guard,
class_decl_paths crashed under RBS 3.x with undefined method 'primary_decl'.
298 299 300 301 302 303 304 305 |
# File 'lib/rigor/environment/rbs_loader.rb', line 298 def primary_decl_for(entry) if entry.respond_to?(:primary_decl) entry.primary_decl elsif entry.respond_to?(:primary) primary = entry.primary primary.respond_to?(:decl) ? primary.decl : primary end end |
.project_entry?(entry, project_files) ⇒ Boolean
True when a class_decls entry was declared in one of the project's own signature files (by
declaration location), so the sweep skips the bundled stdlib / vendored universe.
329 330 331 332 333 334 335 336 |
# File 'lib/rigor/environment/rbs_loader.rb', line 329 def project_entry?(entry, project_files) decl = primary_decl_for(entry) location = decl&.location buffer_name = location&.buffer&.name return false unless buffer_name project_files.include?(File.(buffer_name.to_s)) end |
.project_sig_files(signature_paths) ⇒ Object
The absolute paths of every .rbs file under the project's signature_paths: (NOT vendored /
stdlib RBS — those are well-formed, so attempting to build them would only waste time). Used to
scope the referenced-type build sweep.
214 215 216 217 218 219 220 221 |
# File 'lib/rigor/environment/rbs_loader.rb', line 214 def project_sig_files(signature_paths) signature_paths.flat_map do |path| path = Pathname(path) unless path.is_a?(Pathname) next [] unless path.directory? Dir.glob(path.join("**", "*.rbs")).map { |p| File.(p) } end.to_set end |
.quarantined_project_signatures(signature_paths) ⇒ Object
The project signature_paths: files that FAIL to parse, as [absolute_path, first_error_line] pairs
(sorted, deterministic). Detection is independent of add_project_signatures so the warning fires
even on a cache hit (where the env was already built with the file quarantined). Cheap: it only
re-parses the user's own (usually small) sig/ set, and returns empty immediately when there is no
signature_paths:.
259 260 261 262 263 264 265 266 267 268 269 |
# File 'lib/rigor/environment/rbs_loader.rb', line 259 def quarantined_project_signatures(signature_paths) project_sig_files(signature_paths).sort.filter_map do |file| buffer = ::RBS::Buffer.new(name: file, content: File.read(file, encoding: "UTF-8")) ::RBS::Parser.parse_signature(buffer) nil rescue ::RBS::ParsingError => e [file, e..to_s.lines.first.to_s.strip] rescue Errno::ENOENT, Errno::EISDIR, Errno::EACCES nil end end |
.reset_default! ⇒ void
This method returns an undefined value.
Used by tests to discard the cached default loader; production code MUST NOT call this. The shared loader holds a several-MB RBS::Environment, so dropping it during a normal run wastes the cost of rebuilding it.
54 55 56 |
# File 'lib/rigor/environment/rbs_loader.rb', line 54 def reset_default! @default = nil end |
.resolve_quarantining_virtual_collisions(env, virtual_rbs) ⇒ Object
Backstop for a virtual-vs-anything RBS::DuplicatedDeclarationError that only materialises at
resolve_type_names (which rebuilds the env from sources). add_virtual_rbs's transactional
rescue already handles the add-time case — empirically everything on rbs 4.x — but the rbs gemspec
range spans >= 3.0, < 5.0 (ADR-79) and WHERE duplicate detection fires is an rbs-internal choice
this code must not depend on. Resolution rule is the same as the add-time path: the explicit
signature wins, the colliding VIRTUAL buffer is dropped whole (RBS::Environment#unload) and
resolution retries; every pass removes at least one virtual buffer, so the loop is bounded by the
virtual-entry count. A duplicate involving no virtual buffer (sig-vs-sig), or an env without
#unload (rbs 3.x), re-raises into the existing one-warning degrade path.
The dropped set is not returned: consumers recover it from the built env via #virtual_rbs_collision_quarantined, which also works on a cache HIT where this build never ran.
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 |
# File 'lib/rigor/environment/rbs_loader.rb', line 119 def resolve_quarantining_virtual_collisions(env, virtual_rbs) virtual_names = virtual_rbs.to_set { |name, _content| name.to_s } (virtual_names.size + 1).times do return [env, env.resolve_type_names] rescue ::RBS::DuplicatedDeclarationError => e raise unless env.respond_to?(:unload) culprits = e.decls.filter_map { |decl| decl.location&.buffer&.name } .uniq.select { |name| virtual_names.include?(name) } raise if culprits.empty? env = env.unload(culprits) end [env, env.resolve_type_names] end |
.stub_missing_referenced_types(base_env, resolved, project_files) ⇒ Object
158 159 160 161 162 163 164 165 166 167 168 169 |
# File 'lib/rigor/environment/rbs_loader.rb', line 158 def stub_missing_referenced_types(base_env, resolved, project_files) return resolved if project_files.empty? MAX_STUB_PASSES.times do missing = unresolved_referenced_types(resolved, project_files) break if missing.empty? append_stub_declarations(base_env, missing) resolved = base_env.resolve_type_names end resolved end |
.synthesize_missing_namespaces(env) ⇒ Object
Robustness (ADR-5): a project whose RBS declares qualified names (class Foo::Bar) without ever
declaring the enclosing namespace (module Foo) is invalid by upstream RBS rules —
RBS::DefinitionBuilder#build_instance raises NoTypeFoundError: Could not find ::Foo, which the
loader's fail-soft rescue turns into a silent dispatch miss (every method on every such class
degrades to Dynamic[Top]). This is a common authoring mistake (e.g. shugo/textbringer ships a
sig/ that rbs validate itself rejects). Rather than let an otherwise-usable signature set
contribute nothing, synthesize an empty module declaration for each undeclared enclosing
namespace so the definitions build. We only ever add names that are absent — a genuinely-declared
namespace (module or class, here or in a loaded gem) is left untouched.
180 181 182 183 184 185 186 187 188 189 190 191 192 |
# File 'lib/rigor/environment/rbs_loader.rb', line 180 def synthesize_missing_namespaces(env) missing = collect_missing_namespaces(env) return if missing.empty? source = missing.map { |name| "module #{name}\nend\n" }.join buffer = ::RBS::Buffer.new(name: SYNTHETIC_NAMESPACE_BUFFER, content: source) _, directives, decls = ::RBS::Parser.parse_signature(buffer) add_parsed_decls(env, buffer, directives, decls) rescue ::RBS::BaseError # Fail-soft: synthesis is an opportunistic uplift, never a hard requirement. A parse failure here # just leaves the env as it was (dispatch misses on the affected classes). nil end |
.unresolved_referenced_types(env, project_files) ⇒ Object
Builds every project class (instance + singleton side) and returns the ::-stripped names of the
types whose absence raised NoTypeFoundError. Only the FIRST missing reference per class surfaces
per build, which is why the caller loops.
274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 |
# File 'lib/rigor/environment/rbs_loader.rb', line 274 def unresolved_referenced_types(env, project_files) builder = ::RBS::DefinitionBuilder.new(env: env) missing = [] env.class_decls.each do |type_name, entry| next unless project_entry?(entry, project_files) %i[build_instance build_singleton].each do |build| builder.public_send(build, type_name) rescue ::RBS::NoTypeFoundError => e name = e.[/Could not find (\S+)/, 1] missing << name.sub(/\A::/, "") if name rescue ::RBS::BaseError # Other build failures (duplicate decl, mixin cycle, ...) are not ours to repair here — leave # them fail-soft. end end missing.uniq end |
.vendored_gem_names ⇒ Array[String]
Gem names whose RBS ships under data/vendored_gem_sigs/<gem>/. The directory walk is the source
of truth (the README.md sibling is not a gem and is excluded). Callers building the RBS env use
this set to drop the matching rbs collection install directory before it double-declares against
the vendored copy — the same hazard DEFAULT_LIBRARIES creates for stdlib-extracted gems. See
RbsCollectionDiscovery's skip_gem_names:.
476 477 478 479 480 481 482 |
# File 'lib/rigor/environment/rbs_loader.rb', line 476 def vendored_gem_names return [] unless File.directory?(VENDORED_GEM_SIGS_ROOT) Dir.children(VENDORED_GEM_SIGS_ROOT).reject do |child| File.file?(File.join(VENDORED_GEM_SIGS_ROOT, child)) end end |
.vendored_gem_sig_paths ⇒ Array[Pathname]
463 464 465 466 467 468 469 |
# File 'lib/rigor/environment/rbs_loader.rb', line 463 def vendored_gem_sig_paths return [] unless File.directory?(VENDORED_GEM_SIGS_ROOT) Dir.children(VENDORED_GEM_SIGS_ROOT).map do |gem_dir| Pathname(File.join(VENDORED_GEM_SIGS_ROOT, gem_dir)) end end |
Instance Method Details
#class_decl_paths ⇒ Object
Returns a frozen Hash<String, String> mapping each loaded class / module name (top-level prefixed)
to the file path of its FIRST declaration's RBS source. Used by Analysis::RunStats to
attribute the type universe between "project sig/" (paths under the configured signature_paths)
and "bundled" (everything else — RBS core, stdlib libraries, gem-bundled RBS). Each value is a frozen
String so the whole result is Ractor.shareable? — the Phase 4b worker pool ships a snapshot back
to the coordinator on the first :prepare message.
698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 |
# File 'lib/rigor/environment/rbs_loader.rb', line 698 def class_decl_paths return {}.freeze if env.nil? result = {} env.class_decls.each do |rbs_name, entry| decl = self.class.primary_decl_for(entry) next if decl.nil? location = decl.location next if location.nil? buffer = location.buffer name = buffer.respond_to?(:name) ? buffer.name : nil next if name.nil? result[rbs_name.to_s.dup.freeze] = name.to_s.dup.freeze end result.freeze rescue ::RBS::BaseError {}.freeze end |
#class_known?(name) ⇒ Boolean
Returns true when an RBS class or module declaration with the given name is loaded. Accepts unprefixed or top-level-prefixed names ("Integer" or "::Integer"). Memoized per-name (positive and negative results both cache).
When cache_store is set, the loader fetches the entire set of known class / module / alias names
once (per process) through Cache::RbsKnownClassNames.fetch and answers class_known? from the
in-memory Set. Cold runs pay a single env walk and persist the result; warm runs (and a separate
loader sharing the same Store) skip the env walk entirely.
604 605 606 607 608 609 610 611 612 613 |
# File 'lib/rigor/environment/rbs_loader.rb', line 604 def class_known?(name) key = name.to_s return @class_known_cache[key] if @class_known_cache.key?(key) @class_known_cache[key] = if cache_store cached_class_known(name) else compute_class_known(name) end end |
#class_ordering(lhs, rhs) ⇒ ordering
845 846 847 |
# File 'lib/rigor/environment/rbs_loader.rb', line 845 def class_ordering(lhs, rhs) @hierarchy.class_ordering(lhs, rhs) end |
#class_type_param_names(class_name) ⇒ Array[Symbol]
Slice 4 phase 2d. Returns the class's declared type-parameter names as Symbols (e.g., [:Elem] for
Array, [:K, :V] for Hash). Used by the dispatcher to build the substitution map from receiver
type_args into the method's return type. The instance definition is the canonical source because
singleton methods (e.g., Array.new) parameterize over the same Elem as instance methods.
Returns an empty array for non-generic classes and for unknown names (the loader stays fail-soft).
NOTE: in the rbs gem, RBS::Definition#type_params returns Array<Symbol> directly, not the AST
TypeParam object (those live on the AST level).
When cache_store is set, the loader fetches the entire type-parameter-name table once (per
process) through Cache::RbsClassTypeParamNames.fetch and answers point lookups from it. Cold runs
build the table once and persist it; warm runs (and a separate loader sharing the same Store) skip
the env walk entirely.
833 834 835 836 837 838 839 840 841 842 843 |
# File 'lib/rigor/environment/rbs_loader.rb', line 833 def class_type_param_names(class_name) if cache_store key = class_name.to_s.delete_prefix("::") return type_param_names_table.fetch(key, []).dup end definition = instance_definition(class_name) return [] unless definition definition.type_params.dup end |
#constant_names ⇒ Array<String>
Returns every RBS-declared constant name (top-level prefixed, e.g., "::Math::PI")
currently loaded into the environment. Used by the cache producer that materialises the
constant-type table; ordinary callers should keep using #constant_type for point lookups.
852 853 854 855 856 857 858 |
# File 'lib/rigor/environment/rbs_loader.rb', line 852 def constant_names return [] if env.nil? env.constant_decls.keys.map(&:to_s) rescue ::RBS::BaseError [] end |
#constant_type(name) ⇒ Type::t?
Slice A constant-value lookup. Returns the translated Rigor::Type for a non-class constant
declaration (BUCKETS: Array[Symbol], DEFAULT_PATH: String, ...) or nil when no constant entry
exists for name in the loaded RBS environment. Callers MUST treat the return value as authoritative
when present and as "unknown" when nil; the loader does NOT consult the class declarations here —
class objects are still resolved through #class_known? and Environment#singleton_for_name.
When cache_store is set, the loader fetches the entire translated constant table once (per
process) through Cache::RbsConstantTable.fetch and answers point lookups from it. Cold runs pay
the translation cost up-front and write the result to disk; warm runs skip the translation entirely
and pay only a Marshal.load of the table.
884 885 886 887 888 889 890 891 892 893 894 895 |
# File 'lib/rigor/environment/rbs_loader.rb', line 884 def constant_type(name) rbs_name = parse_type_name(name) return nil unless rbs_name if cache_store constant_type_table[rbs_name.to_s] else translate_constant_decl(rbs_name) end rescue ::RBS::BaseError nil end |
#each_class_decl_annotation ⇒ Object
ADR-20 slice 2e — iterates over every %a{...} annotation attached to a class- or module-level
declaration in the loaded RBS environment, yielding (annotation_string, source_location) pairs.
Used by Inference::HktRegistry.scan_rbs_loader to find rigor:v1:hkt_register /
rigor:v1:hkt_define directives in user-authored overlays and merge them into the per-Environment
HKT registry. Yields nothing when the env failed to build (fail-soft, same shape as
#each_known_class_name).
656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 |
# File 'lib/rigor/environment/rbs_loader.rb', line 656 def each_class_decl_annotation return enum_for(:each_class_decl_annotation) unless block_given? return if env.nil? env.class_decls.each_value do |entry| entry.each_decl do |decl| next unless decl.respond_to?(:annotations) decl.annotations.each { |a| yield a.string, a.location } end end rescue ::RBS::BaseError, ::Ractor::IsolationError # fail-soft: matches each_known_class_name's policy. Ractor::IsolationError surfaces when the scan # is invoked from a non-main Ractor pool worker before ADR-15's full deep-freeze migration completes # — the worker falls back to the base (builtins-only) registry rather than crashing. end |
#each_class_decl_annotation_with_name ⇒ Object
Like #each_class_decl_annotation, but also yields the owning class / module's RBS name as the
first block argument: (class_name, annotation_string, location). Used by
RbsExtended::ConformanceChecker to resolve a rigor:v1:conforms-to directive back to the
class it annotates. Same fail-soft policy as the un-named variant.
677 678 679 680 681 682 683 684 685 686 687 688 689 690 |
# File 'lib/rigor/environment/rbs_loader.rb', line 677 def each_class_decl_annotation_with_name return enum_for(:each_class_decl_annotation_with_name) unless block_given? return if env.nil? env.class_decls.each do |rbs_name, entry| entry.each_decl do |decl| next unless decl.respond_to?(:annotations) decl.annotations.each { |a| yield rbs_name.to_s, a.string, a.location } end end rescue ::RBS::BaseError, ::Ractor::IsolationError # fail-soft: see #each_class_decl_annotation. end |
#each_constant_decl ⇒ Object
Yields (name, entry) for every RBS constant declaration currently loaded into the environment. The
cache producer uses this to materialise the constant-type table without going back through
#constant_type (which would recurse back into the cache when cache_store is set).
863 864 865 866 867 868 869 870 871 872 |
# File 'lib/rigor/environment/rbs_loader.rb', line 863 def each_constant_decl return enum_for(:each_constant_decl) unless block_given? return if env.nil? env.constant_decls.each do |rbs_name, entry| yield rbs_name.to_s, entry end rescue ::RBS::BaseError # fail-soft: a broken RBS environment yields no entries. end |
#each_known_class_name {|name| ... } ⇒ Object
Yields every known class / module / alias name (top-level prefixed) currently loaded into the environment. The cache producer that materialises the known-name set uses this so it never recurses back through #class_known?.
638 639 640 641 642 643 644 645 646 647 648 |
# File 'lib/rigor/environment/rbs_loader.rb', line 638 def each_known_class_name return enum_for(:each_known_class_name) unless block_given? return if env.nil? env.class_decls.each_key { |rbs_name| yield rbs_name.to_s } env.class_alias_decls.each_key { |rbs_name| yield rbs_name.to_s } rescue ::RBS::BaseError # fail-soft: a broken RBS environment yields no names. Analyzer-internal errors (NameError, # NoMethodError, LoadError) are NOT swallowed — those are bugs and must surface so they don't hide # silently the way the v0.0.9 cache `Cache::Descriptor` regression did. end |
#env_build_failure ⇒ Array(String, String, Array<String>)?
The total RBS-environment build failure captured this run, or nil when the env built. Unlike
#quarantined_signatures — which the env survives, one file lighter, and which is re-derived by
re-parsing so a cache HIT reports it too — a total failure (typically RBS::DuplicatedDeclarationError:
a signature_paths: entry redeclaring a constant/class Rigor's bundled RBS already ships) collapses the
WHOLE env to nil. A failed build produces no cached success to hide behind (nothing is persisted, so
every run re-attempts and re-raises), so this is captured directly in #env's rescue rather than
re-derived. Forcing env (any query does) populates it.
552 553 554 555 |
# File 'lib/rigor/environment/rbs_loader.rb', line 552 def env_build_failure env unless @state[:env_loaded] @state[:env_build_failure] end |
#expand_type_alias(rbs_alias) ⇒ RBS::Types::t?
Returns the alias's aliased type one level out, with type arguments substituted
for a generic alias (string → ::String | ::_ToStr; range[int?] → ::Range[int?] | ::_Range[int?]), or nil for an unresolved name. Lets a caller see through the alias that
Inference::RbsTypeTranslator otherwise degrades to untyped, which is why an interface/alias
parameter does not reject nil. expand_alias2 handles the (rarer) generic case — a range[T]
param previously fell back to "admits", which suppressed e.g. MatchData#[](nil).
783 784 785 786 787 788 789 790 791 792 793 |
# File 'lib/rigor/environment/rbs_loader.rb', line 783 def (rbs_alias) return nil if env.nil? name = rbs_alias.name name = name.absolute! unless name.absolute? return nil unless env.type_alias_decls.key?(name) builder.(name, rbs_alias.args) rescue ::RBS::BaseError, StandardError nil end |
#instance_definition(class_name) ⇒ RBS::Definition?
Built on demand from the (possibly cache-loaded) env; the in-memory @instance_definition_cache
keeps the per-process short-circuit. ADR-54 WD1 retired the definitions disk blob: given a cached
env, Marshal.load-ing every definition was measurably slower (and allocation-heavier) than
rebuilding the ones a run actually touches.
728 729 730 731 732 733 |
# File 'lib/rigor/environment/rbs_loader.rb', line 728 def instance_definition(class_name) key = class_name.to_s return @instance_definition_cache[key] if @instance_definition_cache.key?(key) @instance_definition_cache[key] = build_instance_definition(class_name) end |
#instance_method(class_name:, method_name:) ⇒ RBS::Definition::Method?
736 737 738 739 740 741 |
# File 'lib/rigor/environment/rbs_loader.rb', line 736 def instance_method(class_name:, method_name:) definition = instance_definition(class_name) return nil unless definition definition.methods[method_name.to_sym] end |
#instance_method_names(class_name) ⇒ Array<Symbol>?
Returns every instance-method name on class_name — own, inherited, and
included — as resolved by RBS::DefinitionBuilder. Returns nil (NOT []) when the class
definition cannot be built so callers can tell "no methods" apart from "unknown class". Used by the
rigor:v1:conforms-to presence check (RbsExtended::ConformanceChecker).
747 748 749 750 751 752 |
# File 'lib/rigor/environment/rbs_loader.rb', line 747 def instance_method_names(class_name) definition = instance_definition(class_name) return nil unless definition definition.methods.keys end |
#interface_definition(interface_name) ⇒ RBS::Definition?
Returns the built definition for the RBS interface interface_name
(_RewindableStream), whose .methods are the required members (including interface-ancestor
members). Returns nil when the name does not resolve to a loaded interface (a typo, or the
defining library / sig set is not on the load path). Fail-soft on RBS build errors.
758 759 760 761 762 763 764 765 766 767 |
# File 'lib/rigor/environment/rbs_loader.rb', line 758 def interface_definition(interface_name) rbs_name = parse_type_name(interface_name) return nil unless rbs_name return nil if env.nil? return nil unless env.interface_decls.key?(rbs_name) builder.build_interface(rbs_name) rescue ::RBS::BaseError nil end |
#interface_method_names(interface_name) ⇒ Array<Symbol>?
Returns every method name required by the RBS interface interface_name, or nil
when it does not resolve. Thin accessor over #interface_definition for the presence check.
771 772 773 |
# File 'lib/rigor/environment/rbs_loader.rb', line 771 def interface_method_names(interface_name) interface_definition(interface_name)&.methods&.keys end |
#prewarm ⇒ Object
ADR-15 Phase 4b.x — eagerly drives every cached producer (plus the eager definitions tables, computed
from the cached env since ADR-54 WD1) so a subsequent worker Ractor can serve all of its RBS queries
without ever calling RBS::EnvironmentLoader.new. The loader path that calls
EnvironmentLoader.new transitively reads a chain of non-Ractor.shareable? module constants
(RBS::EnvironmentLoader::DEFAULT_CORE_ROOT, RBS::Repository::DEFAULT_STDLIB_ROOT,
Gem::Requirement::DefaultRequirement, …) and trips Ractor::IsolationError. Pre-warming on the
main Ractor — env blob loaded, derived tables built — keeps workers off that chain
(RBS::DefinitionBuilder over an already-built env does not touch it).
No-op when cache_store is nil — without a Store the worker has no choice but to build env via the
loader, so the caller MUST ensure pool mode runs with caching enabled. Returns self so the call
chains cleanly from the Runner pre-spawn hook.
909 910 911 912 913 914 915 916 917 918 919 920 |
# File 'lib/rigor/environment/rbs_loader.rb', line 909 def prewarm return self if cache_store.nil? env known_class_names_set constant_type_table type_param_names_table ancestor_names_table instance_definitions_table singleton_definitions_table self end |
#quarantined_signatures ⇒ Array<Array(String, String)>
The project signature_paths: files that were QUARANTINED this run (they do not parse, so
add_project_signatures skipped them to keep the rest of the env alive), as
[absolute_path, first_error_line] pairs. Memoised per loader: the detection re-parses only the user's
own sig/ set, but every consumer (the rbs.coverage.quarantined-signature diagnostic, rigor doctor,
the stderr banner) reads it, and a cache HIT reaches it too — the env was built with the file already
quarantined, so the condition is invisible in the cached env itself.
538 539 540 |
# File 'lib/rigor/environment/rbs_loader.rb', line 538 def quarantined_signatures @state[:quarantined] ||= self.class.quarantined_project_signatures(@signature_paths).freeze end |
#rbs_cache_descriptor ⇒ Object
ADR-54 WD4 — the shared cache descriptor for every RBS-derived producer consulting this loader.
Building it digests every .rbs file under signature_paths + the vendored gem sigs, and the
result is identical across producers, so one build is memoised per loader (on @state, alongside
:env — the env itself is loader-lifetime-memoised, so this adds no new staleness class).
926 927 928 929 930 931 |
# File 'lib/rigor/environment/rbs_loader.rb', line 926 def rbs_cache_descriptor @state[:rbs_cache_descriptor] ||= begin require_relative "../cache/rbs_descriptor" Cache::RbsDescriptor.build(self) end end |
#rbs_module?(name) ⇒ Boolean
Returns true when the named RBS declaration is a Module (RBS::AST::Declarations::Module) rather
than a Class. The user_class_fallback_receiver tier consults this to route
Nominal[M].some_kernel_method (where M is a module mixin like PP::ObjectMixin) through the
Nominal[Object] fallback, because every concrete includer of M sees Kernel / Object instance
methods as part of its own ancestor chain.
Returns false for classes, for unknown names, and when the RBS environment failed to build (fail-soft).
623 624 625 626 627 628 629 630 631 632 633 |
# File 'lib/rigor/environment/rbs_loader.rb', line 623 def rbs_module?(name) return false if env.nil? rbs_name = parse_type_name(name) return false if rbs_name.nil? entry = env.class_decls[rbs_name] entry.is_a?(::RBS::Environment::ModuleEntry) rescue ::RBS::BaseError false end |
#reflection ⇒ Object
ADR-15 Phase 2b — return the loader's read-only query surface as a frozen, Ractor.shareable?
Rigor::Environment::Reflection value object. Built lazily on first access; the loader memoises so repeated calls
return the same instance.
The Reflection consumes the loader's already-warmed cache producers (or, when no cache_store is
set, eagerly walks the env). Once constructed, the Reflection carries the derived tables
independently and never re-consults the loader — making it safe to share across Ractors while the
loader stays per- process / per-Ractor for write-path operations.
941 942 943 944 945 946 947 948 949 950 951 952 953 |
# File 'lib/rigor/environment/rbs_loader.rb', line 941 def reflection @state[:reflection] ||= begin require_relative "reflection" Environment::Reflection.new( known_class_names: known_class_names_set, instance_definitions: instance_definitions_table, singleton_definitions: singleton_definitions_table, type_param_names: type_param_names_table, constant_types: constant_type_table, ancestor_names: ancestor_names_table ) end end |
#singleton_definition(class_name) ⇒ RBS::Definition?
Built on demand from the env with a per-process memo; the same on-demand discipline as #instance_definition (ADR-54 WD1).
802 803 804 805 806 807 |
# File 'lib/rigor/environment/rbs_loader.rb', line 802 def singleton_definition(class_name) key = class_name.to_s return @singleton_definition_cache[key] if @singleton_definition_cache.key?(key) @singleton_definition_cache[key] = build_singleton_definition(class_name) end |
#singleton_method(class_name:, method_name:) ⇒ RBS::Definition::Method?
Returns the class method on class_name. For example,
singleton_method(class_name: "Integer", method_name: :sqrt) returns the definition for
Integer.sqrt, while singleton_method(class_name: "Foo", method_name: :new) returns Class#new
for any class type.
813 814 815 816 817 818 |
# File 'lib/rigor/environment/rbs_loader.rb', line 813 def singleton_method(class_name:, method_name:) definition = singleton_definition(class_name) return nil unless definition definition.methods[method_name.to_sym] end |
#synthesized_namespaces ⇒ Object
The enclosing namespaces synthesize_missing_namespaces had to invent because the project's
signature_paths: RBS declared qualified names (class Foo::Bar) without ever declaring Foo.
Recovered by scanning the built env for class/module entries whose every declaration originated from
the synthetic buffer, so the answer survives the marshalled-env cache (where no build-time collector
would). Returns ::-stripped names, shallowest-first. Empty for a well-formed sig set (the common
case) and whenever the env failed to build.
526 527 528 |
# File 'lib/rigor/environment/rbs_loader.rb', line 526 def synthesized_namespaces names_synthesized_in(SYNTHETIC_NAMESPACE_BUFFER) end |
#synthesized_stub_types ⇒ Object
The referenced-but-undeclared types stub_missing_referenced_types stubbed so the project classes
that mention them could build (e.g. an unavailable DRb::DRbServer, or a stale
Textbringer::EditorError). Recovered off the built env like #synthesized_namespaces, so it
survives the marshalled-env cache.
583 584 585 |
# File 'lib/rigor/environment/rbs_loader.rb', line 583 def synthesized_stub_types names_synthesized_in(SYNTHETIC_STUB_BUFFER) end |
#synthesized_type_names ⇒ Object
Every type name Rigor invented to make an otherwise-inert / unbuildable project signature set resolve
— both the namespace stubs and the referenced-type stubs. MethodDispatcher resolves a call whose
receiver is one of these (and that no real signature answered) to Dynamic[Top], so the empty stub
never mis-fires call.undefined-method. Memoised; empty (and cheap) for the common well-formed sig
set.
592 593 594 |
# File 'lib/rigor/environment/rbs_loader.rb', line 592 def synthesized_type_names @state[:synthesized_type_names] ||= (synthesized_namespaces + synthesized_stub_types).to_set end |
#virtual_rbs_collision_quarantined ⇒ Array<String>
Virtual (inline-synthesized) contributions dropped by the collision quarantine (resolve_quarantining_virtual_collisions): buffer names absent from the built env even though the entry's content is non-empty and parses (a parse failure is the synthesizer's own WD6 skip, reported through the synthesis reporter instead). Derived from the env rather than recorded during build — the #quarantined_signatures trick — so a cache HIT, which never runs the build, reports the same condition: the marshalled env simply lacks the dropped buffers.
565 566 567 568 569 570 571 572 573 574 575 576 577 |
# File 'lib/rigor/environment/rbs_loader.rb', line 565 def virtual_rbs_collision_quarantined @state[:virtual_rbs_collisions] ||= begin built = @state[:env] if built.nil? || @virtual_rbs.empty? [].freeze else present = built.buffers.to_set(&:name) @virtual_rbs.filter_map do |name, content| name if !content.empty? && !present.include?(name) && self.class.parseable_rbs?(content) end.freeze end end end |