Skip to content

Commit 2a5e113

Browse files
authored
Follow aliases when they are used in mixins or parent classes (#620)
When a constant alias is used in a mixin operation or a parent class, we need to follow the alias to figure out what namespace it points to.
2 parents 5fea946 + adc0cd9 commit 2a5e113

1 file changed

Lines changed: 135 additions & 71 deletions

File tree

rust/rubydex/src/resolution.rs

Lines changed: 135 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -286,32 +286,22 @@ impl<'a> Resolver<'a> {
286286
// `Foo` is undefined). The method is orphaned as a consequence.
287287
continue;
288288
};
289-
self.get_or_create_singleton_class(owner_decl_id)
290-
.expect("Methods with self receiver should always be inside a namespace")
289+
let Some(singleton_id) = self.get_or_create_singleton_class(owner_decl_id) else {
290+
// The enclosing declaration is a constant that can't be promoted to a namespace
291+
// (e.g., `CONST = 1; module CONST; def self.bar; end; end`). The method is
292+
// orphaned as a consequence.
293+
continue;
294+
};
295+
singleton_id
291296
}
292297
Some(Receiver::ConstantReceiver(name_id)) => {
293-
let mut receiver_decl_id = match self.graph.names().get(name_id).unwrap() {
298+
let receiver_decl_id = match self.graph.names().get(name_id).unwrap() {
294299
NameRef::Resolved(resolved) => *resolved.declaration_id(),
295300
NameRef::Unresolved(_) => {
296301
continue;
297302
}
298303
};
299304

300-
let receiver_decl = self.graph.declarations().get(&receiver_decl_id).unwrap();
301-
302-
// If the method receiver is a constant alias, it needs to point to a namespace or else we don't have a place to declare the method
303-
if matches!(receiver_decl, Declaration::ConstantAlias(_)) {
304-
let resolved_ids = self.resolve_alias_chains(receiver_decl_id);
305-
let Some(namespace) = resolved_ids
306-
.iter()
307-
.find(|id| self.graph.declarations().get(id).unwrap().as_namespace().is_some())
308-
else {
309-
continue;
310-
};
311-
312-
receiver_decl_id = *namespace;
313-
}
314-
315305
let Some(singleton_id) = self.get_or_create_singleton_class(receiver_decl_id) else {
316306
continue;
317307
};
@@ -387,22 +377,7 @@ impl<'a> Resolver<'a> {
387377
continue;
388378
};
389379

390-
let mut receiver_decl_id = *resolved.declaration_id();
391-
let receiver_decl = self.graph.declarations().get(&receiver_decl_id).unwrap();
392-
393-
// If the method receiver is a constant alias, it needs to point to a namespace or else we don't have a place to declare the method
394-
if matches!(receiver_decl, Declaration::ConstantAlias(_)) {
395-
let resolved_ids = self.resolve_alias_chains(receiver_decl_id);
396-
let Some(namespace) = resolved_ids.iter().find(|id| {
397-
self.graph.declarations().get(id).unwrap().as_namespace().is_some()
398-
}) else {
399-
continue;
400-
};
401-
402-
receiver_decl_id = *namespace;
403-
}
404-
405-
receiver_decl_id
380+
*resolved.declaration_id()
406381
}
407382
};
408383

