Skip to content

Commit e47df8c

Browse files
committed
Re-queue singleton-body instance variables when receiver is missing
Direct instance variables inside `class << Foo` (e.g. `@bar = 1`) used to fall back to `*OBJECT_ID` via `unwrap_or` when the singleton's declaration was missing, planting `Object::<Object>#@bar` and never recovering after `Foo` was restored. Mirror the policy used for methods/attrs: when `definition_id_to_declaration_id(nesting_id)` returns `None` for a SingletonClass nesting, push the def back to `pending_work` and let the next resolve place it on the right owner.
1 parent 0d7390a commit e47df8c

2 files changed

Lines changed: 39 additions & 5 deletions

File tree

rust/rubydex/src/model/graph.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4357,6 +4357,36 @@ mod incremental_resolution_tests {
43574357
assert_declaration_exists!(incremental, "Foo::<Foo>#bar()");
43584358
}
43594359

4360+
/// Same shape as `singleton_definition_survives_receiver_delete_readd` but
4361+
/// for a direct instance variable inside the singleton block. Without
4362+
/// re-queueing the def, `@bar` would land on `Object::<Object>` (via the
4363+
/// missing-singleton-decl fallback) and stay there after `Foo` is restored.
4364+
#[test]
4365+
fn singleton_body_ivar_survives_receiver_delete_readd() {
4366+
let mut incremental = GraphTest::new();
4367+
incremental.index_uri("file:///foo.rb", "class Foo; end");
4368+
incremental.index_uri("file:///singleton.rb", "class << Foo; @bar = 1; end");
4369+
incremental.resolve();
4370+
assert_declaration_exists!(incremental, "Foo::<Foo>::<<Foo>>#@bar");
4371+
4372+
incremental.delete_uri("file:///foo.rb");
4373+
incremental.resolve();
4374+
assert_declaration_does_not_exist!(incremental, "Foo::<Foo>::<<Foo>>#@bar");
4375+
// Must not fall back to Object's singleton.
4376+
assert_declaration_does_not_exist!(incremental, "Object::<Object>#@bar");
4377+
4378+
incremental.index_uri("file:///foo.rb", "class Foo; end");
4379+
incremental.resolve();
4380+
4381+
let mut fresh = GraphTest::new();
4382+
fresh.index_uri("file:///foo.rb", "class Foo; end");
4383+
fresh.index_uri("file:///singleton.rb", "class << Foo; @bar = 1; end");
4384+
fresh.resolve();
4385+
4386+
assert_declaration_ids_match(&incremental, &fresh);
4387+
assert_declaration_exists!(incremental, "Foo::<Foo>::<<Foo>>#@bar");
4388+
}
4389+
43604390
/// A singleton class materialized solely by a constant reference (e.g.
43614391
/// `Foo.new` creating `Foo::<Foo>` even though no `class << Foo` block
43624392
/// exists) must be collected when its sole supporting reference is

rust/rubydex/src/resolution.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -472,11 +472,15 @@ impl<'a> Resolver<'a> {
472472
// If in a singleton class body directly, the owner is the singleton class's singleton class
473473
// Like `class << Foo; @bar = 1; end`, where `@bar` is owned by `Foo::<Foo>::<<Foo>>`
474474
Definition::SingletonClass(_) => {
475-
let singleton_class_decl_id = self
476-
.graph
477-
.definition_id_to_declaration_id(nesting_id)
478-
.copied()
479-
.unwrap_or(*OBJECT_ID);
475+
// The singleton's declaration may be missing (e.g. its receiver was
476+
// just deleted). Re-queue and let the next resolve place `@bar` on
477+
// the right owner — falling back to Object would create
478+
// `Object::<Object>#@bar` and never recover.
479+
let Some(&singleton_class_decl_id) = self.graph.definition_id_to_declaration_id(nesting_id)
480+
else {
481+
self.graph.push_work(Unit::Definition(id));
482+
continue;
483+
};
480484
let owner_id = self
481485
.get_or_create_singleton_class(singleton_class_decl_id)
482486
.expect("singleton class nesting should always be a namespace");

0 commit comments

Comments
 (0)