Skip to content

Resolve constants against enclosing lexical scope in class << self#764

Merged
vinistock merged 1 commit intomainfrom
cx-resolve-constants-singleton-scope
Apr 27, 2026
Merged

Resolve constants against enclosing lexical scope in class << self#764
vinistock merged 1 commit intomainfrom
cx-resolve-constants-singleton-scope

Conversation

@splantio
Copy link
Copy Markdown
Contributor

@splantio splantio commented Apr 24, 2026

Summary

Fixes a constant-resolution bug in class << self bodies. Constants referenced from inside a singleton class block (or from methods defined inside one) were not resolving against the enclosing lexical scope. For example:

module A
  module B
    class Sibling; end

    class Main
      class << self
        def m
          Sibling   # was left unresolved
        end
      end
    end
  end
end

Root cause

The resolution algorithm already walks lexical scopes correctly; it follows the nesting linked list on each Name. The bug was in the indexer: when the singleton class Name was created, its nesting was set to None, so the walk terminated at the singleton class frame and never reached A::B or A.

Fix

One-line change in RubyIndexer::visit_singleton_class_node: populate the singleton class Name's nesting with the current lexical scope at the point of the class << self block, matching how regular classes and modules are indexed.

Two existing tests are updated to reflect the new Name identity: <Foo> with a nesting interns to a different Name than <Foo> without one.

Tests

Added 5 tests covering:

  • Sibling used inside a method in class << self
  • Sibling used inside class << self nested under an outer class
  • Sibling used directly in the class << self body (not inside a method)
  • Non-regression: sibling still resolves from instance method / class body / top level
  • Does not over-resolve unknown constants

All 528 rubydex tests pass; clippy and fmt are clean.

@splantio splantio requested a review from a team as a code owner April 24, 2026 20:58
@vinistock
Copy link
Copy Markdown
Member

Oh, wow I can't believe we forgot to add a test for that. Thanks for the PR! The scenarios are all great, but the fix is actually significantly simpler. The resolution algorithm already walks lexical scopes correctly, but we're mistakenly not associating the nesting with the name objects for singleton.

Here's the fix (needs some test updates too, but logic changes it's just this):

--- a/rust/rubydex/src/indexing/ruby_indexer.rs
+++ b/rust/rubydex/src/indexing/ruby_indexer.rs
@@ -1507,9 +1507,10 @@ impl Visit<'_> for RubyIndexer<'_> {
         };
 
         let string_id = self.local_graph.intern_string(singleton_class_name);
+        let nesting = self.current_lexical_scope_name_id();
         let name_id = self
             .local_graph
-            .add_name(Name::new(string_id, ParentScope::Attached(attached_target), None));
+            .add_name(Name::new(string_id, ParentScope::Attached(attached_target), nesting));

@vinistock vinistock added the bugfix A change that fixes an existing bug label Apr 24, 2026
@splantio splantio force-pushed the cx-resolve-constants-singleton-scope branch 2 times, most recently from e749f3b to 72890a9 Compare April 24, 2026 21:31
Copy link
Copy Markdown
Member

@vinistock vinistock left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The failure for lint is fixed on main, so a rebase will probably fix it. Thanks for the contribution!

Constants referenced inside a `class << self` body (or inside methods
defined in one) did not resolve against the enclosing lexical scope.
For example, `Sibling` in this snippet was left unresolved:

    module A
      module B
        class Sibling; end

        class Main
          class << self
            def m
              Sibling
            end
          end
        end
      end
    end

The resolution algorithm already walks lexical scopes correctly — it
follows the `nesting` linked list on each `Name`. The bug was in the
indexer: when the singleton class `Name` was created, its `nesting`
was set to `None`, so the walk terminated at the singleton class frame
and never reached `A::B`.

Populate the singleton class `Name`'s `nesting` with the current
lexical scope at the point of the `class << self` block, matching how
regular classes and modules are indexed.

Two existing tests are updated to reflect the new name identity:
`<Foo>` with a nesting is a different interned `Name` than `<Foo>`
without one.
@splantio splantio force-pushed the cx-resolve-constants-singleton-scope branch from 72890a9 to 998687f Compare April 24, 2026 21:41
@vinistock vinistock merged commit ae36310 into main Apr 27, 2026
36 checks passed
@vinistock vinistock deleted the cx-resolve-constants-singleton-scope branch April 27, 2026 13:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix A change that fixes an existing bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants