Skip to content

Expose lexical nesting at a Definition's source position #831

Description

@paracycle

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:

  1. 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.

  2. 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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions