Skip to content

Commit 8a49bfb

Browse files
soutaroclaude
andcommitted
Add Rubydex::Method#signatures with alias resolution
Add signatures method to method declarations (Rubydex::Method) that aggregates signatures from all definitions, resolving method aliases to the original method's signatures. - Add query::method_definitions() in Rust for alias-aware lookup - Add rdx_declaration_method_signatures C API function - Register Method#signatures in the C extension Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 17371dd commit 8a49bfb

4 files changed

Lines changed: 306 additions & 3 deletions

File tree

ext/rubydex/declaration.c

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include "handle.h"
55
#include "reference.h"
66
#include "rustbindings.h"
7+
#include "signature.h"
78
#include "utils.h"
89

910
VALUE cDeclaration;
@@ -331,6 +332,18 @@ static VALUE rdxr_declaration_references(VALUE self) {
331332
return self;
332333
}
333334

335+
// Method#signatures -> [Rubydex::Signature]
336+
static VALUE rdxr_method_declaration_signatures(VALUE self) {
337+
HandleData *data;
338+
TypedData_Get_Struct(self, HandleData, &handle_type, data);
339+
340+
void *graph;
341+
TypedData_Get_Struct(data->graph_obj, void *, &graph_type, graph);
342+
343+
SignatureArray *arr = rdx_declaration_method_signatures(graph, data->id);
344+
return rdxi_signatures_to_ruby(arr, data->graph_obj, Qnil);
345+
}
346+
334347
void rdxi_initialize_declaration(VALUE mRubydex) {
335348
cDeclaration = rb_define_class_under(mRubydex, "Declaration", rb_cObject);
336349
cNamespace = rb_define_class_under(mRubydex, "Namespace", cDeclaration);
@@ -341,6 +354,7 @@ void rdxi_initialize_declaration(VALUE mRubydex) {
341354
cConstant = rb_define_class_under(mRubydex, "Constant", cDeclaration);
342355
cConstantAlias = rb_define_class_under(mRubydex, "ConstantAlias", cDeclaration);
343356
cMethod = rb_define_class_under(mRubydex, "Method", cDeclaration);
357+
rb_define_method(cMethod, "signatures", rdxr_method_declaration_signatures, 0);
344358
cGlobalVariable = rb_define_class_under(mRubydex, "GlobalVariable", cDeclaration);
345359
cInstanceVariable = rb_define_class_under(mRubydex, "InstanceVariable", cDeclaration);
346360
cClassVariable = rb_define_class_under(mRubydex, "ClassVariable", cDeclaration);

rust/rubydex-sys/src/declaration_api.rs

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ use rubydex::model::declaration::{Ancestor, Declaration, Namespace};
55
use std::ffi::CString;
66
use std::ptr;
77

8-
use crate::definition_api::{DefinitionsIter, rdx_definitions_iter_new_from_ids};
8+
use crate::definition_api::{
9+
DefinitionsIter, SignatureArray, SignatureEntry, collect_method_signatures, rdx_definitions_iter_new_from_ids,
10+
};
911
use crate::graph_api::{GraphPointer, with_graph};
1012
use crate::reference_api::{CReference, ReferenceKind, ReferencesIter};
1113
use crate::utils;
@@ -439,3 +441,42 @@ pub unsafe extern "C" fn rdx_declaration_references_iter_new(
439441
ReferencesIter::new(entries.into_boxed_slice())
440442
})
441443
}
444+
445+
/// Returns a newly allocated array of signatures for the given method declaration id.
446+
/// Aggregates signatures from all definitions. For alias definitions, resolves to the
447+
/// original method's signatures.
448+
/// Returns NULL if the declaration is not a method declaration.
449+
/// Caller must free the returned pointer with `rdx_definition_signatures_free`.
450+
///
451+
/// # Safety
452+
/// - `pointer` must be a valid pointer previously returned by `rdx_graph_new`.
453+
/// - `declaration_id` must be a valid declaration id.
454+
///
455+
/// # Panics
456+
/// This function will panic if declarations or definitions cannot be found.
457+
#[unsafe(no_mangle)]
458+
pub unsafe extern "C" fn rdx_declaration_method_signatures(
459+
pointer: GraphPointer,
460+
declaration_id: u64,
461+
) -> *mut SignatureArray {
462+
with_graph(pointer, |graph| {
463+
let decl_id = DeclarationId::new(declaration_id);
464+
let method_defs = rubydex::query::method_definitions(graph, decl_id);
465+
466+
if method_defs.is_empty() {
467+
return ptr::null_mut();
468+
}
469+
470+
let mut sig_entries: Vec<SignatureEntry> = Vec::new();
471+
for (def_id, method_def) in &method_defs {
472+
collect_method_signatures(graph, method_def, **def_id, &mut sig_entries);
473+
}
474+
475+
let mut boxed = sig_entries.into_boxed_slice();
476+
let len = boxed.len();
477+
let items_ptr = boxed.as_mut_ptr();
478+
std::mem::forget(boxed);
479+
480+
Box::into_raw(Box::new(SignatureArray { items: items_ptr, len }))
481+
})
482+
}

rust/rubydex/src/query.rs

Lines changed: 164 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, MethodDefinition, Parameter};
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

@@ -207,6 +207,73 @@ macro_rules! collect_candidates {
207207
};
208208
}
209209

210+
/// Returns the method definitions associated with a method declaration, resolving aliases.
211+
///
212+
/// For regular method declarations, returns the `MethodDefinition`s directly.
213+
/// For alias declarations, follows the alias to the original method and returns its definitions.
214+
///
215+
/// Returns an empty vec if the declaration is not a method declaration.
216+
#[must_use]
217+
pub fn method_definitions(graph: &Graph, declaration_id: DeclarationId) -> Vec<(DefinitionId, &MethodDefinition)> {
218+
let decl = graph
219+
.declarations()
220+
.get(&declaration_id)
221+
.expect("declaration should exist in graph");
222+
assert!(
223+
matches!(decl, Declaration::Method(_)),
224+
"expected a method declaration, got {:?}",
225+
decl.kind()
226+
);
227+
228+
let owner_id = *decl.owner_id();
229+
let mut result = Vec::new();
230+
231+
for def_id in decl.definitions() {
232+
let defn = graph
233+
.definitions()
234+
.get(def_id)
235+
.expect("definition should exist in graph");
236+
237+
match defn {
238+
Definition::Method(method_def) => {
239+
result.push((*def_id, method_def.as_ref()));
240+
}
241+
Definition::MethodAlias(alias_def) => {
242+
// Resolve alias: look up the original method in the owner's members
243+
let owner = graph
244+
.declarations()
245+
.get(&owner_id)
246+
.expect("owner declaration should exist")
247+
.as_namespace()
248+
.expect("owner should be a namespace");
249+
250+
// Alias target may not exist (e.g. aliasing an undefined method)
251+
let Some(original_decl_id) = owner.member(alias_def.old_name_str_id()) else {
252+
continue;
253+
};
254+
255+
let original_decl = graph
256+
.declarations()
257+
.get(original_decl_id)
258+
.expect("original declaration should exist");
259+
260+
for original_def_id in original_decl.definitions() {
261+
let original_defn = graph
262+
.definitions()
263+
.get(original_def_id)
264+
.expect("original definition should exist in graph");
265+
if let Definition::Method(original_method_def) = original_defn {
266+
result.push((*original_def_id, original_method_def.as_ref()));
267+
}
268+
}
269+
}
270+
_ => {}
271+
}
272+
}
273+
274+
result
275+
}
276+
210277
/// Determines all possible completion candidates based on the current context of the cursor. There are multiple cases
211278
/// that change what has to be collected for completion:
212279
///
@@ -1621,4 +1688,99 @@ mod tests {
16211688

16221689
assert!(!candidates.iter().any(|c| matches!(c, CompletionCandidate::Keyword(_))));
16231690
}
1691+
1692+
/// Helper to get source text at an offset
1693+
fn source_at<'a>(source: &'a str, offset: &crate::offset::Offset) -> &'a str {
1694+
&source[offset.start() as usize..offset.end() as usize]
1695+
}
1696+
1697+
#[test]
1698+
fn test_method_definitions_returns_method_definitions() {
1699+
let mut context = GraphTest::new();
1700+
// 0123456789...
1701+
let source = "class Foo\n def bar(a, b); end\nend\n";
1702+
context.index_uri("file:///foo.rb", source);
1703+
context.resolve();
1704+
1705+
let defs = method_definitions(context.graph(), DeclarationId::from("Foo#bar()"));
1706+
assert_eq!(1, defs.len());
1707+
assert_eq!("def bar(a, b); end", source_at(source, defs[0].1.offset()));
1708+
}
1709+
1710+
#[test]
1711+
fn test_method_definitions_resolves_alias() {
1712+
let mut context = GraphTest::new();
1713+
let source = "class Foo\n def bar(a, b); end\n alias_method :baz, :bar\nend\n";
1714+
context.index_uri("file:///foo.rb", source);
1715+
context.resolve();
1716+
1717+
let defs = method_definitions(context.graph(), DeclarationId::from("Foo#baz()"));
1718+
assert_eq!(1, defs.len());
1719+
// Returns the original method's definition
1720+
assert_eq!("def bar(a, b); end", source_at(source, defs[0].1.offset()));
1721+
}
1722+
1723+
#[test]
1724+
fn test_method_definitions_alias_to_undefined_method() {
1725+
let mut context = GraphTest::new();
1726+
context.index_uri("file:///foo.rb", "class Foo\n alias_method :baz, :nonexistent\nend\n");
1727+
context.resolve();
1728+
1729+
let defs = method_definitions(context.graph(), DeclarationId::from("Foo#baz()"));
1730+
assert!(defs.is_empty());
1731+
}
1732+
1733+
#[test]
1734+
fn test_method_definitions_with_override() {
1735+
let mut context = GraphTest::new();
1736+
let source = "class Foo\n def bar(a); end\n def bar(a, b); end\nend\n";
1737+
context.index_uri("file:///foo.rb", source);
1738+
context.resolve();
1739+
1740+
let defs = method_definitions(context.graph(), DeclarationId::from("Foo#bar()"));
1741+
assert_eq!(2, defs.len());
1742+
1743+
let mut texts: Vec<&str> = defs.iter().map(|(_, d)| source_at(source, d.offset())).collect();
1744+
texts.sort();
1745+
assert_eq!(vec!["def bar(a); end", "def bar(a, b); end"], texts);
1746+
}
1747+
1748+
#[test]
1749+
#[should_panic(expected = "expected a method declaration")]
1750+
fn test_method_definitions_panics_for_non_method() {
1751+
let mut context = GraphTest::new();
1752+
context.index_uri("file:///foo.rb", "class Foo; end");
1753+
context.resolve();
1754+
1755+
method_definitions(context.graph(), DeclarationId::from("Foo"));
1756+
}
1757+
1758+
#[test]
1759+
fn test_method_definitions_from_rbs() {
1760+
let mut context = GraphTest::new();
1761+
let source = "class Foo\n def bar: (String name) -> void\n | (Integer id) -> String\nend\n";
1762+
context.index_rbs_uri("file:///foo.rbs", source);
1763+
context.resolve();
1764+
1765+
let defs = method_definitions(context.graph(), DeclarationId::from("Foo#bar()"));
1766+
assert_eq!(1, defs.len());
1767+
assert_eq!(
1768+
"def bar: (String name) -> void\n | (Integer id) -> String",
1769+
source_at(source, defs[0].1.offset())
1770+
);
1771+
}
1772+
1773+
#[test]
1774+
fn test_method_definitions_from_rbs_alias() {
1775+
let mut context = GraphTest::new();
1776+
let rb_source = "class Foo\n def bar(a, b); end\nend\n";
1777+
let rbs_source = "class Foo\n alias baz bar\nend\n";
1778+
context.index_uri("file:///foo.rb", rb_source);
1779+
context.index_rbs_uri("file:///foo.rbs", rbs_source);
1780+
context.resolve();
1781+
1782+
let defs = method_definitions(context.graph(), DeclarationId::from("Foo#baz()"));
1783+
assert_eq!(1, defs.len());
1784+
assert_eq!("def bar(a, b); end", source_at(rb_source, defs[0].1.offset()));
1785+
}
16241786
}

