Skip to content

Commit d55bfaa

Browse files
committed
Re-queue constant-receiver definitions when receiver is missing
Mirror the `resolve_lexical_owner_or_requeue` policy for the constant- receiver paths in `handle_remaining_definitions`. Previously, an unresolved `NameRef` would silently `continue` and drop the work, so `def Foo.bar`, `@x` inside `def Foo.bar`, and `Foo.alias_method` were all lost across a delete/re-add of `Foo`. Extract `resolve_constant_receiver_or_requeue` and route the three ConstantReceiver call sites through it, with regression tests for each.
1 parent 7797fb4 commit d55bfaa

2 files changed

Lines changed: 108 additions & 13 deletions

File tree

rust/rubydex/src/model/graph.rs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4354,6 +4354,90 @@ mod incremental_resolution_tests {
43544354
assert_declaration_exists!(incremental, "Foo::<Foo>#bar()");
43554355
}
43564356

4357+
/// Same delete/re-add gap, but with explicit singleton method syntax
4358+
/// (`def Foo.bar`). The constant receiver is unresolved while `Foo` is
4359+
/// missing, so the method definition must be re-queued rather than dropped.
4360+
#[test]
4361+
fn explicit_singleton_method_survives_receiver_delete_readd() {
4362+
let mut incremental = GraphTest::new();
4363+
incremental.index_uri("file:///foo.rb", "class Foo; end");
4364+
incremental.index_uri("file:///singleton.rb", "def Foo.bar; end");
4365+
incremental.resolve();
4366+
assert_declaration_exists!(incremental, "Foo::<Foo>#bar()");
4367+
4368+
incremental.delete_uri("file:///foo.rb");
4369+
incremental.resolve();
4370+
assert_declaration_does_not_exist!(incremental, "Foo::<Foo>#bar()");
4371+
4372+
incremental.index_uri("file:///foo.rb", "class Foo; end");
4373+
incremental.resolve();
4374+
4375+
let mut fresh = GraphTest::new();
4376+
fresh.index_uri("file:///foo.rb", "class Foo; end");
4377+
fresh.index_uri("file:///singleton.rb", "def Foo.bar; end");
4378+
fresh.resolve();
4379+
4380+
assert_declaration_ids_match(&incremental, &fresh);
4381+
assert_declaration_exists!(incremental, "Foo::<Foo>#bar()");
4382+
}
4383+
4384+
/// Instance variables inside explicit singleton methods also depend on
4385+
/// resolving the constant receiver. Both the method and ivar definitions
4386+
/// must survive the temporary unresolved receiver.
4387+
#[test]
4388+
fn explicit_singleton_method_ivar_survives_receiver_delete_readd() {
4389+
let mut incremental = GraphTest::new();
4390+
incremental.index_uri("file:///foo.rb", "class Foo; end");
4391+
incremental.index_uri("file:///singleton.rb", "def Foo.bar; @x = 1; end");
4392+
incremental.resolve();
4393+
assert_declaration_exists!(incremental, "Foo::<Foo>#bar()");
4394+
assert_declaration_exists!(incremental, "Foo::<Foo>#@x");
4395+
4396+
incremental.delete_uri("file:///foo.rb");
4397+
incremental.resolve();
4398+
assert_declaration_does_not_exist!(incremental, "Foo::<Foo>#bar()");
4399+
assert_declaration_does_not_exist!(incremental, "Foo::<Foo>#@x");
4400+
4401+
incremental.index_uri("file:///foo.rb", "class Foo; end");
4402+
incremental.resolve();
4403+
4404+
let mut fresh = GraphTest::new();
4405+
fresh.index_uri("file:///foo.rb", "class Foo; end");
4406+
fresh.index_uri("file:///singleton.rb", "def Foo.bar; @x = 1; end");
4407+
fresh.resolve();
4408+
4409+
assert_declaration_ids_match(&incremental, &fresh);
4410+
assert_declaration_exists!(incremental, "Foo::<Foo>#bar()");
4411+
assert_declaration_exists!(incremental, "Foo::<Foo>#@x");
4412+
}
4413+
4414+
/// Method aliases with a constant receiver (`Foo.alias_method`) use a
4415+
/// separate receiver path from `def Foo.bar`; it must also preserve work
4416+
/// while the receiver is temporarily absent.
4417+
#[test]
4418+
fn constant_receiver_method_alias_survives_receiver_delete_readd() {
4419+
let mut incremental = GraphTest::new();
4420+
incremental.index_uri("file:///foo.rb", "class Foo; end");
4421+
incremental.index_uri("file:///alias.rb", "Foo.alias_method :new_name, :old_name");
4422+
incremental.resolve();
4423+
assert_declaration_exists!(incremental, "Foo#new_name()");
4424+
4425+
incremental.delete_uri("file:///foo.rb");
4426+
incremental.resolve();
4427+
assert_declaration_does_not_exist!(incremental, "Foo#new_name()");
4428+
4429+
incremental.index_uri("file:///foo.rb", "class Foo; end");
4430+
incremental.resolve();
4431+
4432+
let mut fresh = GraphTest::new();
4433+
fresh.index_uri("file:///foo.rb", "class Foo; end");
4434+
fresh.index_uri("file:///alias.rb", "Foo.alias_method :new_name, :old_name");
4435+
fresh.resolve();
4436+
4437+
assert_declaration_ids_match(&incremental, &fresh);
4438+
assert_declaration_exists!(incremental, "Foo#new_name()");
4439+
}
4440+
43574441
/// Same shape as `singleton_definition_survives_receiver_delete_readd` but
43584442
/// for a method alias inside the singleton block. Without re-queueing the
43594443
/// def, `alias new old` would be silently dropped while `Foo` is missing

