Skip to content

Commit a05a09b

Browse files
committed
Implement MethodAliasDefinition#signatures
1 parent 26dff63 commit a05a09b

4 files changed

Lines changed: 360 additions & 2 deletions

File tree

ext/rubydex/definition.c

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,18 @@ static VALUE rdxr_method_definition_signatures(VALUE self) {
177177
return rdxi_signatures_to_ruby(arr, data->graph_obj, self);
178178
}
179179

180+
// MethodAliasDefinition#signatures -> [Rubydex::Signature]
181+
static VALUE rdxr_method_alias_definition_signatures(VALUE self) {
182+
HandleData *data;
183+
TypedData_Get_Struct(self, HandleData, &handle_type, data);
184+
185+
void *graph;
186+
TypedData_Get_Struct(data->graph_obj, void *, &graph_type, graph);
187+
188+
SignatureArray *arr = rdx_method_alias_definition_signatures(graph, data->id);
189+
return rdxi_signatures_to_ruby(arr, data->graph_obj, Qnil);
190+
}
191+
180192
void rdxi_initialize_definition(VALUE mod) {
181193
mRubydex = mod;
182194

@@ -207,5 +219,6 @@ void rdxi_initialize_definition(VALUE mod) {
207219
cInstanceVariableDefinition = rb_define_class_under(mRubydex, "InstanceVariableDefinition", cDefinition);
208220
cClassVariableDefinition = rb_define_class_under(mRubydex, "ClassVariableDefinition", cDefinition);
209221
cMethodAliasDefinition = rb_define_class_under(mRubydex, "MethodAliasDefinition", cDefinition);
222+
rb_define_method(cMethodAliasDefinition, "signatures", rdxr_method_alias_definition_signatures, 0);
210223
cGlobalVariableAliasDefinition = rb_define_class_under(mRubydex, "GlobalVariableAliasDefinition", cDefinition);
211224
}

rust/rubydex-sys/src/definition_api.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,66 @@ fn collect_method_signatures(
473473
}
474474
}
475475

476+
/// Returns signatures for a `MethodAliasDefinition` by following the alias chain.
477+
/// Returns NULL if the definition is not a method alias or the chain cannot be resolved.
478+
///
479+
/// # Safety
480+
/// - `pointer` must be a valid pointer previously returned by `rdx_graph_new`.
481+
/// - `definition_id` must be a valid definition id.
482+
#[unsafe(no_mangle)]
483+
pub unsafe extern "C" fn rdx_method_alias_definition_signatures(
484+
pointer: GraphPointer,
485+
definition_id: u64,
486+
) -> *mut SignatureArray {
487+
with_graph(pointer, |graph| {
488+
let def_id = DefinitionId::new(definition_id);
489+
let Some(Definition::MethodAlias(alias)) = graph.definitions().get(&def_id) else {
490+
return ptr::null_mut();
491+
};
492+
493+
// Follow the alias chain until we find method definitions
494+
let mut current_results = rubydex::query::dealias_method(graph, alias);
495+
let mut visited = std::collections::HashSet::new();
496+
visited.insert(def_id);
497+
498+
loop {
499+
let mut sig_entries: Vec<SignatureEntry> = Vec::new();
500+
let mut next_aliases = Vec::new();
501+
502+
for result in &current_results {
503+
match result {
504+
rubydex::query::DealiasMethodResult::Method(id) => {
505+
if let Some(Definition::Method(method_def)) = graph.definitions().get(id) {
506+
collect_method_signatures(graph, method_def, id.get(), &mut sig_entries);
507+
}
508+
}
509+
rubydex::query::DealiasMethodResult::Alias(id) => {
510+
if visited.insert(*id)
511+
&& let Some(Definition::MethodAlias(next_alias)) = graph.definitions().get(id)
512+
{
513+
next_aliases.extend(rubydex::query::dealias_method(graph, next_alias));
514+
}
515+
}
516+
}
517+
}
518+
519+
if !sig_entries.is_empty() {
520+
let mut boxed = sig_entries.into_boxed_slice();
521+
let len = boxed.len();
522+
let items_ptr = boxed.as_mut_ptr();
523+
std::mem::forget(boxed);
524+
return Box::into_raw(Box::new(SignatureArray { items: items_ptr, len }));
525+
}
526+
527+
if next_aliases.is_empty() {
528+
return ptr::null_mut();
529+
}
530+
531+
current_results = next_aliases;
532+
}
533+
})
534+
}
535+
476536
/// Frees a `SignatureArray` previously returned by `rdx_definition_signatures`.
477537
///
478538
/// # Safety

rust/rubydex/src/query.rs

Lines changed: 212 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ use std::thread;
66
use url::Url;
77