test/declaration_test.rb

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,4 +538,90 @@ def initialize
538538
assert_equal("Foo#initialize()", decl.name)
539539
end
540540
end
541+
542+
def test_method_declaration_signatures
543+
with_context do |context|
544+
context.write!("file1.rb", <<~RUBY)
545+
class Foo
546+
def bar(a, b = 1); end
547+
end
548+
RUBY
549+
550+
graph = Rubydex::Graph.new
551+
graph.index_all(context.glob("**/*.rb"))
552+
graph.resolve
553+
554+
decl = graph["Foo#bar()"]
555+
assert_instance_of(Rubydex::Method, decl)
556+
557+
signatures = decl.signatures
558+
assert_equal(1, signatures.length)
559+
560+
sig = signatures.first
561+
assert_instance_of(Rubydex::Signature, sig)
562+
assert_instance_of(Rubydex::MethodDefinition, sig.method_definition)
563+
564+
params = sig.parameters
565+
assert_equal(2, params.length)
566+
assert_equal([:req, :a], params[0][0..1])
567+
assert_equal([:opt, :b], params[1][0..1])
568+
end
569+
end
570+
571+
def test_method_declaration_signatures_with_alias
572+
with_context do |context|
573+
context.write!("file1.rb", <<~RUBY)
574+
class Foo
575+
def bar(a, b); end
576+
alias_method :baz, :bar
577+
end
578+
RUBY
579+
580+
graph = Rubydex::Graph.new
581+
graph.index_all(context.glob("**/*.rb"))
582+
graph.resolve
583+
584+
# Alias resolves to original method's signatures
585+
decl = graph["Foo#baz()"]
586+
assert_instance_of(Rubydex::Method, decl)
587+
588+
signatures = decl.signatures
589+
assert_equal(1, signatures.length)
590+
591+
params = signatures.first.parameters
592+
assert_equal(2, params.length)
593+
assert_equal([:req, :a], params[0][0..1])
594+
assert_equal([:req, :b], params[1][0..1])
595+
596+
# method_definition points to the original MethodDefinition
597+
assert_instance_of(Rubydex::MethodDefinition, signatures.first.method_definition)
598+
assert_equal("bar()", signatures.first.method_definition.name)
599+
end
600+
end
601+
602+
def test_method_declaration_signatures_with_override
603+
with_context do |context|
604+
context.write!("file1.rb", <<~RUBY)
605+
class Foo
606+
def bar(a); end
607+
def bar(a, b); end
608+
end
609+
RUBY
610+
611+
graph = Rubydex::Graph.new
612+
graph.index_all(context.glob("**/*.rb"))
613+
graph.resolve
614+
615+
decl = graph["Foo#bar()"]
616+
signatures = decl.signatures
617+
assert_equal(2, signatures.length)
618+
619+
# Each signature comes from a different definition
620+
params0 = signatures[0].parameters
621+
params1 = signatures[1].parameters
622+
623+
param_counts = [params0.length, params1.length].sort
624+
assert_equal([1, 2], param_counts)
625+
end
626+
end
541627
end

0 commit comments

Comments
 (0)