Skip to content

Commit d1f4139

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 d1f4139

5 files changed

Lines changed: 312 additions & 4 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-sys/src/definition_api.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -433,7 +433,7 @@ pub unsafe extern "C" fn rdx_definition_signatures(pointer: GraphPointer, defini
433433
})
434434
}
435435

436-
/// Helper: build signature entries from a MethodDefinition and append them to the output vector.
436+
/// Helper: build signature entries from a `MethodDefinition` and append them to the output vector.
437437
pub(crate) fn collect_method_signatures(
438438
graph: &rubydex::model::graph::Graph,
439439
method_def: &rubydex::model::definitions::MethodDefinition,

rust/rubydex/src/query.rs

Lines changed: 169 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,78 @@ 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+
/// # Panics
216+
///
217+
/// Panics if:
218+
/// - the `declaration_id` does not exist in the graph
219+
/// - the declaration is not a method declaration
220+
/// - any definition or owner declaration referenced by the method is missing from the graph
221+
#[must_use]
222+
pub fn method_definitions(graph: &Graph, declaration_id: DeclarationId) -> Vec<(DefinitionId, &MethodDefinition)> {
223+
let decl = graph
224+
.declarations()
225+
.get(&declaration_id)
226+
.expect("declaration should exist in graph");
227+
assert!(
228+
matches!(decl, Declaration::Method(_)),
229+
"expected a method declaration, got {:?}",
230+
decl.kind()
231+
);
232+
233+
let owner_id = *decl.owner_id();
234+
let mut result = Vec::new();
235+
236+
for def_id in decl.definitions() {
237+
let defn = graph
238+
.definitions()
239+
.get(def_id)
240+
.expect("definition should exist in graph");
241+
242+
match defn {
243+
Definition::Method(method_def) => {
244+
result.push((*def_id, method_def.as_ref()));
245+
}
246+
Definition::MethodAlias(alias_def) => {
247+
// Resolve alias: look up the original method in the owner's members
248+
let owner = graph
249+
.declarations()
250+
.get(&owner_id)
251+
.expect("owner declaration should exist")
252+
.as_namespace()
253+
.expect("owner should be a namespace");
254+
255+
// Alias target may not exist (e.g. aliasing an undefined method)
256+
let Some(original_decl_id) = owner.member(alias_def.old_name_str_id()) else {
257+
continue;
258+
};
259+
260+
let original_decl = graph
261+
.declarations()
262+
.get(original_decl_id)
263+
.expect("original declaration should exist");
264+
265+
for original_def_id in original_decl.definitions() {
266+
let original_defn = graph
267+
.definitions()
268+
.get(original_def_id)
269+
.expect("original definition should exist in graph");
270+
if let Definition::Method(original_method_def) = original_defn {
271+
result.push((*original_def_id, original_method_def.as_ref()));
272+
}
273+
}
274+
}
275+
_ => {}
276+
}
277+
}
278+
279+
result
280+
}
281+
210282
/// Determines all possible completion candidates based on the current context of the cursor. There are multiple cases
211283
/// that change what has to be collected for completion:
212284
///
@@ -1621,4 +1693,99 @@ mod tests {
16211693

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

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)