Skip to content
Open
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
137 changes: 132 additions & 5 deletions rust/rubydex/src/indexing/rbs_indexer.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
//! Visit the RBS AST and create type definitions.

use ruby_rbs::node::{
self, ClassNode, CommentNode, ConstantNode, ExtendNode, GlobalNode, IncludeNode, ModuleNode, Node, NodeList,
PrependNode, TypeNameNode, Visit,
self, AliasKind, ClassNode, CommentNode, ConstantNode, ExtendNode, GlobalNode, IncludeNode, ModuleNode, Node,
NodeList, PrependNode, TypeNameNode, Visit,
};

use crate::diagnostic::Rule;
use crate::indexing::local_graph::LocalGraph;
use crate::model::comment::Comment;
use crate::model::definitions::{
ClassDefinition, ConstantDefinition, Definition, DefinitionFlags, ExtendDefinition, GlobalVariableDefinition,
IncludeDefinition, Mixin, ModuleDefinition, PrependDefinition,
IncludeDefinition, MethodAliasDefinition, Mixin, ModuleDefinition, PrependDefinition, Receiver,
};
use crate::model::document::Document;
use crate::model::ids::{DefinitionId, NameId, ReferenceId, UriId};
Expand Down Expand Up @@ -324,18 +324,50 @@ impl Visit for RBSIndexer<'_> {
Mixin::Extend(ExtendDefinition::new(ref_id))
});
}

fn visit_alias_node(&mut self, alias_node: &node::AliasNode) {
let lexical_nesting_id = self.parent_lexical_scope_id();

let receiver = match alias_node.kind() {
AliasKind::Instance => None,
AliasKind::Singleton => lexical_nesting_id.map(Receiver::SelfReceiver),
};

let new_name = Self::bytes_to_string(alias_node.new_name().name());
let old_name = Self::bytes_to_string(alias_node.old_name().name());

let new_name_str_id = self.local_graph.intern_string(format!("{new_name}()"));
let old_name_str_id = self.local_graph.intern_string(format!("{old_name}()"));

let offset = Offset::from_rbs_location(&alias_node.location());
let comments = Self::collect_comments(alias_node.comment());

let definition = Definition::MethodAlias(Box::new(MethodAliasDefinition::new(
new_name_str_id,
old_name_str_id,
self.uri_id,
offset,
comments,
Self::flags(&alias_node.annotations()),
lexical_nesting_id,
receiver,
)));

self.register_definition(definition, lexical_nesting_id);
}
}

#[cfg(test)]
mod tests {
use ruby_rbs::node::{self, Node, NodeList};

use crate::indexing::rbs_indexer::RBSIndexer;
use crate::model::definitions::DefinitionFlags;
use crate::model::definitions::{Definition, DefinitionFlags};
use crate::test_utils::LocalGraphTest;
use crate::{
assert_def_comments_eq, assert_def_mixins_eq, assert_def_name_eq, assert_def_name_offset_eq, assert_def_str_eq,
assert_def_superclass_ref_eq, assert_definition_at, assert_local_diagnostics_eq, assert_no_local_diagnostics,
assert_def_superclass_ref_eq, assert_definition_at, assert_local_diagnostics_eq, assert_method_has_receiver,
assert_no_local_diagnostics, assert_string_eq,
};

fn index_source(source: &str) -> LocalGraphTest {
Expand Down Expand Up @@ -714,4 +746,99 @@ mod tests {
assert!(def.flags().contains(DefinitionFlags::DEPRECATED));
});
}

#[test]
fn index_alias_node() {
let context = index_source({
"
class Foo
# Some documentation
alias bar baz
alias qux quux
end
"
});

assert_no_local_diagnostics!(&context);

assert_definition_at!(&context, "1:1-5:4", Class, |class_def| {
assert_eq!(2, class_def.members().len());

assert_definition_at!(&context, "3:3-3:16", MethodAlias, |def| {
assert_string_eq!(&context, def.new_name_str_id(), "bar()");
assert_string_eq!(&context, def.old_name_str_id(), "baz()");
assert_def_comments_eq!(&context, def, ["Some documentation\n"]);
assert_eq!(class_def.id(), def.lexical_nesting_id().unwrap());
});

assert_definition_at!(&context, "4:3-4:17", MethodAlias, |def| {
assert_string_eq!(&context, def.new_name_str_id(), "qux()");
assert_string_eq!(&context, def.old_name_str_id(), "quux()");
});
});
}