rust/rubydex/src/resolution.rs

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -287,11 +287,8 @@ impl<'a> Resolver<'a> {
287287
unreachable!("SelfReceiver methods should be routed to handle_definition_unit");
288288
}
289289
Some(Receiver::ConstantReceiver(name_id)) => {
290-
let receiver_decl_id = match self.graph.names().get(name_id).unwrap() {
291-
NameRef::Resolved(resolved) => *resolved.declaration_id(),
292-
NameRef::Unresolved(_) => {
293-
continue;
294-
}
290+
let Some(receiver_decl_id) = self.resolve_constant_receiver_or_requeue(*name_id, id) else {
291+
continue;
295292
};
296293

297294
let Some(singleton_id) = self.get_or_create_singleton_class(receiver_decl_id) else {
@@ -383,11 +380,12 @@ impl<'a> Resolver<'a> {
383380
.definition_id_to_declaration_id(*def_id)
384381
.expect("SelfReceiver definition should have a declaration"),
385382
Receiver::ConstantReceiver(name_id) => {
386-
let Some(NameRef::Resolved(resolved)) = self.graph.names().get(name_id) else {
383+
let Some(receiver_decl_id) =
384+
self.resolve_constant_receiver_or_requeue(*name_id, id)
385+
else {
387386
continue;
388387
};
389-
390-
*resolved.declaration_id()
388+
receiver_decl_id
391389
}
392390
};
393391

@@ -525,12 +523,12 @@ impl<'a> Resolver<'a> {
525523
};
526524
owner_id
527525
}
528-
Some(Receiver::ConstantReceiver(name_id)) => match self.graph.names().get(name_id).unwrap() {
529-
NameRef::Resolved(resolved) => *resolved.declaration_id(),
530-
NameRef::Unresolved(_) => {
526+
Some(Receiver::ConstantReceiver(name_id)) => {
527+
let Some(resolved) = self.resolve_constant_receiver_or_requeue(*name_id, id) else {
531528
continue;
532-
}
533-
},
529+
};
530+
resolved
531+
}
534532
None => {
535533
let lexical = *alias.lexical_nesting_id();
536534
let Some(resolved) = self.resolve_lexical_owner_or_requeue(lexical, id) else {
@@ -566,6 +564,19 @@ impl<'a> Resolver<'a> {
566564
}
567565
}
568566

567+
/// Resolves a constant receiver for `handle_remaining_definitions`.
568+
/// If the receiver name is unresolved, preserve the definition for a later
569+
/// resolve cycle instead of dropping work during an incremental delete/re-add gap.
570+
fn resolve_constant_receiver_or_requeue(&mut self, name_id: NameId, id: DefinitionId) -> Option<DeclarationId> {
571+
match self.graph.names().get(&name_id).unwrap() {
572+
NameRef::Resolved(resolved) => Some(*resolved.declaration_id()),
573+
NameRef::Unresolved(_) => {
574+
self.graph.push_work(Unit::Definition(id));
575+
None
576+
}
577+
}
578+
}
579+
569580
fn create_declaration<F>(
570581
&mut self,
571582
str_id: StringId,

0 commit comments

Comments
 (0)