diff --git a/rust/rubydex/src/indexing/ruby_indexer.rs b/rust/rubydex/src/indexing/ruby_indexer.rs index a4e16723..ef86d451 100644 --- a/rust/rubydex/src/indexing/ruby_indexer.rs +++ b/rust/rubydex/src/indexing/ruby_indexer.rs @@ -361,17 +361,12 @@ impl<'a> RubyIndexer<'a> { }) } - // Runs the given closure if the given call `node` is invoked directly on `self` for each one of its string or - // symbol arguments + // Runs the given closure for each string or symbol argument of a call node. fn each_string_or_symbol_arg(node: &ruby_prism::CallNode, mut f: F) where F: FnMut(String, ruby_prism::Location), { - let receiver = node.receiver(); - - if (receiver.is_none() || receiver.unwrap().as_self_node().is_some()) - && let Some(arguments) = node.arguments() - { + if let Some(arguments) = node.arguments() { for argument in &arguments.arguments() { match argument { ruby_prism::Node::SymbolNode { .. } => { @@ -1549,6 +1544,11 @@ impl Visit<'_> for RubyIndexer<'_> { } let mut index_attr = |kind: AttrKind, call: &ruby_prism::CallNode| { + let receiver = call.receiver(); + if receiver.is_some() && receiver.unwrap().as_self_node().is_none() { + return; + } + let call_offset = Offset::from_prism_location(&call.location()); Self::each_string_or_symbol_arg(call, |name, location| { @@ -1645,6 +1645,21 @@ impl Visit<'_> for RubyIndexer<'_> { } } "alias_method" => { + let recv_node = node.receiver(); + let recv_ref = recv_node.as_ref(); + if recv_ref.is_some_and(|recv| { + !matches!( + recv, + ruby_prism::Node::SelfNode { .. } + | ruby_prism::Node::ConstantReadNode { .. } + | ruby_prism::Node::ConstantPathNode { .. } + ) + }) { + // TODO: Add a diagnostic for dynamic receivers + self.visit_call_node_parts(node); + return; + } + let mut names: Vec<(String, Offset)> = Vec::new(); Self::each_string_or_symbol_arg(node, |name, location| { @@ -1662,7 +1677,15 @@ impl Visit<'_> for RubyIndexer<'_> { 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 method_receiver = self.method_receiver(node.receiver().as_ref(), node.location()); + let (receiver, method_receiver) = match recv_ref { + Some( + recv @ (ruby_prism::Node::ConstantPathNode { .. } | ruby_prism::Node::ConstantReadNode { .. }), + ) => { + let name_id = self.index_constant_reference(recv, true); + (name_id.map(Receiver::ConstantReceiver), name_id) + } + _ => (None, self.method_receiver(recv_ref, node.location())), + }; let reference = MethodRef::new(old_name_str_id, self.uri_id, old_offset.clone(), method_receiver); self.local_graph.add_method_reference(reference); @@ -1677,6 +1700,7 @@ impl Visit<'_> for RubyIndexer<'_> { comments, flags, self.current_nesting_definition_id(), + receiver, ))); let definition_id = self.local_graph.add_definition(definition); @@ -1987,6 +2011,7 @@ impl Visit<'_> for RubyIndexer<'_> { comments, flags, self.current_nesting_definition_id(), + None, ))); let definition_id = self.local_graph.add_definition(definition); @@ -4553,6 +4578,7 @@ mod tests { assert_definition_at!(&context, "1:1-5:4", Class, |foo| { assert_definition_at!(&context, "3:5-3:24", MethodAlias, |alias_method| { + assert!(alias_method.receiver().is_none()); assert_eq!(foo.id(), alias_method.lexical_nesting_id().unwrap()); }); }); @@ -4940,7 +4966,7 @@ mod tests { let old_name = context.graph().strings().get(def.old_name_str_id()).unwrap(); assert_eq!(new_name.as_str(), "foo()"); assert_eq!(old_name.as_str(), "bar()"); - + assert!(def.receiver().is_none()); assert_eq!(foo_class_def.id(), def.lexical_nesting_id().unwrap()); }); @@ -4949,7 +4975,7 @@ mod tests { let old_name = context.graph().strings().get(def.old_name_str_id()).unwrap(); assert_eq!(new_name.as_str(), "baz()"); assert_eq!(old_name.as_str(), "qux()"); - + assert!(def.receiver().is_none()); assert_eq!(foo_class_def.id(), def.lexical_nesting_id().unwrap()); }); }); @@ -4971,7 +4997,7 @@ mod tests { let old_name = context.graph().strings().get(def.old_name_str_id()).unwrap(); assert_eq!(new_name.as_str(), "foo()"); assert_eq!(old_name.as_str(), "bar()"); - + assert!(def.receiver().is_none()); assert!(def.lexical_nesting_id().is_none()); }); @@ -5009,7 +5035,7 @@ mod tests { let old_name = context.graph().strings().get(def.old_name_str_id()).unwrap(); assert_eq!(new_name.as_str(), "foo_symbol()"); assert_eq!(old_name.as_str(), "bar_symbol()"); - + assert!(def.receiver().is_none()); assert!(def.lexical_nesting_id().is_none()); }); @@ -5018,7 +5044,7 @@ mod tests { let old_name = context.graph().strings().get(def.old_name_str_id()).unwrap(); assert_eq!(new_name.as_str(), "foo_string()"); assert_eq!(old_name.as_str(), "bar_string()"); - + assert!(def.receiver().is_none()); assert!(def.lexical_nesting_id().is_none()); }); @@ -5028,12 +5054,146 @@ mod tests { let old_name = context.graph().strings().get(def.old_name_str_id()).unwrap(); assert_eq!(new_name.as_str(), "baz()"); assert_eq!(old_name.as_str(), "qux()"); - + assert!(def.receiver().is_none()); assert_eq!(foo_class_def.id(), def.lexical_nesting_id().unwrap()); }); }); } + #[test] + fn index_alias_method_with_self_receiver_maps_to_none() { + let context = index_source({ + " + class Foo + self.alias_method :bar, :baz + end + " + }); + + assert_no_local_diagnostics!(&context); + + assert_definition_at!(&context, "2:3-2:31", MethodAlias, |def| { + assert!(def.receiver().is_none()); + }); + } + + #[test] + fn index_alias_method_with_constant_receiver() { + let context = index_source({ + " + class Foo; end + Foo.alias_method :bar, :baz + " + }); + + assert_no_local_diagnostics!(&context); + + assert_definition_at!(&context, "2:1-2:28", 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 index_alias_method_in_singleton_class_has_no_receiver() { + let context = index_source({ + " + class Foo + def self.find; end + + class << self + alias_method :find_old, :find + end + end + " + }); + + assert_no_local_diagnostics!(&context); + + assert_definition_at!(&context, "1:1-7:4", Class, |_foo| { + assert_definition_at!(&context, "4:3-6:6", SingletonClass, |singleton| { + assert_definition_at!(&context, "5:5-5:34", MethodAlias, |def| { + assert_string_eq!(&context, def.new_name_str_id(), "find_old()"); + assert_string_eq!(&context, def.old_name_str_id(), "find()"); + assert!(def.receiver().is_none()); + assert_eq!(singleton.id(), def.lexical_nesting_id().unwrap()); + }); + }); + }); + } + + #[test] + fn index_alias_keyword_in_singleton_class_has_no_receiver() { + // Same as above: `alias` inside `class << self` has no receiver. + let context = index_source({ + " + class Foo + def self.find; end + + class << self + alias find_old find + end + end + " + }); + + assert_no_local_diagnostics!(&context); + + assert_definition_at!(&context, "4:3-6:6", SingletonClass, |singleton| { + assert_definition_at!(&context, "5:5-5:24", MethodAlias, |def| { + assert!(def.receiver().is_none()); + assert_eq!(singleton.id(), def.lexical_nesting_id().unwrap()); + }); + }); + } + + #[test] + fn index_alias_method_with_nested_constant_receiver() { + let context = index_source({ + " + module A + class B + def original; end + end + end + + A::B.alias_method :new_name, :original + " + }); + + assert_no_local_diagnostics!(&context); + + assert_definition_at!(&context, "7:1-7:39", MethodAlias, |def| { + assert_string_eq!(&context, def.new_name_str_id(), "new_name()"); + assert_method_has_receiver!(&context, def, "B"); + assert!(def.lexical_nesting_id().is_none()); + }); + } + + #[test] + fn index_alias_method_with_dynamic_receiver_not_indexed() { + let context = index_source({ + " + class Foo + def original; end + end + + foo.alias_method :new_name, :original + " + }); + + assert_no_local_diagnostics!(&context); + + let alias_count = context + .graph() + .definitions() + .values() + .filter(|def| matches!(def, Definition::MethodAlias(_))) + .count(); + assert_eq!(0, alias_count); + } + #[test] fn index_alias_global_variables() { let context = index_source({ diff --git a/rust/rubydex/src/model/definitions.rs b/rust/rubydex/src/model/definitions.rs index d91ba3ab..9d657e45 100644 --- a/rust/rubydex/src/model/definitions.rs +++ b/rust/rubydex/src/model/definitions.rs @@ -1361,10 +1361,12 @@ pub struct MethodAliasDefinition { flags: DefinitionFlags, comments: Box<[Comment]>, lexical_nesting_id: Option, + receiver: Option, } -assert_mem_size!(MethodAliasDefinition, 72); +assert_mem_size!(MethodAliasDefinition, 88); impl MethodAliasDefinition { + #[allow(clippy::too_many_arguments)] #[must_use] pub const fn new( new_name_str_id: StringId, @@ -1374,6 +1376,7 @@ impl MethodAliasDefinition { comments: Box<[Comment]>, flags: DefinitionFlags, lexical_nesting_id: Option, + receiver: Option, ) -> Self { Self { new_name_str_id, @@ -1383,6 +1386,7 @@ impl MethodAliasDefinition { flags, comments, lexical_nesting_id, + receiver, } } @@ -1427,6 +1431,11 @@ impl MethodAliasDefinition { &self.lexical_nesting_id } + #[must_use] + pub fn receiver(&self) -> &Option { + &self.receiver + } + #[must_use] pub fn flags(&self) -> &DefinitionFlags { &self.flags diff --git a/rust/rubydex/src/resolution.rs b/rust/rubydex/src/resolution.rs index 2c914f41..266779a3 100644 --- a/rust/rubydex/src/resolution.rs +++ b/rust/rubydex/src/resolution.rs @@ -509,8 +509,25 @@ impl<'a> Resolver<'a> { } } Definition::MethodAlias(alias) => { - let Some(owner_id) = self.resolve_lexical_owner(*alias.lexical_nesting_id()) else { - continue; + // 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() { + NameRef::Resolved(resolved) => *resolved.declaration_id(), + NameRef::Unresolved(_) => { + continue; + } + }, + None => { + let Some(resolved) = self.resolve_lexical_owner(*alias.lexical_nesting_id()) else { + continue; + }; + resolved + } }; self.create_declaration(*alias.new_name_str_id(), id, owner_id, |name| { @@ -2540,6 +2557,99 @@ mod tests { assert_members_eq!(context, "Foo", ["bar()", "foo()"]); } + #[test] + fn resolving_method_alias_with_self_receiver() { + // SelfReceiver resolves to instance methods (the class directly), not the singleton + let mut context = GraphTest::new(); + context.index_uri("file:///foo.rb", { + r" + class Foo + def original; end + self.alias_method :aliased, :original + end + " + }); + context.resolve(); + + assert_no_diagnostics!(&context); + + assert_members_eq!(context, "Foo", ["aliased()", "original()"]); + } + + #[test] + fn resolving_alias_method_in_singleton_class_lands_on_singleton() { + // `class << self; alias_method ...; end` — alias lands on singleton via lexical nesting + let mut context = GraphTest::new(); + context.index_uri("file:///foo.rb", { + r" + class Foo + def self.find; end + + class << self + alias_method :find_old, :find + end + end + " + }); + context.resolve(); + + assert_no_diagnostics!(&context); + + assert_members_eq!(context, "Foo::", ["find()", "find_old()"]); + } + + #[test] + fn resolving_self_alias_method_is_equivalent_to_bare_alias_method() { + // `self.alias_method` and bare `alias_method` resolve identically (instance methods) + let mut context = GraphTest::new(); + context.index_uri("file:///with_self.rb", { + r" + class WithSelf + def original; end + self.alias_method :aliased, :original + end + " + }); + context.index_uri("file:///without_self.rb", { + r" + class WithoutSelf + def original; end + alias_method :aliased, :original + end + " + }); + context.resolve(); + + assert_no_diagnostics!(&context); + + // Both resolve identically: alias lands on instance methods + assert_members_eq!(context, "WithSelf", ["aliased()", "original()"]); + assert_members_eq!(context, "WithoutSelf", ["aliased()", "original()"]); + } + + #[test] + fn resolving_method_alias_with_constant_receiver() { + let mut context = GraphTest::new(); + context.index_uri("file:///foo.rb", { + r" + class Bar + def to_s; end + end + + class Foo + Bar.alias_method(:new_to_s, :to_s) + end + " + }); + context.resolve(); + + assert_no_diagnostics!(&context); + + // Bar.alias_method places the alias on Bar's instance methods + assert_no_members!(context, "Foo"); + assert_members_eq!(context, "Bar", ["new_to_s()", "to_s()"]); + } + #[test] fn resolving_global_variable_alias() { let mut context = GraphTest::new();