Rubydex's public API doesn't surface the lexical nesting (Module.nesting) at a Definition's source position. That's the piece of information needed to resolve relative constant references — e.g. resolving a bare Bar referenced inside class Foo::Bar's body, or inside Class.new(...) do ... end, via Graph#resolve_constant(name, nesting). Today there's no accessor that gives that.
Background
Lexical nesting and constant ownership are two independent relationships in Ruby:
Module.nesting is purely syntactic: the chain of source-level class/module openings enclosing a given position.
- Constant ownership is structural:
Foo::Bar's owner is Foo because Bar is a constant defined inside Foo, regardless of where the source happens to open it.
For the simple case class Foo; class Bar; def hello; end; end; end the two chains happen to coincide, but in general they're different — see the examples below.
Rubydex exposes constant ownership today (declaration.owner). It doesn't expose lexical nesting through any path I could find. The full public surface of a MethodDefinition is:
name
location (uri, start/end line/column)
name_location
deprecated?
comments
signatures
declaration
ClassDefinition/ModuleDefinition add mixins/superclass. None of them expose nesting.
Example 1 — compound-path opening
class Foo::Bar
module Baz
def hello
end
end
end
At the def hello site, Module.nesting is [Foo::Bar::Baz, Foo::Bar]. (Not [Foo::Bar::Baz, Foo::Bar, Foo] — the source never lexically opened Foo.)
Rubydex doesn't expose this. The available declaration.owner chain answers a different question (constant ownership) and isn't a substitute.
Example 2 — anonymous enclosing scope
class Foo
module Bar
Class.new do
def world
end
end
end
end
At the def world site, Module.nesting is [Foo::Bar, Foo].
Again, no way to get this out of Rubydex. The owner-of-the-anonymous-class chain isn't relevant — we want lexical nesting at the source position, not the constant ancestry of the (anonymous) declaration.
What I'd like
Some way to ask "what's the lexical nesting at this source position?" without re-parsing the file out-of-band. Either of these would unblock us:
-
A nesting accessor on every Definition subclass.
defn.nesting
# => ["Foo::Bar::Baz", "Foo::Bar"] for example 1
# => ["Foo::Bar", "Foo"] for example 2
Returns the chain of source-lexical class/module openings enclosing the definition, in Module.nesting order (deepest first). For anonymous enclosing scopes, the anonymous frame either gets a synthetic name (matching today's …<anonymous> form) or is skipped — either is fine, as long as the surrounding named scopes are preserved.
-
A Graph#nesting_at(uri, line) method. Same semantics, but addressable from any caller that has a [file, line] (e.g. from Method#source_location) without already holding a Definition.
Option 1 is the one we'd consume directly, applied uniformly to MethodDefinition, ClassDefinition, ModuleDefinition, AttrReaderDefinition, etc. Option 2 is a nice complement for ad-hoc callers.
Why this matters
We hit this in Shopify/tapioca#2639 while removing Tapioca's require-hook RBS rewriter. Whenever an inline RBS comment references a relative constant — #: -> Bar inside class Foo::Bar's body, or #: -> ValueType[Integer] inside Class.new(...) do; def cast; end; end — we need lexical nesting at the definition site to feed Graph#resolve_constant. We currently fall back to re-parsing the source with Prism, which duplicates work Rubydex already does during indexing.
Happy to send a PR if there's a preferred shape.
Environment
rubydex 0.2.3 (arm64-darwin)
- Ruby 4.0.2
Rubydex's public API doesn't surface the lexical nesting (
Module.nesting) at aDefinition's source position. That's the piece of information needed to resolve relative constant references — e.g. resolving a bareBarreferenced insideclass Foo::Bar's body, or insideClass.new(...) do ... end, viaGraph#resolve_constant(name, nesting). Today there's no accessor that gives that.Background
Lexical nesting and constant ownership are two independent relationships in Ruby:
Module.nestingis purely syntactic: the chain of source-levelclass/moduleopenings enclosing a given position.Foo::Bar's owner isFoobecauseBaris a constant defined insideFoo, regardless of where the source happens to open it.For the simple case
class Foo; class Bar; def hello; end; end; endthe two chains happen to coincide, but in general they're different — see the examples below.Rubydex exposes constant ownership today (
declaration.owner). It doesn't expose lexical nesting through any path I could find. The full public surface of aMethodDefinitionis:ClassDefinition/ModuleDefinitionaddmixins/superclass. None of them expose nesting.Example 1 — compound-path opening
At the
def hellosite,Module.nestingis[Foo::Bar::Baz, Foo::Bar]. (Not[Foo::Bar::Baz, Foo::Bar, Foo]— the source never lexically openedFoo.)Rubydex doesn't expose this. The available
declaration.ownerchain answers a different question (constant ownership) and isn't a substitute.Example 2 — anonymous enclosing scope
At the
def worldsite,Module.nestingis[Foo::Bar, Foo].Again, no way to get this out of Rubydex. The owner-of-the-anonymous-class chain isn't relevant — we want lexical nesting at the source position, not the constant ancestry of the (anonymous) declaration.
What I'd like
Some way to ask "what's the lexical nesting at this source position?" without re-parsing the file out-of-band. Either of these would unblock us:
A
nestingaccessor on everyDefinitionsubclass.Returns the chain of source-lexical
class/moduleopenings enclosing the definition, inModule.nestingorder (deepest first). For anonymous enclosing scopes, the anonymous frame either gets a synthetic name (matching today's…<anonymous>form) or is skipped — either is fine, as long as the surrounding named scopes are preserved.A
Graph#nesting_at(uri, line)method. Same semantics, but addressable from any caller that has a[file, line](e.g. fromMethod#source_location) without already holding aDefinition.Option 1 is the one we'd consume directly, applied uniformly to
MethodDefinition,ClassDefinition,ModuleDefinition,AttrReaderDefinition, etc. Option 2 is a nice complement for ad-hoc callers.Why this matters
We hit this in Shopify/tapioca#2639 while removing Tapioca's require-hook RBS rewriter. Whenever an inline RBS comment references a relative constant —
#: -> Barinsideclass Foo::Bar's body, or#: -> ValueType[Integer]insideClass.new(...) do; def cast; end; end— we need lexical nesting at the definition site to feedGraph#resolve_constant. We currently fall back to re-parsing the source with Prism, which duplicates work Rubydex already does during indexing.Happy to send a PR if there's a preferred shape.
Environment
rubydex0.2.3 (arm64-darwin)