Skip to content

Commit 7797fb4

Browse files
committed
Re-queue method aliases when singleton receiver is missing
`Definition::MethodAlias` inside `class << Foo` had the same drop-on- missing-owner shape as the previously-fixed methods/attrs/instance variables: when `resolve_lexical_owner` returned `None`, the alias was silently skipped, so deleting and re-adding `Foo` lost `Foo::<Foo>#new()` even though `Foo::<Foo>#old()` was restored. Extract a `resolve_lexical_owner_or_requeue` helper and route Method, Attr*, MethodAlias, and the singleton-body InstanceVariable path through it so the policy is uniform. Also tightens a few of the doc comments introduced by the previous commits.
1 parent e47df8c commit 7797fb4

2 files changed

Lines changed: 70 additions & 41 deletions

File tree

rust/rubydex/src/model/graph.rs

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4323,10 +4323,7 @@ mod incremental_resolution_tests {
43234323
/// Delete the receiver of a `class << Foo` block, resolve, then re-add the
43244324
/// receiver. The singleton's definition unit must survive the gap so that
43254325
/// when `Foo` returns, `Foo::<Foo>` and any methods inside the block are
4326-
/// reattached. Regression for the case where dropping singleton TODO
4327-
/// creation also dropped the singleton's definition unit on
4328-
/// `Outcome::Unresolved(None)` — emitting `Retry(None)` instead keeps the
4329-
/// unit queued across resolve cycles.
4326+
/// reattached.
43304327
#[test]
43314328
fn singleton_definition_survives_receiver_delete_readd() {
43324329
let mut incremental = GraphTest::new();
@@ -4357,6 +4354,34 @@ mod incremental_resolution_tests {
43574354
assert_declaration_exists!(incremental, "Foo::<Foo>#bar()");
43584355
}
43594356

4357+
/// Same shape as `singleton_definition_survives_receiver_delete_readd` but
4358+
/// for a method alias inside the singleton block. Without re-queueing the
4359+
/// def, `alias new old` would be silently dropped while `Foo` is missing
4360+
/// and `Foo::<Foo>#new()` would never be restored.
4361+
#[test]
4362+
fn singleton_body_method_alias_survives_receiver_delete_readd() {
4363+
let mut incremental = GraphTest::new();
4364+
incremental.index_uri("file:///foo.rb", "class Foo; end");
4365+
incremental.index_uri("file:///singleton.rb", "class << Foo; def old; end; alias new old; end");
4366+
incremental.resolve();
4367+
assert_declaration_exists!(incremental, "Foo::<Foo>#old()");
4368+
assert_declaration_exists!(incremental, "Foo::<Foo>#new()");
4369+
4370+
incremental.delete_uri("file:///foo.rb");
4371+
incremental.resolve();
4372+
4373+
incremental.index_uri("file:///foo.rb", "class Foo; end");
4374+
incremental.resolve();
4375+
4376+
let mut fresh = GraphTest::new();
4377+
fresh.index_uri("file:///foo.rb", "class Foo; end");
4378+
fresh.index_uri("file:///singleton.rb", "class << Foo; def old; end; alias new old; end");
4379+
fresh.resolve();
4380+
4381+
assert_declaration_ids_match(&incremental, &fresh);
4382+
assert_declaration_exists!(incremental, "Foo::<Foo>#new()");
4383+
}
4384+
43604385
/// Same shape as `singleton_definition_survives_receiver_delete_readd` but
43614386
/// for a direct instance variable inside the singleton block. Without
43624387
/// re-queueing the def, `@bar` would land on `Object::<Object>` (via the

rust/rubydex/src/resolution.rs

Lines changed: 41 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -301,13 +301,8 @@ impl<'a> Resolver<'a> {
301301
singleton_id
302302
}
303303
None => {
304-
// resolve_lexical_owner returns None when the immediate nesting is
305-
// a SingletonClass def whose declaration is missing (e.g. receiver
306-
// was just deleted). Re-queue so the next resolve, after the
307-
// singleton is reattached, can place the method on the right owner.
308-
let Some(resolved) = self.resolve_lexical_owner(*method_definition.lexical_nesting_id())
309-
else {
310-
self.graph.push_work(Unit::Definition(id));
304+
let lexical = *method_definition.lexical_nesting_id();
305+
let Some(resolved) = self.resolve_lexical_owner_or_requeue(lexical, id) else {
311306
continue;
312307
};
313308
resolved
@@ -319,32 +314,35 @@ impl<'a> Resolver<'a> {
319314
});
320315
}
321316
Definition::AttrAccessor(attr) => {
322-
let Some(owner_id) = self.resolve_lexical_owner(*attr.lexical_nesting_id()) else {
323-
self.graph.push_work(Unit::Definition(id));
317+
let lexical = *attr.lexical_nesting_id();
318+
let str_id = *attr.str_id();
319+
let Some(owner_id) = self.resolve_lexical_owner_or_requeue(lexical, id) else {
324320
continue;
325321
};
326322

327-
self.create_declaration(*attr.str_id(), id, owner_id, |name| {
323+
self.create_declaration(str_id, id, owner_id, |name| {
328324
Declaration::Method(Box::new(MethodDeclaration::new(name, owner_id)))
329325
});
330326
}
331327
Definition::AttrReader(attr) => {
332-
let Some(owner_id) = self.resolve_lexical_owner(*attr.lexical_nesting_id()) else {
333-
self.graph.push_work(Unit::Definition(id));
328+
let lexical = *attr.lexical_nesting_id();
329+
let str_id = *attr.str_id();
330+
let Some(owner_id) = self.resolve_lexical_owner_or_requeue(lexical, id) else {
334331
continue;
335332
};
336333

337-
self.create_declaration(*attr.str_id(), id, owner_id, |name| {
334+
self.create_declaration(str_id, id, owner_id, |name| {
338335
Declaration::Method(Box::new(MethodDeclaration::new(name, owner_id)))
339336
});
340337
}
341338
Definition::AttrWriter(attr) => {
342-
let Some(owner_id) = self.resolve_lexical_owner(*attr.lexical_nesting_id()) else {
343-
self.graph.push_work(Unit::Definition(id));
339+
let lexical = *attr.lexical_nesting_id();
340+
let str_id = *attr.str_id();
341+
let Some(owner_id) = self.resolve_lexical_owner_or_requeue(lexical, id) else {
344342
continue;
345343
};
346344

347-
self.create_declaration(*attr.str_id(), id, owner_id, |name| {
345+
self.create_declaration(str_id, id, owner_id, |name| {
348346
Declaration::Method(Box::new(MethodDeclaration::new(name, owner_id)))
349347
});
350348
}
@@ -534,7 +532,8 @@ impl<'a> Resolver<'a> {
534532
}
535533
},
536534
None => {
537-
let Some(resolved) = self.resolve_lexical_owner(*alias.lexical_nesting_id()) else {
535+
let lexical = *alias.lexical_nesting_id();
536+
let Some(resolved) = self.resolve_lexical_owner_or_requeue(lexical, id) else {
538537
continue;
539538
};
540539
resolved
@@ -615,6 +614,22 @@ impl<'a> Resolver<'a> {
615614
}
616615
}
617616

617+
/// Wraps `resolve_lexical_owner` for `handle_remaining_definitions` callers:
618+
/// when the owner can't be resolved (its singleton nesting has no declaration
619+
/// yet), re-queue the definition so the next resolve can place it on the
620+
/// right owner instead of dropping the work.
621+
fn resolve_lexical_owner_or_requeue(
622+
&mut self,
623+
lexical_nesting_id: Option<DefinitionId>,
624+
id: DefinitionId,
625+
) -> Option<DeclarationId> {
626+
let resolved = self.resolve_lexical_owner(lexical_nesting_id);
627+
if resolved.is_none() {
628+
self.graph.push_work(Unit::Definition(id));
629+
}
630+
resolved
631+
}
632+
618633
/// Resolves owner from lexical nesting.
619634
fn resolve_lexical_owner(&self, lexical_nesting_id: Option<DefinitionId>) -> Option<DeclarationId> {
620635
let Some(id) = lexical_nesting_id else {
@@ -623,13 +638,9 @@ impl<'a> Resolver<'a> {
623638

624639
// If no declaration exists yet for this definition, walk up the lexical chain.
625640
// This handles the case where attr_* definitions inside methods are processed
626-
// before the method definition itself.
627-
//
628-
// Exception: a SingletonClass with no declaration (because its receiver was just
629-
// deleted) must not fall back to the surrounding scope. Doing so would attach
630-
// members like `def bar` to e.g. `Object` instead of waiting for the singleton
631-
// to be reattached on a later resolve. Return `None` so the caller can skip and
632-
// re-queue the definition.
641+
// before the method definition itself. A SingletonClass with no declaration
642+
// is an exception: returning the surrounding scope would attach its members to
643+
// the wrong owner (e.g. `Object`) and never recover, so return None.
633644
let Some(declaration_id) = self.graph.definition_id_to_declaration_id(id) else {
634645
let definition = self.graph.definitions().get(&id).unwrap();
635646
if matches!(definition, Definition::SingletonClass(_)) {
@@ -1050,20 +1061,13 @@ impl<'a> Resolver<'a> {
10501061
let str_id = *name_ref.str();
10511062

10521063
let outcome = match self.name_owner_id(name_id, singleton) {
1053-
// name_owner_id returns Unresolved(None) only when the parent scope is genuinely
1054-
// unknown (e.g., `class A::B::C` where `A` doesn't exist). For regular declarations
1055-
// we create Todo placeholders for the missing parent chain so `B::C` can still be
1056-
// placed; Todos get promoted when real definitions appear later.
1064+
// For a regular declaration, create Todo placeholders for the missing parent chain
1065+
// so e.g. `class A::B::C` with `A` undefined can still be placed; Todos are promoted
1066+
// when real definitions appear later.
10571067
//
1058-
// Singleton classes are the exception: `class << UndefinedReceiver` attaches via
1059-
// `set_singleton_class_id`, not `add_member`, so a TODO receiver would never gain a
1060-
// member and would remain empty forever. Skip TODO creation and emit Retry instead
1061-
// of Unresolved so the unit is preserved across resolve cycles. `made_progress` only
1062-
// flips on Resolved, so retrying is safe — the inner fixpoint terminates and any
1063-
// leftover unit is drained back to `pending_work`. This matters for delete/re-add:
1064-
// after deleting `class Foo`, the `class << Foo; def bar; end` definition unit
1065-
// re-enters resolution with Foo missing; emitting Retry keeps it queued so the next
1066-
// resolve (with Foo restored) can attach `Foo::<Foo>#bar` again.
1068+
// Singletons attach via `set_singleton_class_id` (not `add_member`), so a Todo
1069+
// receiver would never gain a member. Emit Retry so the unit is preserved for a
1070+
// later resolve where the receiver may exist.
10671071
Outcome::Unresolved(None) if singleton => Outcome::Retry(None),
10681072
Outcome::Unresolved(None) => Outcome::Resolved(self.create_todo_for_parent(name_id), None),
10691073
other => other,

0 commit comments

Comments
 (0)