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 reads the PROJECT declarations and mirrors rbs's own membership test (unresolved_referenced_types); it is 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 pass could not see past, but empty stubs reference nothing, so the fixpoint converges quickly. 5- INVALID_ENCODING_NOTE =
The quarantine note for a file rejected by invalid_encoding? — worded to be distinct from any rbs-emitted parse error so specs (and users) can tell Rigor's pre-parser skip from the parser's own UTF-8 diagnostics.
"not valid UTF-8 — skipped before reaching the RBS parser"- 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_bundled_signatures(rbs_loader, loaded_library_names) ⇒ Object
Adds the Rigor-shipped signature sources to
rbs_loader: everydata/vendored_gem_sigs/<gem>/directory, then thedata/core_overlay/files — the overlay 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 abstractNumericthat Rigor's arithmetic-chain widening produces). -
.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.
-
.bigdecimal_library_declares_big_math? ⇒ Boolean
Resolves
library: "bigdecimal"on a THROWAWAY loader (never the one building the env) and reports whether any directory it resolves to shipsbig_math.rbs. -
.build_env_for(libraries:, signature_paths:, virtual_rbs: []) ⇒ Object
Builds an
RBS::Environmentfrom explicitlibrariesandsignature_paths. -
.collect_declaration_references(env, decl, missing, checked) ⇒ Object
Decl-level references the builder validates: a super class's / module self type's / mixin's type ARGUMENTS, plus every member.
-
.collect_member_references(env, members, missing, checked) ⇒ Object
Member-level references.
-
.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. -
.collect_type_references(env, type, missing, checked) ⇒ Object
Walks one type (or method type) and appends the names no declaration provides.
- .core_overlay_sig_paths ⇒ Object
-
.declared_reference?(env, name, checked) ⇒ Boolean
DefinitionBuilder#validate_type_name's membership test, memoised per env walk (a project's method signatures name the same handful of types over and over). -
.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
-
.entry_declarations(entry) ⇒ Object
Collects the AST declaration nodes behind a
class_declsentry across the supported RBS range (rbs >= 3.0, < 5.0). -
.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.
-
.invalid_encoding?(content) ⇒ Boolean
Pre-parser guard for content Rigor hands to
RBS::Parser. -
.libraries_without_shadowed_bigdecimal_math(libraries) ⇒ Array<String>
The same list, minus
bigdecimal-mathwhenbigdecimalalready bringsBigMathin. -
.parse_signature_file(file) ⇒ Object
Parse one project
.rbsinto[buffer, directives, decls], or nil when it is unparseable / unreadable / not valid UTF-8. -
.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 (or are not valid UTF-8), 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_declaration_for(name, names) ⇒ Object
The declaration one stubbed name needs, keyed on the syntax of its leaf:.
- .stub_missing_referenced_types(base_env, resolved, project_files) ⇒ Object
-
.supplement_dependency_loaded?(supplements, path, loaded_library_names) ⇒ Boolean
True when
pathcarries no library dependency, or its library loaded. -
.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
The
::-stripped names of every type a PROJECT signature references that no loaded declaration provides — the input to RbsLoader.append_stub_declarations. -
.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.
800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 |
# File 'lib/rigor/environment/rbs_loader.rb', line 800 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`, `:definition_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.
783 784 785 |
# File 'lib/rigor/environment/rbs_loader.rb', line 783 def cache_store @cache_store end |
#libraries ⇒ Array[String] (readonly)
Returns the value of attribute libraries.
783 784 785 |
# File 'lib/rigor/environment/rbs_loader.rb', line 783 def libraries @libraries end |
#signature_paths ⇒ Array[String | _ToPath] (readonly)
Returns the value of attribute signature_paths.
783 784 785 |
# File 'lib/rigor/environment/rbs_loader.rb', line 783 def signature_paths @signature_paths end |
#virtual_rbs ⇒ Object (readonly)
Returns the value of attribute virtual_rbs.
783 784 785 |
# File 'lib/rigor/environment/rbs_loader.rb', line 783 def virtual_rbs @virtual_rbs end |
Class Method Details
.add_bundled_signatures(rbs_loader, loaded_library_names) ⇒ Object
Adds the Rigor-shipped signature sources to rbs_loader: every data/vendored_gem_sigs/<gem>/
directory, then the data/core_overlay/ files — the overlay 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). The overlay is added per-file, not
per-directory, because the LIBRARY_SUPPLEMENT_CORE_OVERLAYS files must be gated individually.
710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 |
# File 'lib/rigor/environment/rbs_loader.rb', line 710 def add_bundled_signatures(rbs_loader, loaded_library_names) vendored_gem_sig_paths.each do |path| next unless path.directory? next unless supplement_dependency_loaded?(LIBRARY_SUPPLEMENT_VENDORED_DIRS, path, loaded_library_names) rbs_loader.add(path: path) end .each do |dir| next unless dir.directory? dir.children.sort.each do |file| next unless file.file? && file.extname == ".rbs" next unless supplement_dependency_loaded?(LIBRARY_SUPPLEMENT_CORE_OVERLAYS, file, loaded_library_names) rbs_loader.add(path: file) end end end |
.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.
509 510 511 512 513 514 515 516 517 518 519 |
# File 'lib/rigor/environment/rbs_loader.rb', line 509 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).
290 291 292 293 294 295 296 297 298 |
# File 'lib/rigor/environment/rbs_loader.rb', line 290 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.
617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 |
# File 'lib/rigor/environment/rbs_loader.rb', line 617 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? # Same pre-parser guard as {.parse_signature_file}: a synthesizer echoing project bytes can carry # invalid UTF-8, which pre-4.1 rbs lexers could hang on — and a hang escapes the rescue below. next if invalid_encoding?(content.to_s) 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. Returns true when at least one declaration landed, so stub_missing_referenced_types can stop on a pass that made no progress.
Each name gets the declaration kind its own syntax requires (stub_declaration_for), and each
declaration is validated ALONE before it joins the buffer. Declaring every name class — the shape
before #237 — made a dangling interface (_Foo) or type-alias (foo) reference unparseable, and
since the batch shares one buffer the RBS::BaseError rescue below then discarded every stub in it,
well-formed ones included. Measured on herb, whose 74 missing names are all dangling type aliases:
the pass synthesized nothing at all, leaving the project's 48 signature files inert, while still
paying for the detection sweep. A per-declaration check costs one small parse per name and bounds
the damage of one bad name to itself.
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.
553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 |
# File 'lib/rigor/environment/rbs_loader.rb', line 553 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 false if names.empty? source = names.sort_by { |n| n.count(":") }.filter_map do |name| declaration = stub_declaration_for(name, names) declaration if parseable_rbs?(declaration) end.join return false if source.empty? 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) true rescue ::RBS::BaseError false end |
.bigdecimal_library_declares_big_math? ⇒ Boolean
Resolves library: "bigdecimal" on a THROWAWAY loader (never the one building the env) and reports
whether any directory it resolves to ships big_math.rbs. Fails soft to false: an unresolvable
library must not take the whole environment build down, and keeping both entries is the
pre-existing behaviour.
129 130 131 132 133 134 135 136 137 138 139 |
# File 'lib/rigor/environment/rbs_loader.rb', line 129 def bigdecimal_library_declares_big_math? probe = ::RBS::EnvironmentLoader.new(core_root: nil) return false unless probe.has_library?(library: BIGDECIMAL_LIBRARY, version: nil) probe.add(library: BIGDECIMAL_LIBRARY, version: nil) shadowed = false probe.each_dir { |_source, dir| shadowed ||= dir.join(BIG_MATH_SIG_BASENAME).file? } shadowed rescue StandardError false 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 |
# File 'lib/rigor/environment/rbs_loader.rb', line 66 def build_env_for(libraries:, signature_paths:, virtual_rbs: []) rbs_loader = RBS::EnvironmentLoader.new libraries = libraries_without_shadowed_bigdecimal_math(libraries) loaded_libraries = libraries.select do |library| rbs_loader.has_library?(library: library, version: nil) end loaded_libraries.each do |library| 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. add_bundled_signatures(rbs_loader, loaded_libraries.to_set(&:to_s)) 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_declaration_references(env, decl, missing, checked) ⇒ Object
Decl-level references the builder validates: a super class's / module self type's / mixin's type
ARGUMENTS, plus every member. super_class is a Declarations::Class accessor and self_types a
Declarations::Module one, so both are reached behind a shape check.
400 401 402 403 404 405 406 407 408 409 410 |
# File 'lib/rigor/environment/rbs_loader.rb', line 400 def collect_declaration_references(env, decl, missing, checked) if decl.respond_to?(:super_class) decl.super_class&.args&.each { |arg| collect_type_references(env, arg, missing, checked) } end if decl.respond_to?(:self_types) decl.self_types&.each do |self_type| self_type.args.each { |arg| collect_type_references(env, arg, missing, checked) } end end collect_member_references(env, decl.members, missing, checked) end |
.collect_member_references(env, members, missing, checked) ⇒ Object
Member-level references. initialize and the singleton side are skipped because
validate_type_params never reaches them (see unresolved_referenced_types); :singleton_instance
(def self?.x) defines the instance side too, so it is NOT skipped.
415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 |
# File 'lib/rigor/environment/rbs_loader.rb', line 415 def collect_member_references(env, members, missing, checked) members.each do |member| next if member.respond_to?(:kind) && member.kind == :singleton case member when ::RBS::AST::Members::MethodDefinition next if member.name == :initialize member.overloads.each do |overload| collect_type_references(env, overload.method_type, missing, checked) end when ::RBS::AST::Members::AttrReader, ::RBS::AST::Members::AttrWriter, ::RBS::AST::Members::AttrAccessor collect_type_references(env, member.type, missing, checked) when ::RBS::AST::Members::Include, ::RBS::AST::Members::Extend, ::RBS::AST::Members::Prepend member.args.each { |arg| collect_type_references(env, arg, missing, checked) } end end 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.
254 255 256 257 258 259 260 261 262 263 264 265 266 |
# File 'lib/rigor/environment/rbs_loader.rb', line 254 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 |
.collect_type_references(env, type, missing, checked) ⇒ Object
Walks one type (or method type) and appends the names no declaration provides. Only the three node
classes VarianceCalculator#type raises for carry a checkable name; everything else is traversed for
the types nested inside it.
439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 |
# File 'lib/rigor/environment/rbs_loader.rb', line 439 def collect_type_references(env, type, missing, checked) return if type.nil? case type when ::RBS::Types::ClassInstance, ::RBS::Types::Interface, ::RBS::Types::Alias name = type.name name = name.absolute! unless name.absolute? missing[name.to_s.sub(/\A::/, "")] = true unless declared_reference?(env, name, checked) end return unless type.respond_to?(:each_type) type.each_type do |nested| collect_type_references(env, nested, missing, checked) end end |
.core_overlay_sig_paths ⇒ Object
667 668 669 670 671 |
# File 'lib/rigor/environment/rbs_loader.rb', line 667 def return [] unless File.directory?(CORE_OVERLAY_SIGS_ROOT) [Pathname(CORE_OVERLAY_SIGS_ROOT)] end |
.declared_reference?(env, name, checked) ⇒ Boolean
DefinitionBuilder#validate_type_name's membership test, memoised per env walk (a project's method
signatures name the same handful of types over and over). A name whose normalization raises is
treated as declared: repairing it is not this pass's job, and a wrong stub is the worse failure.
458 459 460 461 462 463 464 465 466 467 |
# File 'lib/rigor/environment/rbs_loader.rb', line 458 def declared_reference?(env, name, checked) key = name.to_s return checked[key] if checked.key?(key) checked[key] = begin env.type_name?(env.normalize_type_name(name)) rescue StandardError true end 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.
604 605 606 607 608 609 610 |
# File 'lib/rigor/environment/rbs_loader.rb', line 604 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 |
.entry_declarations(entry) ⇒ Object
Collects the AST declaration nodes behind a class_decls entry across the supported RBS range (`rbs
3.0, < 5.0
). RBS 4'sModuleEntry/ClassEntryexposeeach_decl` yielding bare ASTdeclarations; RBS 3.x exposes
decls, an array ofMultiEntry::Dwrappers whose#declis the AST declaration. The single-declshape is handled defensively so the loader survives an rbs-gem minor bump. Class-side because both the env-build detection walk and the instance-side#names_synthesized_inneed it, and the guard must stay single-rooted.
489 490 491 492 493 494 495 496 497 498 499 |
# File 'lib/rigor/environment/rbs_loader.rb', line 489 def entry_declarations(entry) if entry.respond_to?(:each_decl) [].tap { |acc| entry.each_decl { |decl| acc << decl } } elsif entry.respond_to?(:decls) entry.decls.map { |d| d.respond_to?(:decl) ? d.decl : d } elsif entry.respond_to?(:decl) [entry.decl] else [] end 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.
752 753 754 755 756 757 758 759 |
# File 'lib/rigor/environment/rbs_loader.rb', line 752 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 |
.invalid_encoding?(content) ⇒ Boolean
Pre-parser guard for content Rigor hands to RBS::Parser. On rbs 4.1+ an invalid UTF-8 byte is a
clean ParsingError (ruby/rbs#2983), but on the older releases the gemspec supports (>= 3.0, < 5.0) the C lexer could infinite-loop or abort on it (fixed upstream in ruby/rbs#2973) — a hang no
rescue can catch, and the one failure mode the quarantine's fail-soft rescues cannot absorb. So
the check runs before the parser on every rbs version: uniform behaviour, and the quarantine note
stays actionable ("fix the file's encoding") rather than version-dependent.
311 312 313 |
# File 'lib/rigor/environment/rbs_loader.rb', line 311 def invalid_encoding?(content) !content.valid_encoding? end |
.libraries_without_shadowed_bigdecimal_math(libraries) ⇒ Array<String>
Returns the same list, minus bigdecimal-math when bigdecimal already brings
BigMath in.
118 119 120 121 122 123 |
# File 'lib/rigor/environment/rbs_loader.rb', line 118 def libraries_without_shadowed_bigdecimal_math(libraries) return libraries unless libraries.include?(BIGDECIMAL_LIBRARY) && libraries.include?(BIGDECIMAL_MATH_LIBRARY) return libraries unless bigdecimal_library_declares_big_math? libraries - [BIGDECIMAL_MATH_LIBRARY] end |
.parse_signature_file(file) ⇒ Object
Parse one project .rbs into [buffer, directives, decls], or nil when it is unparseable /
unreadable / not valid UTF-8. Mirrors RBS::EnvironmentLoader#each_signature's per-file parse so
the decls register identically to the loader's batch path.
318 319 320 321 322 323 324 325 326 327 |
# File 'lib/rigor/environment/rbs_loader.rb', line 318 def parse_signature_file(file) content = File.read(file, encoding: "UTF-8") return nil if invalid_encoding?(content) buffer = ::RBS::Buffer.new(name: file, content: content) _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).
144 145 146 147 148 149 150 151 152 153 154 |
# File 'lib/rigor/environment/rbs_loader.rb', line 144 def parseable_rbs?(content) # Pre-parser encoding guard ({.invalid_encoding?}): invalid UTF-8 raises `ArgumentError` (not # `ParsingError`) out of `RBS::Parser.magic_comment`'s regex on rbs 4.1, escaping the rescue below, # and could hang the C lexer outright on the older releases the gemspec supports. return false if invalid_encoding?(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'.
474 475 476 477 478 479 480 481 |
# File 'lib/rigor/environment/rbs_loader.rb', line 474 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.
523 524 525 526 527 528 529 530 |
# File 'lib/rigor/environment/rbs_loader.rb', line 523 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.
271 272 273 274 275 276 277 278 |
# File 'lib/rigor/environment/rbs_loader.rb', line 271 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 (or are not valid UTF-8), 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:.
334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 |
# File 'lib/rigor/environment/rbs_loader.rb', line 334 def quarantined_project_signatures(signature_paths) project_sig_files(signature_paths).sort.filter_map do |file| # The note carries the path itself because the warn composer prints only this element — a # `ParsingError` message embeds its `path:line:` prefix, so the composer never adds one. content = File.read(file, encoding: "UTF-8") next [file, "#{file}: #{INVALID_ENCODING_NOTE}"] if invalid_encoding?(content) buffer = ::RBS::Buffer.new(name: file, content: content) ::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.
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 |
# File 'lib/rigor/environment/rbs_loader.rb', line 168 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_declaration_for(name, names) ⇒ Object
The declaration one stubbed name needs, keyed on the syntax of its leaf:
- a name other stubbed names nest inside is a namespace, so
module; - an RBS interface name (
_Foo) may only be declaredinterface; - a type-alias name (
foo) may only be declaredtype, and aliasesuntypedso a value of that type reads asDynamic[Top]— the honest answer for a type Rigor invented; - anything else is
class(referenced types appear in instance position far more often than as mixins).
The interface stub is FP-safe without joining #synthesized_type_names: RbsTypeTranslator maps every interface type to untyped, and the nil / non-nil acceptance guards in Analysis::CheckRules treat a method-less interface as accepting everything.
589 590 591 592 593 594 595 596 597 598 599 600 |
# File 'lib/rigor/environment/rbs_loader.rb', line 589 def stub_declaration_for(name, names) leaf = name.split("::").last.to_s if names.any? { |other| other != name && other.start_with?("#{name}::") } "module #{name}\nend\n" elsif leaf.start_with?("_") "interface #{name}\nend\n" elsif leaf.match?(/\A[a-z]/) "type #{name} = untyped\n" else "class #{name}\nend\n" end end |
.stub_missing_referenced_types(base_env, resolved, project_files) ⇒ Object
206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 |
# File 'lib/rigor/environment/rbs_loader.rb', line 206 def stub_missing_referenced_types(base_env, resolved, project_files) return resolved if project_files.empty? previous = nil MAX_STUB_PASSES.times do missing = unresolved_referenced_types(resolved, project_files) break if missing.empty? # Bound the fixpoint by PROGRESS, not by the cap alone. A pass that appends no declaration, or # that re-detects the set it already saw, cannot converge — and before this guard the cap was the # only stop, so the pathological input paid the full detection sweep five times over (measured on # herb: five passes, nothing synthesized). The cap stays as the backstop for a genuinely deepening # chain of references. current = missing.to_set break if current == previous || !append_stub_declarations(base_env, missing) previous = current resolved = base_env.resolve_type_names end resolved end |
.supplement_dependency_loaded?(supplements, path, loaded_library_names) ⇒ Boolean
Returns true when path carries no library dependency, or its library loaded.
733 734 735 736 |
# File 'lib/rigor/environment/rbs_loader.rb', line 733 def supplement_dependency_loaded?(supplements, path, loaded_library_names) library = supplements[path.basename.to_s] library.nil? || loaded_library_names.include?(library) 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.
237 238 239 240 241 242 243 244 245 246 247 248 249 |
# File 'lib/rigor/environment/rbs_loader.rb', line 237 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
The ::-stripped names of every type a PROJECT signature references that no loaded declaration
provides — the input to append_stub_declarations.
Detection READS the declarations; it does not build them. Until #207 it built every project class
instance- and singleton-side with a throwaway RBS::DefinitionBuilder and recovered the name from
the raised NoTypeFoundError — correct by construction, and 7.84M allocations, a third of a cold
check lib on Rigor's own tree, to find nothing once sig/ is self-consistent. The walk below
costs ~21k.
It mirrors rbs's raise sites, so the answer is the builder's:
DefinitionBuilder#validate_type_presenceover the ARGS of a super class, a moduleselftype, and a mixin. The name itself is deliberately NOT checked: a missing super class or mixin raisesNoSuperclassFoundError/NoMixinFoundError, which this pass has never stubbed.VarianceCalculator#typeover every method typevalidate_type_paramsreaches, which raises forClassInstance/Interface/Aliasonly. It skipsinitialize, and it is not called for the singleton side — so a name reachable only throughdef initialize:ordef self.x:is not reported here, exactly as the builder did not report it. Stubbing those names anyway cost allocations on the corpus and changed no diagnostic.
Membership is decided with the builder's own predicate (declared_reference?), so a name reported here is one no declaration in the env provides. A resolvable name can never be reported, which is what keeps the stub safe: ADR-5 tier 2 trades precision for a fail-soft, and a stub for a name that WOULD have resolved would shadow a real type instead.
One reduction in scope. The builder also walked each project class's ANCESTORS, so a dangling
reference inside a gem's signature reachable from a project class was reported too; this walk
reads project declarations only. No such name occurs across the eight RBS-shipping projects the
change was measured on, and the cost of missing one is the fail-soft Dynamic ADR-5 tier 2 already
accepts — not a false diagnostic. A project INTERFACE's own dangling reference is likewise not
reported, and likewise was not by the builder (validate_type_params does not variance-walk the
methods a class imports from an interface) — pinned by spec so the two stay together.
Equivalence with the builder sweep is pinned by spec, which keeps the sweep as its oracle. Full
evaluation: docs/notes/20260730-stub-pass1-static-detection-evaluation.md.
386 387 388 389 390 391 392 393 394 395 |
# File 'lib/rigor/environment/rbs_loader.rb', line 386 def unresolved_referenced_types(env, project_files) missing = {} checked = {} env.class_decls.each_value do |entry| next unless project_entry?(entry, project_files) entry_declarations(entry).each { |decl| collect_declaration_references(env, decl, missing, checked) } end missing.keys 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:.
774 775 776 777 778 779 780 |
# File 'lib/rigor/environment/rbs_loader.rb', line 774 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]
761 762 763 764 765 766 767 |
# File 'lib/rigor/environment/rbs_loader.rb', line 761 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.
1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 |
# File 'lib/rigor/environment/rbs_loader.rb', line 1002 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.
908 909 910 911 912 913 914 915 916 917 |
# File 'lib/rigor/environment/rbs_loader.rb', line 908 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
1149 1150 1151 |
# File 'lib/rigor/environment/rbs_loader.rb', line 1149 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.
1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 |
# File 'lib/rigor/environment/rbs_loader.rb', line 1137 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.
1156 1157 1158 1159 1160 1161 1162 |
# File 'lib/rigor/environment/rbs_loader.rb', line 1156 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.
1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 |
# File 'lib/rigor/environment/rbs_loader.rb', line 1188 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).
960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 |
# File 'lib/rigor/environment/rbs_loader.rb', line 960 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.
981 982 983 984 985 986 987 988 989 990 991 992 993 994 |
# File 'lib/rigor/environment/rbs_loader.rb', line 981 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).
1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 |
# File 'lib/rigor/environment/rbs_loader.rb', line 1167 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?.
942 943 944 945 946 947 948 949 950 951 952 |
# File 'lib/rigor/environment/rbs_loader.rb', line 942 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.
851 852 853 854 |
# File 'lib/rigor/environment/rbs_loader.rb', line 851 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).
1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 |
# File 'lib/rigor/environment/rbs_loader.rb', line 1087 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.
1032 1033 1034 1035 1036 1037 |
# File 'lib/rigor/environment/rbs_loader.rb', line 1032 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?
1040 1041 1042 1043 1044 1045 |
# File 'lib/rigor/environment/rbs_loader.rb', line 1040 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).
1051 1052 1053 1054 1055 1056 |
# File 'lib/rigor/environment/rbs_loader.rb', line 1051 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.
1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 |
# File 'lib/rigor/environment/rbs_loader.rb', line 1062 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.
1075 1076 1077 |
# File 'lib/rigor/environment/rbs_loader.rb', line 1075 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.
1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 |
# File 'lib/rigor/environment/rbs_loader.rb', line 1213 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.
837 838 839 |
# File 'lib/rigor/environment/rbs_loader.rb', line 837 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).
1230 1231 1232 1233 1234 1235 |
# File 'lib/rigor/environment/rbs_loader.rb', line 1230 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).
927 928 929 930 931 932 933 934 935 936 937 |
# File 'lib/rigor/environment/rbs_loader.rb', line 927 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.
1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 |
# File 'lib/rigor/environment/rbs_loader.rb', line 1245 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).
1106 1107 1108 1109 1110 1111 |
# File 'lib/rigor/environment/rbs_loader.rb', line 1106 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.
1117 1118 1119 1120 1121 1122 |
# File 'lib/rigor/environment/rbs_loader.rb', line 1117 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.
825 826 827 |
# File 'lib/rigor/environment/rbs_loader.rb', line 825 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.
Class / module stubs only — the interface and type stubs stub_declaration_for also emits live in
interface_decls / type_alias_decls, not class_decls. That is deliberate: this list exists to feed
#synthesized_type_names, whose consumers key on a nominal receiver's class name, and both of those
kinds already read as untyped through Inference::RbsTypeTranslator.
887 888 889 |
# File 'lib/rigor/environment/rbs_loader.rb', line 887 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.
896 897 898 |
# File 'lib/rigor/environment/rbs_loader.rb', line 896 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.
864 865 866 867 868 869 870 871 872 873 874 875 876 |
# File 'lib/rigor/environment/rbs_loader.rb', line 864 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 |