Skip to content

Index Class.new and Module.new#402

Merged
vinistock merged 2 commits intomainfrom
12-23-index_class_and_module_new
Jan 12, 2026
Merged

Index Class.new and Module.new#402
vinistock merged 2 commits intomainfrom
12-23-index_class_and_module_new

Conversation

@vinistock
Copy link
Member

@vinistock vinistock commented Dec 23, 2025

Initial step for #392

This PR starts handling Class.new and Module.new. The shared characteristic between both is that they create a new possible owner for definitions. That means, you can have methods and instance variables be associated with them.

However, neither of those produces of a new lexical scope, so constant references that occur inside of the block ignore these new classes and modules. Only the class and module keywords can produce new lexical scopes.

To account for this, I turned the definitions_stack into a nesting_stack where we can have two types of entries:

  • LexicalScope: an owner definition that also needs to be accounted for constant references
  • Owner: an owner that doesn't influence constant references

Copy link
Member Author

vinistock commented Dec 23, 2025

This stack of pull requests is managed by Graphite. Learn more about stacking.

@vinistock vinistock self-assigned this Dec 23, 2025
@vinistock vinistock marked this pull request as ready for review December 23, 2025 19:43
@vinistock vinistock requested a review from a team as a code owner December 23, 2025 19:43
Copy link
Contributor

@Morriar Morriar left a comment

Choose a reason for hiding this comment

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

What's the plan for anonymous ones like this:

class Foo
  Class.new do
    def foo; end
  end
end

I think we'll need to represent class/module defs without a name (or <ANONYMOUS>?) so we can push them onto the stack and avoid registering foo in the wrong parent.

Maybe the handling of Class.new & Module.new should also move to visit_call_node?

vinistock added a commit that referenced this pull request Jan 8, 2026
This PR extracts just the new `Nesting` enum portion of #402, to make it
a bit easier to handle the existing conflicts with main.

Basically, the `definitions_stack` becomes a `nesting_stack`, where we
can store the IDs with a bit of differentiation and perform the right
behaviour:

- LexicalScope: anywhere the `class` or `module` keywords get used,
which produces a new lexical scope that binds constants and class
variables
- Owner: definitions that can own things, but without producing a new
lexical scope (like the ones created by `Class.new`)
- Method: a method. Just used for instance variables, so that we know
we're inside of a method
vinistock added a commit that referenced this pull request Jan 8, 2026
Another extraction to simplify #402

The impl block for `ClassDefinition` was the only one that was not next to its struct. This PR just moves it next to it.
@vinistock vinistock force-pushed the 12-23-index_class_and_module_new branch 3 times, most recently from e7de98b to 8e60e29 Compare January 9, 2026 17:16
@vinistock vinistock requested a review from Morriar January 9, 2026 17:16
Copy link
Member

@st0012 st0012 left a comment

Choose a reason for hiding this comment

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

Can we add these tests to resolution?

    #[test]
    fn resolving_global_variable_alias_inside_method() {
        let mut context = GraphTest::new();
        context.index_uri("file:///foo.rb", {
            r"
            class Foo
              def setup
                alias $bar $baz
              end
            end
            "
        });
        context.resolve();

        // Global variable aliases should still be owned by Object, regardless of where defined
        let read_lock = context.graph().declarations().read().unwrap();
        let object = read_lock.get(&DeclarationId::from("Object")).unwrap();
        assert_members(object, &["$bar"]);
    }

    #[test]
    fn resolving_method_defined_inside_method() {
        let mut context = GraphTest::new();
        context.index_uri("file:///foo.rb", {
            r"
            class Foo
              def setup
                def inner_method; end
              end
            end
            "
        });
        context.resolve();

        let read_lock = context.graph().declarations().read().unwrap();
        let foo_class = read_lock.get(&DeclarationId::from("Foo")).unwrap();
        // inner_method should be owned by Foo, not by setup
        assert_members(foo_class, &["setup", "inner_method"]);
    }

    #[test]
    fn resolving_attr_accessors_inside_method() {
        let mut context = GraphTest::new();
        context.index_uri("file:///foo.rb", {
            r"
            class Foo
              def self.setup
                attr_reader :reader_attr
                attr_writer :writer_attr
                attr_accessor :accessor_attr
              end
            end
            "
        });
        context.resolve();

        let read_lock = context.graph().declarations().read().unwrap();
        let foo_singleton_class = read_lock.get(&DeclarationId::from("Foo::<Foo>")).unwrap();
        assert_members(foo_singleton_class, &["setup"]);
        let foo_class = read_lock.get(&DeclarationId::from("Foo")).unwrap();
        // All attr_* should be owned by Foo, not by setup
        assert_members(foo_class, &["reader_attr", "writer_attr", "accessor_attr"]);
    }

/// # Panics
///
/// Panics if the definition is not a class, module, or singleton class
fn current_lexical_scope_name_id(&self) -> Option<NameId> {
Copy link
Member

Choose a reason for hiding this comment

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

Will it be better to name it like current_lexical_owner_name_id?
Owner includes class/module/singleton class/anonymous class & module. Lexical owner excludes the last one.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think the nomenclature needs to be a touch different. We now have 3 types of nestings:

  • LexicalScope: anything that uses the class or module keywords
  • Owner: a namespace that can own things, but isn't a new lexical scope (e.g.: Class.new)
  • Method: just to indicate we're inside a method, but it doesn't represent a new lexical scope and can't own anything

I think using "lexical owner" can be confusing because it kind of mixes the first two nesting variants. Personally, I would use lexical_scope and owner separately without mixing the two.

Copy link
Member

Choose a reason for hiding this comment

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

That's fair. Can this distinction go into the architecture doc?

Also TIL: nomenclature

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes! I'll add it to #423

@vinistock vinistock force-pushed the 12-23-index_class_and_module_new branch from 8e60e29 to 065b5d9 Compare January 12, 2026 18:26
@vinistock vinistock requested a review from st0012 January 12, 2026 18:26
@vinistock
Copy link
Member Author

Added the test cases 👍.

@vinistock vinistock merged commit 4e5a211 into main Jan 12, 2026
26 checks passed
@vinistock vinistock deleted the 12-23-index_class_and_module_new branch January 12, 2026 19:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants