@@ -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