#[test]
fn index_alias_node_with_deprecation() {
let context = index_source({
"
class Foo
%a{deprecated}
alias bar baz
end
"
});

assert_no_local_diagnostics!(&context);

assert_definition_at!(&context, "3:3-3:16", MethodAlias, |def| {
assert!(def.flags().contains(DefinitionFlags::DEPRECATED));
});
}

#[test]
fn index_alias_node_singleton() {
let context = index_source({
"
class Foo
alias self.bar self.baz
end
"
});

assert_no_local_diagnostics!(&context);
assert_eq!(context.graph().definitions().len(), 2);

assert_definition_at!(&context, "2:3-2:26", MethodAlias, |def| {
assert_string_eq!(&context, def.new_name_str_id(), "bar()");
assert_string_eq!(&context, def.old_name_str_id(), "baz()");
assert_method_has_receiver!(&context, def, "Foo");
});
}

#[test]
fn mixed_singleton_instance_alias_is_not_indexed() {
// Mixed aliases (`alias self.x y` and `alias x self.y`) are not valid RBS.
// Verify that no alias definitions are produced for these inputs.
for source in [
"
class Foo
alias self.bar baz
end
",
"
class Foo
alias bar self.baz
end
",
] {
let context = index_source(source);
let has_alias = context
.graph()
.definitions()
.values()
.any(|d| matches!(d, Definition::MethodAlias(_)));
assert!(!has_alias, "Expected no alias definitions for: {source}");
}
}
}
35 changes: 2 additions & 33 deletions rust/rubydex/src/indexing/ruby_indexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2068,8 +2068,8 @@ impl Visit<'_> for RubyIndexer<'_> {
mod tests {
use crate::{
assert_def_comments_eq, assert_def_mixins_eq, assert_def_name_eq, assert_def_name_offset_eq, assert_def_str_eq,
assert_def_superclass_ref_eq, assert_definition_at, assert_local_diagnostics_eq, assert_name_path_eq,
assert_no_local_diagnostics, assert_string_eq,
assert_def_superclass_ref_eq, assert_definition_at, assert_local_diagnostics_eq, assert_method_has_receiver,
assert_name_path_eq, assert_no_local_diagnostics, assert_string_eq,
model::{
definitions::{Definition, Parameter, Receiver},
ids::{StringId, UriId},
Expand All @@ -2080,37 +2080,6 @@ mod tests {

// Method assertions

/// Asserts that a method has the expected receiver.
///
/// Usage:
/// - `assert_method_has_receiver!(ctx, method, "Foo")`
/// - `assert_method_has_receiver!(ctx, method, "<Bar>")`
macro_rules! assert_method_has_receiver {
($context:expr, $method:expr, $expected_receiver:expr) => {{
let name_id = match $method.receiver() {
Some(Receiver::SelfReceiver(def_id)) => {
let def = $context.graph().definitions().get(def_id).unwrap();
*def.name_id().expect("SelfReceiver definition should have a name_id")
}
Some(Receiver::ConstantReceiver(name_id)) => *name_id,
None => {
panic!(
"Method receiver mismatch: expected `{}`, got `None`",
$expected_receiver
);
}
};

let name = $context.graph().names().get(&name_id).unwrap();
let actual_name = $context.graph().strings().get(name.str()).unwrap().as_str();
assert_eq!(
$expected_receiver, actual_name,
"method receiver mismatch: expected `{}`, got `{}`",
$expected_receiver, actual_name
);
}};
}

/// Asserts that a parameter matches the expected kind.
///
/// Usage:
Expand Down
2 changes: 1 addition & 1 deletion rust/rubydex/src/model/definitions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -724,7 +724,7 @@ pub struct MethodDefinition {
assert_mem_size!(MethodDefinition, 112);

/// The receiver of a singleton method definition.
#[derive(Debug)]
#[derive(Debug, Clone, Copy)]
pub enum Receiver {
/// `def self.foo` - receiver is the enclosing definition (class, module, singleton class or DSL)
SelfReceiver(DefinitionId),
Expand Down
62 changes: 54 additions & 8 deletions rust/rubydex/src/resolution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -511,26 +511,36 @@ impl<'a> Resolver<'a> {
Definition::MethodAlias(alias) => {
// Method aliases operate on instance methods. The SelfReceiver arm is for
// RBS `alias self.x self.y`.
let owner_id = match alias.receiver() {
Some(Receiver::SelfReceiver(def_id)) => *self
.graph
.definition_id_to_declaration_id(*def_id)
.expect("SelfReceiver definition should have a declaration"),
Some(Receiver::ConstantReceiver(name_id)) => match self.graph.names().get(name_id).unwrap() {
let receiver = *alias.receiver();
let new_name_str_id = *alias.new_name_str_id();
let lexical_nesting_id = *alias.lexical_nesting_id();

let owner_id = match receiver {
Some(Receiver::SelfReceiver(def_id)) => {
let decl_id = *self
.graph
.definition_id_to_declaration_id(def_id)
.expect("SelfReceiver definition should have a declaration");
let Some(owner_id) = self.get_or_create_singleton_class(decl_id) else {
continue;
};
owner_id
}
Some(Receiver::ConstantReceiver(name_id)) => match self.graph.names().get(&name_id).unwrap() {
NameRef::Resolved(resolved) => *resolved.declaration_id(),
NameRef::Unresolved(_) => {
continue;
}
},
None => {
let Some(resolved) = self.resolve_lexical_owner(*alias.lexical_nesting_id()) else {
let Some(resolved) = self.resolve_lexical_owner(lexical_nesting_id) else {
continue;
};
resolved
}
};

self.create_declaration(*alias.new_name_str_id(), id, owner_id, |name| {
self.create_declaration(new_name_str_id, id, owner_id, |name| {
Declaration::Method(Box::new(MethodDeclaration::new(name, owner_id)))
});
}
Expand Down Expand Up @@ -4868,6 +4878,42 @@ mod tests {
assert_ancestors_eq!(context, "Foo", ["Foo", "Baz", "Bar", "Object"]);
}

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

module Baz
def original; end
end
"
});
context.index_rbs_uri("file:///test.rbs", {
r"
class Foo
alias qux bar
alias self.class_alias self.class_method
end

module Baz
alias copy original
end
"
});
context.resolve();

assert_no_diagnostics!(&context);

assert_members_eq!(context, "Foo", ["bar()", "qux()"]);
assert_members_eq!(context, "Foo::<Foo>", ["class_alias()", "class_method()"]);
assert_members_eq!(context, "Baz", ["copy()", "original()"]);
}

#[test]
fn resolving_meta_programming_class_reopened() {
// It's often not possible to provide first-class support to meta-programming constructs, but we have to prevent
Expand Down
35 changes: 35 additions & 0 deletions rust/rubydex/src/test_utils/local_graph_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,41 @@ macro_rules! assert_dependents {
}};
}

// Receiver assertions

/// Asserts that a method has the expected receiver.
///
/// Usage:
/// - `assert_method_has_receiver!(ctx, method, "Foo")`
/// - `assert_method_has_receiver!(ctx, method, "<Bar>")`
#[cfg(test)]
#[macro_export]
macro_rules! assert_method_has_receiver {
($context:expr, $method:expr, $expected_receiver:expr) => {{
let name_id = match $method.receiver() {
Some($crate::model::definitions::Receiver::SelfReceiver(def_id)) => {
let def = $context.graph().definitions().get(def_id).unwrap();
*def.name_id().expect("SelfReceiver definition should have a name_id")
}
Some($crate::model::definitions::Receiver::ConstantReceiver(name_id)) => *name_id,
None => {
panic!(
"Method receiver mismatch: expected `{}`, got `None`",
$expected_receiver
);
}
};

let name = $context.graph().names().get(&name_id).unwrap();
let actual_name = $context.graph().strings().get(name.str()).unwrap().as_str();
assert_eq!(
$expected_receiver, actual_name,
"method receiver mismatch: expected `{}`, got `{}`",
$expected_receiver, actual_name
);
}};
}

// Diagnostic assertions

#[cfg(test)]
Expand Down
Loading