@@ -311,9 +311,6 @@ impl Graph {
311311 self . definition_to_declaration_id ( self . definitions . get ( & definition_id) . unwrap ( ) )
312312 }
313313
314- /// # Panics
315- ///
316- /// Panics if the definition is not found
317314 #[ must_use]
318315 pub fn definition_to_declaration_id ( & self , definition : & Definition ) -> Option < & DeclarationId > {
319316 let ( nesting_name_id, member_str_id) = match definition {
@@ -447,20 +444,21 @@ impl Graph {
447444 }
448445
449446 /// Looks up the declaration for a `SelfReceiver` method/alias through the singleton class.
447+ ///
448+ /// Returns `None` when the owner cannot be resolved to a namespace with a singleton class. This
449+ /// can happen when the enclosing construct resolved to a non-namespace declaration (e.g. a
450+ /// constant or constant alias that a same-named `class`/`module` reopened without promotion), in
451+ /// which case the method has no owning declaration.
450452 fn find_self_receiver_declaration ( & self , def_id : DefinitionId , member_str_id : StringId ) -> Option < & DeclarationId > {
451453 let owner_decl_id = self . definition_id_to_declaration_id ( def_id) ?;
452454 let singleton_id = self
453455 . declarations
454- . get ( owner_decl_id)
455- . unwrap ( )
456- . as_namespace ( )
457- . unwrap ( )
456+ . get ( owner_decl_id) ?
457+ . as_namespace ( ) ?
458458 . singleton_class ( ) ?;
459459 self . declarations
460- . get ( singleton_id)
461- . unwrap ( )
462- . as_namespace ( )
463- . unwrap ( )
460+ . get ( singleton_id) ?
461+ . as_namespace ( ) ?
464462 . member ( & member_str_id)
465463 }
466464
@@ -1545,8 +1543,8 @@ mod tests {
15451543 use crate :: model:: declaration:: Ancestors ;
15461544 use crate :: test_utils:: GraphTest ;
15471545 use crate :: {
1548- assert_declaration_does_not_exist, assert_dependents , assert_descendants , assert_members_eq ,
1549- assert_no_diagnostics, assert_no_members,
1546+ assert_declaration_does_not_exist, assert_declaration_kind_eq , assert_dependents , assert_descendants ,
1547+ assert_members_eq , assert_no_diagnostics, assert_no_members,
15501548 } ;
15511549
15521550 #[ test]
@@ -1568,6 +1566,41 @@ mod tests {
15681566 ) ;
15691567 }
15701568
1569+ #[ test]
1570+ fn singleton_method_in_non_namespace_owner_does_not_panic ( ) {
1571+ // `Aliased = Bar` assigns a constant to another constant, producing a (non-promotable)
1572+ // `ConstantAlias` declaration. Reopening it with `class Aliased` is valid Ruby (it reopens
1573+ // `Bar`), but the class definition is attached to the existing `ConstantAlias` declaration
1574+ // without promoting it to a namespace. A `def self.foo` inside then has a `SelfReceiver`
1575+ // owner whose declaration is not a namespace. Resolving that definition to its declaration
1576+ // must return `None` rather than panicking.
1577+ let mut context = GraphTest :: new ( ) ;
1578+
1579+ context. index_uri (
1580+ "file:///foo.rb" ,
1581+ "
1582+ class Bar
1583+ end
1584+
1585+ Aliased = Bar
1586+
1587+ class Aliased
1588+ def self.foo; end
1589+ end
1590+ " ,
1591+ ) ;
1592+ context. resolve ( ) ;
1593+
1594+ // The declaration stays a non-namespace constant alias.
1595+ assert_declaration_kind_eq ! ( context, "Aliased" , "ConstantAlias" ) ;
1596+
1597+ // Mirrors what consumers like the DOT exporter do: resolve every definition to its
1598+ // declaration. This previously panicked on the singleton method's non-namespace owner.
1599+ for definition in context. graph ( ) . definitions ( ) . values ( ) {
1600+ let _ = context. graph ( ) . definition_to_declaration_id ( definition) ;
1601+ }
1602+ }
1603+
15711604 #[ test]
15721605 fn deleting_file_triggers_name_dependent_cleanup ( ) {
15731606 let mut context = GraphTest :: new ( ) ;
0 commit comments