88
use crate::model::declaration::{Ancestor, Declaration};
9-
use crate::model::definitions::{Definition, Parameter};
9+
use crate::model::definitions::{Definition, MethodAliasDefinition, Parameter, Receiver};
1010
use crate::model::graph::{Graph, OBJECT_ID};
1111
use crate::model::identity_maps::IdentityHashSet;
12-
use crate::model::ids::{DeclarationId, NameId, StringId, UriId};
12+
use crate::model::ids::{DeclarationId, DefinitionId, NameId, StringId, UriId};
1313
use crate::model::keywords::{self, Keyword};
1414
use crate::model::name::NameRef;
1515

@@ -458,6 +458,99 @@ fn method_argument_completion<'a>(
458458
Ok(candidates)
459459
}
460460

461+
/// Result of looking up the enclosing namespace for a definition.
462+
#[allow(dead_code)]
463+
enum EnclosingNamespace {
464+
/// Found a namespace (class, module, or singleton class).
465+
Found(DeclarationId),
466+
/// The definition has no enclosing namespace (top-level).
467+
NotFound,
468+
/// The enclosing namespace could not be determined because name resolution failed.
469+
Unresolved(DefinitionId),
470+
}
471+
472+
/// Walks up the lexical nesting chain from the given `DefinitionId` to find
473+
/// the nearest enclosing namespace (class, module, or singleton class).
474+
fn enclosing_namespace(graph: &Graph, starting_id: Option<&DefinitionId>) -> EnclosingNamespace {
475+
let mut current = starting_id;
476+
while let Some(id) = current {
477+
let def = graph.definitions().get(id).unwrap();
478+
let is_namespace_def = matches!(
479+
def,
480+
Definition::Class(_) | Definition::Module(_) | Definition::SingletonClass(_)
481+
);
482+
483+
let Some(decl_id) = graph.definition_id_to_declaration_id(*id) else {
484+
if is_namespace_def {
485+
return EnclosingNamespace::Unresolved(*id);
486+
}
487+
current = def.lexical_nesting_id().as_ref();
488+
continue;
489+
};
490+
if is_namespace_def {
491+
return EnclosingNamespace::Found(*decl_id);
492+
}
493+
current = def.lexical_nesting_id().as_ref();
494+
}
495+
EnclosingNamespace::NotFound
496+
}
497+
498+
/// Result of dealiasing a single level of method alias.
499+
pub enum DealiasMethodResult {
500+
/// The alias target is a concrete method definition.
501+
Method(DefinitionId),
502+
/// The alias target is another alias.
503+
Alias(DefinitionId),
504+
}
505+
506+
/// Dereferences a `MethodAliasDefinition` by one level, returning the definitions
507+
/// found under the aliased name. Returns an empty vector if the alias target cannot
508+
/// be found (e.g., unresolved constant receiver or missing member).
509+
///
510+
/// # Panics
511+
///
512+
/// Panics if a `SelfReceiver` definition cannot be resolved to a namespace with a singleton class.
513+
#[must_use]
514+
pub fn dealias_method(graph: &Graph, alias: &MethodAliasDefinition) -> Vec<DealiasMethodResult> {
515+
let owner_id = match alias.receiver() {
516+
Some(Receiver::SelfReceiver(def_id)) => {
517+
let decl_id = graph.definition_id_to_declaration_id(*def_id).unwrap();
518+
let decl = graph.declarations().get(decl_id).unwrap();
519+
let ns = decl.as_namespace().unwrap();
520+
*ns.singleton_class().unwrap()
521+
}
522+
Some(Receiver::ConstantReceiver(name_id)) => {
523+
let Some(&id) = graph.name_id_to_declaration_id(*name_id) else {
524+
return vec![];
525+
};
526+
id
527+
}
528+
None => match enclosing_namespace(graph, alias.lexical_nesting_id().as_ref()) {
529+
EnclosingNamespace::Found(id) => id,
530+
EnclosingNamespace::NotFound | EnclosingNamespace::Unresolved(_) => return vec![],
531+
},
532+
};
533+
534+
let owner_decl = graph.declarations().get(&owner_id).unwrap();
535+
let ns = owner_decl.as_namespace().unwrap();
536+
537+
let Some(&method_decl_id) = ns.member(alias.old_name_str_id()) else {
538+
return vec![];
539+
};
540+
let method_decl = graph.declarations().get(&method_decl_id).unwrap();
541+
assert!(matches!(method_decl, Declaration::Method(_)));
542+
543+
method_decl
544+
.definitions()
545+
.iter()
546+
.filter_map(|def_id| match graph.definitions().get(def_id) {
547+
Some(Definition::Method(_)) => Some(DealiasMethodResult::Method(*def_id)),
548+
Some(Definition::MethodAlias(_)) => Some(DealiasMethodResult::Alias(*def_id)),
549+
_ => None,
550+
})
551+
.collect()
552+
}
553+
461554
#[cfg(test)]
462555
mod tests {
463556
use std::str::FromStr;
@@ -1683,6 +1776,123 @@ mod tests {
16831776
assert!(!candidates.iter().any(|c| matches!(c, CompletionCandidate::Keyword(_))));
16841777
}
16851778

1779+
fn get_method_alias_def<'a>(graph: &'a Graph, decl_name: &str) -> &'a MethodAliasDefinition {
1780+
let decl = graph.declarations().get(&DeclarationId::from(decl_name)).unwrap();
1781+
for def_id in decl.definitions() {
1782+
if let Some(Definition::MethodAlias(alias)) = graph.definitions().get(def_id) {
1783+
return alias;
1784+
}
1785+
}
1786+
panic!("No MethodAliasDefinition found for {decl_name}");
1787+
}
1788+
1789+
#[test]
1790+
fn dealias_method_basic() {
1791+
let mut context = GraphTest::new();
1792+
context.index_uri(
1793+
"file:///foo.rb",
1794+
"
1795+
class Foo
1796+
def foo(a, b); end
1797+
alias bar foo
1798+
end
1799+
",
1800+
);
1801+
context.resolve();
1802+
1803+
let alias = get_method_alias_def(context.graph(), "Foo#bar()");
1804+
let results = dealias_method(context.graph(), alias);
1805+
assert_eq!(results.len(), 1);
1806+
assert!(matches!(results[0], DealiasMethodResult::Method(_)));
1807+
}
1808+
1809+
#[test]
1810+
fn dealias_method_chained() {
1811+
let mut context = GraphTest::new();
1812+
context.index_uri(
1813+
"file:///foo.rb",
1814+
"
1815+
class Foo
1816+
def foo(x); end
1817+
alias bar foo
1818+
alias baz bar
1819+
end
1820+
",
1821+
);
1822+
context.resolve();
1823+
1824+
// baz -> bar: one level returns the alias to bar
1825+
let alias = get_method_alias_def(context.graph(), "Foo#baz()");
1826+
let results = dealias_method(context.graph(), alias);
1827+
assert_eq!(results.len(), 1);
1828+
assert!(matches!(results[0], DealiasMethodResult::Alias(_)));
1829+
1830+
// bar -> foo: one more level returns the method definition
1831+
let alias = get_method_alias_def(context.graph(), "Foo#bar()");
1832+
let results = dealias_method(context.graph(), alias);
1833+
assert_eq!(results.len(), 1);
1834+
assert!(matches!(results[0], DealiasMethodResult::Method(_)));
1835+
}
1836+
1837+
#[test]
1838+
fn dealias_method_unresolved() {
1839+
let mut context = GraphTest::new();
1840+
context.index_uri(
1841+
"file:///foo.rb",
1842+
"
1843+
class Foo
1844+
alias bar nonexistent
1845+
end
1846+
",
1847+
);
1848+
context.resolve();
1849+
1850+
let alias = get_method_alias_def(context.graph(), "Foo#bar()");
1851+
let results = dealias_method(context.graph(), alias);
1852+
assert!(results.is_empty());
1853+
}
1854+
1855+
#[test]
1856+
fn dealias_method_multiple_definitions() {
1857+
let mut context = GraphTest::new();
1858+
context.index_uri(
1859+
"file:///foo.rb",
1860+
"
1861+
class Foo
1862+
def foo(a); end
1863+
end
1864+
",
1865+
);
1866+
context.index_uri(
1867+
"file:///foo2.rb",
1868+
"
1869+
class Foo
1870+
alias foo baz # another alias definition of #foo()
1871+
alias bar foo
1872+
end
1873+
",
1874+
);
1875+
context.resolve();
1876+
1877+
let alias = get_method_alias_def(context.graph(), "Foo#bar()");
1878+
let results = dealias_method(context.graph(), alias);
1879+
assert_eq!(results.len(), 2);
1880+
assert_eq!(
1881+
results
1882+
.iter()
1883+
.filter(|r| matches!(r, DealiasMethodResult::Method(_)))
1884+
.count(),
1885+
1
1886+
);
1887+
assert_eq!(
1888+
results
1889+
.iter()
1890+
.filter(|r| matches!(r, DealiasMethodResult::Alias(_)))
1891+
.count(),
1892+
1
1893+
);
1894+
}
1895+
16861896
#[test]
16871897
fn method_call_completion_excludes_keywords() {
16881898
let mut context = GraphTest::new();

0 commit comments

Comments
 (0)