@@ -616,6 +591,14 @@ impl<'a> Resolver<'a> {
616591
fn get_or_create_singleton_class(&mut self, attached_id: DeclarationId) -> Option<DeclarationId> {
617592
let attached_decl = self.graph.declarations().get(&attached_id).unwrap();
618593

594+
// If the attached object is a constant alias, follow the alias chain to find the actual namespace
595+
if matches!(attached_decl, Declaration::ConstantAlias(_)) {
596+
return match self.resolve_to_namespace(attached_id) {
597+
Some(id) => self.get_or_create_singleton_class(id),
598+
None => None,
599+
};
600+
}
601+
619602
if matches!(attached_decl, Declaration::Constant(_)) {
620603
if self.graph.all_definitions_promotable(attached_decl) {
621604
self.graph.promote_constant_to_namespace(attached_id, |name, owner_id| {
@@ -852,15 +835,11 @@ impl<'a> Resolver<'a> {
852835
Mixin::Prepend(_) => {
853836
match self.graph.names().get(constant_reference.name_id()).unwrap() {
854837
NameRef::Resolved(resolved) => {
855-
let declaration_id = resolved.declaration_id();
856-
857-
// Skip if the mixin is not a namespace (e.g.: dynamically defined class or module that we
858-
// misidentified as a constant)
859-
if !self.graph.is_namespace(declaration_id) {
838+
let Some(module_id) = self.resolve_to_namespace(*resolved.declaration_id()) else {
860839
continue;
861-
}
840+
};
862841

863-
let ids = match self.linearize_ancestors(*declaration_id, context) {
842+
let ids = match self.linearize_ancestors(module_id, context) {
864843
Ancestors::Complete(ids) => ids,
865844
Ancestors::Cyclic(ids) => {
866845
context.cyclic = true;
@@ -895,15 +874,11 @@ impl<'a> Resolver<'a> {
895874
Mixin::Include(_) | Mixin::Extend(_) => {
896875
match self.graph.names().get(constant_reference.name_id()).unwrap() {
897876
NameRef::Resolved(resolved) => {
898-
let declaration_id = resolved.declaration_id();
899-
900-
// Skip if the mixin is not a namespace (e.g.: dynamically defined class or module that we
901-
// misidentified as a constant)
902-
if !self.graph.is_namespace(declaration_id) {
877+
let Some(module_id) = self.resolve_to_namespace(*resolved.declaration_id()) else {
903878
continue;
904-
}
879+
};
905880

906-
let mut ids = match self.linearize_ancestors(*declaration_id, context) {
881+
let mut ids = match self.linearize_ancestors(module_id, context) {
907882
Ancestors::Complete(ids) => ids,
908883
Ancestors::Cyclic(ids) => {
909884
context.cyclic = true;
@@ -1200,6 +1175,20 @@ impl<'a> Resolver<'a> {
12001175
}
12011176
}
12021177

1178+
/// If `declaration_id` is already a namespace, returns it directly. If it's a `ConstantAlias`, follows the alias
1179+
/// chain and returns the first namespace found. Returns `None` for all other declaration types or unresolved alias
1180+
/// chains.
1181+
fn resolve_to_namespace(&self, declaration_id: DeclarationId) -> Option<DeclarationId> {
1182+
match self.graph.declarations().get(&declaration_id)? {
1183+
Declaration::Namespace(_) => Some(declaration_id),
1184+
Declaration::ConstantAlias(_) => self
1185+
.resolve_alias_chains(declaration_id)
1186+
.into_iter()
1187+
.find(|id| self.graph.is_namespace(id)),
1188+
_ => None,
1189+
}
1190+
}
1191+
12031192
/// Resolves an alias chain to get all possible final target declarations.
12041193
/// Returns the original ID if it's not an alias or if the target hasn't been resolved yet.
12051194
///
@@ -1253,10 +1242,27 @@ impl<'a> Resolver<'a> {
12531242
return scope_outcome;
12541243
}
12551244

1256-
// Search inheritance chain
1245+
// Search inheritance chain, following alias chains to find the actual namespace
12571246
let ancestor_outcome = match self.graph.names().get(nesting).unwrap() {
12581247
NameRef::Resolved(nesting_name_ref) => {
1259-
self.search_ancestors(*nesting_name_ref.declaration_id(), str_id)
1248+
let resolved_ids = self.resolve_alias_chains(*nesting_name_ref.declaration_id());
1249+
let mut result = Outcome::Unresolved(None);
1250+
1251+
for &id in &resolved_ids {
1252+
match self.graph.declarations().get(&id) {
1253+
Some(Declaration::ConstantAlias(_)) => {
1254+
result = Outcome::Retry(None);
1255+
break;
1256+
}
1257+
Some(Declaration::Namespace(_)) => {
1258+
result = self.search_ancestors(id, str_id);
1259+
break;
1260+
}
1261+
_ => {}
1262+
}
1263+
}
1264+
1265+
result
12601266
}
12611267
NameRef::Unresolved(_) => Outcome::Retry(None),
12621268
};
@@ -1344,23 +1350,11 @@ impl<'a> Resolver<'a> {
13441350
if let NameRef::Resolved(nesting_name_ref) = self.graph.names().get(nesting_id).unwrap() {
13451351
let declaration_id = *nesting_name_ref.declaration_id();
13461352

1347-
match self.graph.declarations().get(&declaration_id) {
1348-
Some(Declaration::Namespace(namespace)) => {
1349-
if let Some(member) = namespace.member(&str_id) {
1350-
return Outcome::Resolved(*member, None);
1351-
}
1352-
}
1353-
Some(Declaration::ConstantAlias(_)) => {
1354-
for id in &self.resolve_alias_chains(declaration_id) {
1355-
if let Some(declaration) = self.graph.declarations().get(id)
1356-
&& let Some(namespace) = declaration.as_namespace()
1357-
&& let Some(member) = namespace.member(&str_id)
1358-
{
1359-
return Outcome::Resolved(*member, None);
1360-
}
1361-
}
1362-
}
1363-
_ => {}
1353+
if let Some(namespace_id) = self.resolve_to_namespace(declaration_id)
1354+
&& let Some(namespace) = self.graph.declarations().get(&namespace_id).unwrap().as_namespace()
1355+
&& let Some(member) = namespace.member(&str_id)
1356+
{
1357+
return Outcome::Resolved(*member, None);
13641358
}
13651359

13661360
current_name = nesting_name_ref.name();
@@ -1583,10 +1577,8 @@ impl<'a> Resolver<'a> {
15831577

15841578
match name {
15851579
NameRef::Resolved(resolved) => {
1586-
let declaration_id = resolved.declaration_id();
1587-
1588-
if self.graph.is_namespace(declaration_id) {
1589-
explicit_parents.push(*declaration_id);
1580+
if let Some(parent_id) = self.resolve_to_namespace(*resolved.declaration_id()) {
1581+
explicit_parents.push(parent_id);
15901582
}
15911583
}
15921584
NameRef::Unresolved(_) => {
@@ -4967,4 +4959,76 @@ mod tests {
49674959
assert_declaration_exists!(context, "Baz");
49684960
assert_constant_reference_to!(context, "CONST", "file:///reopen.rb:5:5-5:10");
49694961
}
4962+
4963+
#[test]
4964+
fn constant_alias_reopened_as_class_with_nested_inheritance() {
4965+
let mut context = GraphTest::new();
4966+
context.index_uri("file:///a.rb", {
4967+
r"
4968+
module Foo
4969+
Bar = ::Object
4970+
end
4971+
4972+
module Foo
4973+
class Bar
4974+
class Baz < Something
4975+
end
4976+
end
4977+
end
4978+
"
4979+
});
4980+
context.resolve();
4981+
4982+
assert_declaration_exists!(context, "Foo::Bar");
4983+
}
4984+
4985+
#[test]
4986+
fn superclass_through_alias() {
4987+
let mut context = GraphTest::new();
4988+
context.index_uri("file:///a.rb", {
4989+
r"
4990+
class Base; end
4991+
AliasedBase = Base
4992+
class Foo < AliasedBase; end
4993+
"
4994+
});
4995+
context.resolve();
4996+
assert_no_diagnostics!(&context);
4997+
assert_ancestors_eq!(context, "Foo", ["Foo", "Base", "Object"]);
4998+
}
4999+
5000+
#[test]
5001+
fn mixin_through_alias() {
5002+
let mut context = GraphTest::new();
5003+
context.index_uri("file:///a.rb", {
5004+
r"
5005+
module M; end
5006+
AliasM = M
5007+
class Foo
5008+
include AliasM
5009+
end
5010+
"
5011+
});
5012+
context.resolve();
5013+
assert_no_diagnostics!(&context);
5014+
assert_ancestors_eq!(context, "Foo", ["Foo", "M", "Object"]);
5015+
}
5016+
5017+
#[test]
5018+
fn self_method_inside_non_promotable_constant() {
5019+
let mut context = GraphTest::new();
5020+
context.index_uri("file:///a.rb", {
5021+
r"
5022+
CONST = 1
5023+
module CONST
5024+
def self.bar
5025+
end
5026+
end
5027+
"
5028+
});
5029+
// Should not panic when a `def self.` method is inside a constant that can't be promoted to a namespace (e.g.,
5030+
// `CONST = 1` is non-promotable).
5031+
context.resolve();
5032+
assert_declaration_exists!(context, "CONST");
5033+
}
49705034
}

0 commit comments

Comments
 (0)