Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions ext/rubydex/declaration.c
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ VALUE cNamespace;
VALUE cClass;
VALUE cModule;
VALUE cSingletonClass;
VALUE cTodo;
VALUE cConstant;
VALUE cConstantAlias;
VALUE cMethod;
Expand All @@ -26,6 +27,8 @@ VALUE rdxi_declaration_class_for_kind(CDeclarationKind kind) {
return cModule;
case CDeclarationKind_SingletonClass:
return cSingletonClass;
case CDeclarationKind_Todo:
return cTodo;
case CDeclarationKind_Constant:
return cConstant;
case CDeclarationKind_ConstantAlias:
Expand Down Expand Up @@ -297,6 +300,7 @@ void rdxi_initialize_declaration(VALUE mRubydex) {
cClass = rb_define_class_under(mRubydex, "Class", cNamespace);
cModule = rb_define_class_under(mRubydex, "Module", cNamespace);
cSingletonClass = rb_define_class_under(mRubydex, "SingletonClass", cNamespace);
cTodo = rb_define_class_under(mRubydex, "Todo", cNamespace);
cConstant = rb_define_class_under(mRubydex, "Constant", cDeclaration);
cConstantAlias = rb_define_class_under(mRubydex, "ConstantAlias", cDeclaration);
cMethod = rb_define_class_under(mRubydex, "Method", cDeclaration);
Expand Down
1 change: 1 addition & 0 deletions ext/rubydex/declaration.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ extern VALUE cNamespace;
extern VALUE cClass;
extern VALUE cModule;
extern VALUE cSingletonClass;
extern VALUE cTodo;
extern VALUE cConstant;
extern VALUE cConstantAlias;
extern VALUE cMethod;
Expand Down
2 changes: 2 additions & 0 deletions rust/rubydex-sys/src/declaration_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub enum CDeclarationKind {
GlobalVariable = 6,
InstanceVariable = 7,
ClassVariable = 8,
Todo = 9,
}

#[repr(C)]
Expand Down Expand Up @@ -51,6 +52,7 @@ impl CDeclaration {
Declaration::Namespace(Namespace::Class(_)) => CDeclarationKind::Class,
Declaration::Namespace(Namespace::Module(_)) => CDeclarationKind::Module,
Declaration::Namespace(Namespace::SingletonClass(_)) => CDeclarationKind::SingletonClass,
Declaration::Namespace(Namespace::Todo(_)) => CDeclarationKind::Todo,
Declaration::Constant(_) => CDeclarationKind::Constant,
Declaration::ConstantAlias(_) => CDeclarationKind::ConstantAlias,
Declaration::Method(_) => CDeclarationKind::Method,
Expand Down
7 changes: 6 additions & 1 deletion rust/rubydex/src/model/declaration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ macro_rules! all_declarations {
Declaration::Namespace(Namespace::Class($var)) => $expr,
Declaration::Namespace(Namespace::Module($var)) => $expr,
Declaration::Namespace(Namespace::SingletonClass($var)) => $expr,
Declaration::Namespace(Namespace::Todo($var)) => $expr,
Declaration::Constant($var) => $expr,
Declaration::ConstantAlias($var) => $expr,
Declaration::Method($var) => $expr,
Expand All @@ -76,6 +77,7 @@ macro_rules! all_namespaces {
Namespace::Class($var) => $expr,
Namespace::Module($var) => $expr,
Namespace::SingletonClass($var) => $expr,
Namespace::Todo($var) => $expr,
}
};
}
Expand Down Expand Up @@ -378,6 +380,7 @@ pub enum Namespace {
Class(Box<ClassDeclaration>),
SingletonClass(Box<SingletonClassDeclaration>),
Module(Box<ModuleDeclaration>),
Todo(Box<TodoDeclaration>),
}
assert_mem_size!(Namespace, 16);

Expand All @@ -388,6 +391,7 @@ impl Namespace {
Namespace::Class(_) => "Class",
Namespace::SingletonClass(_) => "SingletonClass",
Namespace::Module(_) => "Module",
Namespace::Todo(_) => "<TODO>",
}
}

Expand Down Expand Up @@ -504,7 +508,8 @@ namespace_declaration!(Module, ModuleDeclaration);
assert_mem_size!(ModuleDeclaration, 224);
namespace_declaration!(SingletonClass, SingletonClassDeclaration);
assert_mem_size!(SingletonClassDeclaration, 224);

namespace_declaration!(Todo, TodoDeclaration);
assert_mem_size!(TodoDeclaration, 224);
simple_declaration!(ConstantDeclaration);
assert_mem_size!(ConstantDeclaration, 112);
simple_declaration!(MethodDeclaration);
Expand Down
26 changes: 18 additions & 8 deletions rust/rubydex/src/model/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,20 @@ impl Graph {
{
let declaration_id = DeclarationId::from(&fully_qualified_name);

let should_promote = self.declarations.get(&declaration_id).is_some_and(|existing| {
matches!(existing, Declaration::Constant(_))
&& matches!(
self.definitions.get(&definition_id),
Some(Definition::Class(_) | Definition::Module(_) | Definition::SingletonClass(_))
)
&& self.all_definitions_promotable(existing)
});
let is_namespace_definition = matches!(
self.definitions.get(&definition_id),
Some(Definition::Class(_) | Definition::Module(_) | Definition::SingletonClass(_))
);

let should_promote = is_namespace_definition
&& self
.declarations
.get(&declaration_id)
.is_some_and(|existing| match existing {
Declaration::Constant(_) => self.all_definitions_promotable(existing),
Declaration::Namespace(Namespace::Todo(_)) => true,
_ => false,
});

match self.declarations.entry(declaration_id) {
Entry::Occupied(mut occupied_entry) => {
Expand Down Expand Up @@ -633,6 +639,10 @@ impl Graph {
Declaration::Namespace(Namespace::SingletonClass(it)) => {
it.add_member(member_str_id, member_declaration_id);
}
Declaration::Namespace(Namespace::Todo(it)) => it.add_member(member_str_id, member_declaration_id),
Declaration::Constant(_) => {
// TODO: temporary hack to avoid crashing on `Struct.new`, `Class.new` and `Module.new`
}
_ => panic!("Tried to add member to a declaration that isn't a namespace"),
}
}
Expand Down
212 changes: 205 additions & 7 deletions rust/rubydex/src/resolution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::model::{
declaration::{
Ancestor, Ancestors, ClassDeclaration, ClassVariableDeclaration, ConstantAliasDeclaration, ConstantDeclaration,
Declaration, GlobalVariableDeclaration, InstanceVariableDeclaration, MethodDeclaration, ModuleDeclaration,
Namespace, SingletonClassDeclaration,
Namespace, SingletonClassDeclaration, TodoDeclaration,
},
definitions::{Definition, Mixin, Receiver},
graph::{CLASS_ID, Graph, MODULE_ID, OBJECT_ID},
Expand Down Expand Up @@ -1057,12 +1057,60 @@ impl<'a> Resolver<'a> {
fn name_owner_id(&mut self, name_id: NameId) -> Outcome {
let name_ref = self.graph.names().get(&name_id).unwrap();

if let Some(parent_scope) = name_ref.parent_scope().as_ref() {
if let Some(&parent_scope) = name_ref.parent_scope().as_ref() {
// If we have `A::B`, the owner of `B` is whatever `A` resolves to.
// If `A` is an alias, resolve through to get the actual namespace.
match self.resolve_constant_internal(*parent_scope) {
// On `Retry`, we don't create a Todo: the parent may still resolve through inheritance once ancestors are
// linearized. We only create Todos for `Unresolved` outcomes where the parent is genuinely unknown.
match self.resolve_constant_internal(parent_scope) {
Outcome::Resolved(id, linearization) => self.resolve_to_primary_namespace(id, linearization),
other => other,
// Retry or Unresolved(Some(_)) means we might find it later through ancestor linearization
Outcome::Retry(id) => Outcome::Retry(id),
Outcome::Unresolved(Some(id)) => Outcome::Unresolved(Some(id)),
// Only create a Todo when genuinely unresolvable (no pending linearizations)
Outcome::Unresolved(None) => {
let parent_name = self.graph.names().get(&parent_scope).unwrap();
let parent_str_id = *parent_name.str();
let parent_has_explicit_prefix = parent_name.parent_scope().as_ref().is_some();
// NLL: borrow of parent_name ends here

// For bare names (no explicit `::` prefix), always use OBJECT_ID as the owner.
// Using nesting here would create "Nesting::Bar" instead of "Bar" for a bare `Bar`
// reference, which is incorrect: if `Bar` can't be found anywhere, the placeholder
// should live at the top level so it can be promoted when `module Bar` appears later.
let parent_owner_id = if parent_has_explicit_prefix {
match self.name_owner_id(parent_scope) {
Outcome::Resolved(id, _) => id,
_ => *OBJECT_ID,
}
} else {
*OBJECT_ID
};

let fully_qualified_name = if parent_owner_id == *OBJECT_ID {
self.graph.strings().get(&parent_str_id).unwrap().to_string()
} else {
format!(
"{}::{}",
self.graph.declarations().get(&parent_owner_id).unwrap().name(),
self.graph.strings().get(&parent_str_id).unwrap().as_str()
)
};

let declaration_id = DeclarationId::from(&fully_qualified_name);

if let std::collections::hash_map::Entry::Vacant(e) =
self.graph.declarations_mut().entry(declaration_id)
{
e.insert(Declaration::Namespace(Namespace::Todo(Box::new(TodoDeclaration::new(
fully_qualified_name,
parent_owner_id,
)))));
self.graph.add_member(&parent_owner_id, declaration_id, parent_str_id);
}

Outcome::Resolved(declaration_id, None)
}
}
} else if let Some(nesting_id) = name_ref.nesting()
&& !name_ref.parent_scope().is_top_level()
Expand Down Expand Up @@ -1916,9 +1964,12 @@ mod tests {

assert_no_diagnostics!(&context);

assert_declaration_does_not_exist!(context, "Foo");
assert_declaration_does_not_exist!(context, "Foo::Bar");
assert_declaration_does_not_exist!(context, "Foo::Bar::Baz");
assert_declaration_kind_eq!(context, "Foo", "<TODO>");

assert_members_eq!(context, "Object", vec!["Foo"]);
assert_members_eq!(context, "Foo", vec!["Bar"]);
assert_members_eq!(context, "Foo::Bar", vec!["Baz"]);
assert_no_members!(context, "Foo::Bar::Baz");
}

#[test]
Expand Down Expand Up @@ -5261,4 +5312,151 @@ mod tests {
assert_declaration_does_not_exist!(context, "Foo::<Foo>");
assert_declaration_does_not_exist!(context, "Foo::<Foo>#bar()");
}

#[test]
fn resolve_missing_declaration_to_todo() {
let mut context = GraphTest::new();
context.index_uri("file:///foo.rb", {
r"
class Foo::Bar
include Foo::Baz

def bar; end
end

module Foo::Baz
def baz; end
end
"
});
context.resolve();

assert_no_diagnostics!(&context);

assert_declaration_kind_eq!(context, "Foo", "<TODO>");

assert_members_eq!(context, "Object", vec!["Foo"]);
assert_members_eq!(context, "Foo", vec!["Bar", "Baz"]);
assert_members_eq!(context, "Foo::Bar", vec!["bar()"]);
assert_members_eq!(context, "Foo::Baz", vec!["baz()"]);
}

#[test]
fn todo_declaration_promoted_to_real_namespace() {
let mut context = GraphTest::new();
context.index_uri("file:///foo.rb", {
r"
class Foo::Bar
def bar; end
end

class Foo
def foo; end
end
"
});
context.resolve();

assert_no_diagnostics!(&context);

// Foo was initially created as a Todo (from class Foo::Bar), then promoted to Class
assert_declaration_kind_eq!(context, "Foo", "Class");

assert_members_eq!(context, "Object", vec!["Foo"]);
assert_members_eq!(context, "Foo", vec!["Bar", "foo()"]);
assert_members_eq!(context, "Foo::Bar", vec!["bar()"]);
}

#[test]
fn todo_declaration_promoted_to_real_namespace_incrementally() {
let mut context = GraphTest::new();
context.index_uri("file:///bar.rb", {
r"
class Foo::Bar
def bar; end
end
"
});
context.resolve();

assert_no_diagnostics!(&context);
assert_declaration_kind_eq!(context, "Foo", "<TODO>");

context.index_uri("file:///foo.rb", {
r"
class Foo
def foo; end
end
"
});
context.resolve();

assert_no_diagnostics!(&context);

// Foo was promoted from Todo to Class after the second resolution
assert_declaration_kind_eq!(context, "Foo", "Class");

assert_members_eq!(context, "Object", vec!["Foo"]);
assert_members_eq!(context, "Foo", vec!["Bar", "foo()"]);
assert_members_eq!(context, "Foo::Bar", vec!["bar()"]);
}

#[test]
fn qualified_name_inside_nesting_resolves_to_top_level() {
let mut context = GraphTest::new();
context.index_uri("file:///foo.rb", {
r"
module Foo
class Bar::Baz
def qux; end
end
end

module Bar
end
"
});
context.resolve();

assert_no_diagnostics!(&context);
assert_declaration_kind_eq!(context, "Bar", "Module");
assert_members_eq!(context, "Bar", vec!["Baz"]);
assert_declaration_exists!(context, "Bar::Baz");
assert_members_eq!(context, "Bar::Baz", vec!["qux()"]);
assert_declaration_does_not_exist!(context, "Foo::Bar");
}

#[test]
fn qualified_name_inside_nesting_resolves_when_discovered_incrementally() {
let mut context = GraphTest::new();
context.index_uri("file:///baz.rb", {
r"
module Foo
class Bar::Baz
def qux; end
end
end
"
});
context.resolve();

// Bar is unknown — a Todo is created at the top level, not "Foo::Bar"
assert_declaration_kind_eq!(context, "Bar", "<TODO>");
assert_declaration_does_not_exist!(context, "Foo::Bar");

context.index_uri("file:///bar.rb", {
r"
module Bar
end
"
});
context.resolve();

// After discovering top-level Bar, the Todo should be promoted and Baz re-homed.
assert_no_diagnostics!(&context);
assert_declaration_kind_eq!(context, "Bar", "Module");
assert_members_eq!(context, "Bar", vec!["Baz"]);
assert_members_eq!(context, "Bar::Baz", vec!["qux()"]);
assert_declaration_does_not_exist!(context, "Foo::Bar");
}
}
Loading