Skip to content

Commit ab88ef1

Browse files
authored
Merge pull request #867 from Shopify/uk_fix_singleton_method_non_namespace_owner_panic
Fix panic resolving singleton methods on non-namespace owners
2 parents c9f07e6 + 34d672a commit ab88ef1

1 file changed

Lines changed: 46 additions & 13 deletions

File tree

rust/rubydex/src/model/graph.rs

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)