Skip to content

Commit ad43a1b

Browse files
committed
Use Todo promotion to collapse resolution into a single pass
1 parent 95f9a1a commit ad43a1b

2 files changed

Lines changed: 103 additions & 17 deletions

File tree

rust/rubydex/src/model/graph.rs

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -83,14 +83,20 @@ impl Graph {
8383
{
8484
let declaration_id = DeclarationId::from(&fully_qualified_name);
8585

86-
let should_promote = self.declarations.get(&declaration_id).is_some_and(|existing| {
87-
matches!(existing, Declaration::Constant(_))
88-
&& matches!(
89-
self.definitions.get(&definition_id),
90-
Some(Definition::Class(_) | Definition::Module(_) | Definition::SingletonClass(_))
91-
)
92-
&& self.all_definitions_promotable(existing)
93-
});
86+
let is_namespace_definition = matches!(
87+
self.definitions.get(&definition_id),
88+
Some(Definition::Class(_) | Definition::Module(_) | Definition::SingletonClass(_))
89+
);
90+
91+
let should_promote = is_namespace_definition
92+
&& self
93+
.declarations
94+
.get(&declaration_id)
95+
.is_some_and(|existing| match existing {
96+
Declaration::Constant(_) => self.all_definitions_promotable(existing),
97+
Declaration::Namespace(Namespace::Todo(_)) => true,
98+
_ => false,
99+
});
94100

95101
match self.declarations.entry(declaration_id) {
96102
Entry::Occupied(mut occupied_entry) => {
@@ -632,6 +638,10 @@ impl Graph {
632638
Declaration::Namespace(Namespace::SingletonClass(it)) => {
633639
it.add_member(member_str_id, member_declaration_id);
634640
}
641+
Declaration::Namespace(Namespace::Todo(it)) => it.add_member(member_str_id, member_declaration_id),
642+
Declaration::Constant(_) => {
643+
// TODO: temporary hack to avoid crashing on `Struct.new`, `Class.new` and `Module.new`
644+
}
635645
_ => panic!("Tried to add member to a declaration that isn't a namespace"),
636646
}
637647
}

rust/rubydex/src/resolution.rs

Lines changed: 85 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use crate::model::{
77
declaration::{
88
Ancestor, Ancestors, ClassDeclaration, ClassVariableDeclaration, ConstantAliasDeclaration, ConstantDeclaration,
99
Declaration, GlobalVariableDeclaration, InstanceVariableDeclaration, MethodDeclaration, ModuleDeclaration,
10-
Namespace, SingletonClassDeclaration,
10+
Namespace, SingletonClassDeclaration, TodoDeclaration,
1111
},
1212
definitions::{Definition, Mixin, Receiver},
1313
graph::{CLASS_ID, Graph, MODULE_ID, OBJECT_ID},
@@ -211,6 +211,7 @@ impl<'a> Resolver<'a> {
211211
}
212212
Outcome::Unresolved(None) => {
213213
// We couldn't resolve this name. Emit a diagnostic
214+
self.unit_queue.push_back(unit_id);
214215
}
215216
Outcome::Retry(Some(id_needing_linearization)) | Outcome::Unresolved(Some(id_needing_linearization)) => {
216217
self.unit_queue.push_back(unit_id);
@@ -1027,12 +1028,51 @@ impl<'a> Resolver<'a> {
10271028
fn name_owner_id(&mut self, name_id: NameId) -> Outcome {
10281029
let name_ref = self.graph.names().get(&name_id).unwrap();
10291030

1030-
if let Some(parent_scope) = name_ref.parent_scope().as_ref() {
1031+
if let Some(&parent_scope) = name_ref.parent_scope().as_ref() {
10311032
// If we have `A::B`, the owner of `B` is whatever `A` resolves to.
10321033
// If `A` is an alias, resolve through to get the actual namespace.
1033-
match self.resolve_constant_internal(*parent_scope) {
1034+
// On `Retry`, we don't create a Todo: the parent may still resolve through inheritance once ancestors are
1035+
// linearized. We only create Todos for `Unresolved` outcomes where the parent is genuinely unknown.
1036+
match self.resolve_constant_internal(parent_scope) {
10341037
Outcome::Resolved(id, linearization) => self.resolve_to_primary_namespace(id, linearization),
1035-
other => other,
1038+
// Retry or Unresolved(Some(_)) means we might find it later through ancestor linearization
1039+
Outcome::Retry(id) => Outcome::Retry(id),
1040+
Outcome::Unresolved(Some(id)) => Outcome::Unresolved(Some(id)),
1041+
// Only create a Todo when genuinely unresolvable (no pending linearizations)
1042+
Outcome::Unresolved(None) => {
1043+
let parent_name = self.graph.names().get(&parent_scope).unwrap();
1044+
let parent_str_id = *parent_name.str();
1045+
1046+
let parent_owner_id = match self.name_owner_id(parent_scope) {
1047+
Outcome::Resolved(id, _) => id,
1048+
_ => *OBJECT_ID,
1049+
};
1050+
1051+
let fully_qualified_name = if parent_owner_id == *OBJECT_ID {
1052+
self.graph.strings().get(&parent_str_id).unwrap().to_string()
1053+
} else {
1054+
format!(
1055+
"{}::{}",
1056+
self.graph.declarations().get(&parent_owner_id).unwrap().name(),
1057+
self.graph.strings().get(&parent_str_id).unwrap().as_str()
1058+
)
1059+
};
1060+
1061+
let declaration_id = DeclarationId::from(&fully_qualified_name);
1062+
1063+
if !self.graph.declarations().contains_key(&declaration_id) {
1064+
self.graph.declarations_mut().insert(
1065+
declaration_id,
1066+
Declaration::Namespace(Namespace::Todo(Box::new(TodoDeclaration::new(
1067+
fully_qualified_name,
1068+
parent_owner_id,
1069+
)))),
1070+
);
1071+
self.graph.add_member(&parent_owner_id, declaration_id, parent_str_id);
1072+
}
1073+
1074+
Outcome::Resolved(declaration_id, None)
1075+
}
10361076
}
10371077
} else if let Some(nesting_id) = name_ref.nesting()
10381078
&& !name_ref.parent_scope().is_top_level()
@@ -1633,8 +1673,8 @@ mod tests {
16331673
assert_constant_reference_to, assert_declaration_definitions_count_eq, assert_declaration_does_not_exist,
16341674
assert_declaration_exists, assert_declaration_kind_eq, assert_declaration_references_count_eq,
16351675
assert_descendants, assert_diagnostics_eq, assert_instance_variables_eq, assert_members_eq,
1636-
assert_no_constant_alias_target, assert_no_diagnostics, assert_no_members, assert_owner_eq,
1637-
assert_singleton_class_eq,
1676+
assert_no_constant_alias_target, assert_no_diagnostics, assert_no_members, assert_no_todo_declarations,
1677+
assert_owner_eq, assert_singleton_class_eq, assert_todo_declarations_eq,
16381678
};
16391679

16401680
#[test]
@@ -1762,6 +1802,7 @@ mod tests {
17621802
context.resolve();
17631803

17641804
assert_no_diagnostics!(&context);
1805+
assert_no_todo_declarations!(context);
17651806

17661807
assert_members_eq!(context, "Foo", ["Bar", "Baz"]);
17671808
assert_owner_eq!(context, "Foo", "Object");
@@ -1819,6 +1860,7 @@ mod tests {
18191860
context.resolve();
18201861

18211862
assert_no_diagnostics!(&context);
1863+
assert_no_todo_declarations!(context);
18221864

18231865
assert_no_members!(context, "Foo");
18241866
assert_owner_eq!(context, "Foo", "Object");
@@ -1843,6 +1885,7 @@ mod tests {
18431885
context.resolve();
18441886

18451887
assert_no_diagnostics!(&context);
1888+
assert_no_todo_declarations!(context);
18461889

18471890
assert_no_members!(context, "Foo");
18481891
assert_owner_eq!(context, "Foo", "Object");
@@ -1869,9 +1912,12 @@ mod tests {
18691912

18701913
assert_no_diagnostics!(&context);
18711914

1872-
assert_declaration_does_not_exist!(context, "Foo");
1873-
assert_declaration_does_not_exist!(context, "Foo::Bar");
1874-
assert_declaration_does_not_exist!(context, "Foo::Bar::Baz");
1915+
assert_todo_declarations_eq!(context, ["Foo"]);
1916+
1917+
assert_members_eq!(context, "Object", vec!["Foo"]);
1918+
assert_members_eq!(context, "Foo", vec!["Bar"]);
1919+
assert_members_eq!(context, "Foo::Bar", vec!["Baz"]);
1920+
assert_no_members!(context, "Foo::Bar::Baz");
18751921
}
18761922

18771923
#[test]
@@ -4106,6 +4152,7 @@ mod tests {
41064152
context.resolve();
41074153

41084154
assert_no_diagnostics!(&context);
4155+
assert_no_todo_declarations!(context);
41094156

41104157
// In the same order of appearence
41114158
assert_declaration_exists!(context, "Foo");
@@ -4151,6 +4198,7 @@ mod tests {
41514198
context.resolve();
41524199

41534200
assert_no_diagnostics!(&context, &[Rule::ParseWarning]);
4201+
assert_no_todo_declarations!(context);
41544202

41554203
// FIXME: this is wrong, the reference is not to `Bar::Foo`, but to `Foo`
41564204
assert_constant_reference_to!(context, "Bar::Foo", "file:///foo.rb:4:3-4:6");
@@ -4967,4 +5015,32 @@ mod tests {
49675015
assert_declaration_exists!(context, "Baz");
49685016
assert_constant_reference_to!(context, "CONST", "file:///reopen.rb:5:5-5:10");
49695017
}
5018+
5019+
#[test]
5020+
fn resolve_missing_declaration_to_todo() {
5021+
let mut context = GraphTest::new();
5022+
context.index_uri("file:///foo.rb", {
5023+
r"
5024+
class Foo::Bar
5025+
include Foo::Baz
5026+
5027+
def bar; end
5028+
end
5029+
5030+
module Foo::Baz
5031+
def baz; end
5032+
end
5033+
"
5034+
});
5035+
context.resolve();
5036+
5037+
assert_no_diagnostics!(&context);
5038+
5039+
assert_todo_declarations_eq!(context, ["Foo"]);
5040+
5041+
assert_members_eq!(context, "Object", vec!["Foo"]);
5042+
assert_members_eq!(context, "Foo", vec!["Bar", "Baz"]);
5043+
assert_members_eq!(context, "Foo::Bar", vec!["bar()"]);
5044+
assert_members_eq!(context, "Foo::Baz", vec!["baz()"]);
5045+
}
49705046
}

0 commit comments

Comments
 (0)