From b32b6922ff0e07087a46cee83397a28a42ee1402 Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Tue, 31 Mar 2026 10:01:26 +0900 Subject: [PATCH 01/27] Add Rubydex::Signature class and Rust C API for method signatures Co-Authored-By: Claude Opus 4.6 (1M context) --- ext/rubydex/rubydex.c | 2 + ext/rubydex/signature.c | 87 +++++++++++++ ext/rubydex/signature.h | 26 ++++ lib/rubydex.rb | 1 + lib/rubydex/signature.rb | 31 +++++ rust/rubydex-sys/src/definition_api.rs | 165 ++++++++++++++++++++++++- rust/rubydex/src/model/definitions.rs | 17 +++ 7 files changed, 328 insertions(+), 1 deletion(-) create mode 100644 ext/rubydex/signature.c create mode 100644 ext/rubydex/signature.h create mode 100644 lib/rubydex/signature.rb diff --git a/ext/rubydex/rubydex.c b/ext/rubydex/rubydex.c index 8f00e2c3..ea6b7d3d 100644 --- a/ext/rubydex/rubydex.c +++ b/ext/rubydex/rubydex.c @@ -5,6 +5,7 @@ #include "graph.h" #include "location.h" #include "reference.h" +#include "signature.h" VALUE mRubydex; @@ -19,4 +20,5 @@ void Init_rubydex(void) { rdxi_initialize_location(mRubydex); rdxi_initialize_diagnostic(mRubydex); rdxi_initialize_reference(mRubydex); + rdxi_initialize_signature(mRubydex); } diff --git a/ext/rubydex/signature.c b/ext/rubydex/signature.c new file mode 100644 index 00000000..68a2b8c1 --- /dev/null +++ b/ext/rubydex/signature.c @@ -0,0 +1,87 @@ +#include "signature.h" +#include "definition.h" +#include "handle.h" +#include "location.h" + +VALUE cSignature; +VALUE cParameter; +VALUE cPositionalParameter; +VALUE cOptionalPositionalParameter; +VALUE cRestPositionalParameter; +VALUE cKeywordParameter; +VALUE cOptionalKeywordParameter; +VALUE cRestKeywordParameter; +VALUE cBlockParameter; +VALUE cForwardParameter; + +static VALUE parameter_class_for_kind(ParameterKind kind) { + switch (kind) { + case ParameterKind_RequiredPositional: return cPositionalParameter; + case ParameterKind_OptionalPositional: return cOptionalPositionalParameter; + case ParameterKind_Rest: return cRestPositionalParameter; + case ParameterKind_RequiredKeyword: return cKeywordParameter; + case ParameterKind_OptionalKeyword: return cOptionalKeywordParameter; + case ParameterKind_RestKeyword: return cRestKeywordParameter; + case ParameterKind_Block: return cBlockParameter; + case ParameterKind_Forward: return cForwardParameter; + default: rb_raise(rb_eRuntimeError, "Unknown ParameterKind: %d", kind); + } +} + +VALUE rdxi_signatures_to_ruby(SignatureArray *arr, VALUE graph_obj, VALUE default_method_def) { + if (arr == NULL || arr->len == 0) { + if (arr != NULL) { + rdx_definition_signatures_free(arr); + } + return rb_ary_new(); + } + + VALUE signatures = rb_ary_new_capa((long)arr->len); + + for (size_t i = 0; i < arr->len; i++) { + SignatureEntry sig_entry = arr->items[i]; + + VALUE method_def; + if (default_method_def != Qnil) { + method_def = default_method_def; + } else { + VALUE def_argv[] = {graph_obj, ULL2NUM(sig_entry.definition_id)}; + method_def = rb_class_new_instance(2, def_argv, cMethodDefinition); + } + + VALUE parameters = rb_ary_new_capa((long)sig_entry.parameters_len); + for (size_t j = 0; j < sig_entry.parameters_len; j++) { + ParameterEntry param_entry = sig_entry.parameters[j]; + + VALUE param_class = parameter_class_for_kind(param_entry.kind); + VALUE name_sym = ID2SYM(rb_intern(param_entry.name)); + VALUE location = rdxi_build_location_value(param_entry.location); + VALUE param_argv[] = {name_sym, location}; + VALUE param = rb_class_new_instance(2, param_argv, param_class); + + rb_ary_push(parameters, param); + } + + VALUE sig_argv[] = {parameters, method_def}; + VALUE signature = rb_class_new_instance(2, sig_argv, cSignature); + + rb_ary_push(signatures, signature); + } + + rdx_definition_signatures_free(arr); + return signatures; +} + +void rdxi_initialize_signature(VALUE mRubydex) { + cSignature = rb_define_class_under(mRubydex, "Signature", rb_cObject); + + cParameter = rb_define_class_under(cSignature, "Parameter", rb_cObject); + cPositionalParameter = rb_define_class_under(cSignature, "PositionalParameter", cParameter); + cOptionalPositionalParameter = rb_define_class_under(cSignature, "OptionalPositionalParameter", cParameter); + cRestPositionalParameter = rb_define_class_under(cSignature, "RestPositionalParameter", cParameter); + cKeywordParameter = rb_define_class_under(cSignature, "KeywordParameter", cParameter); + cOptionalKeywordParameter = rb_define_class_under(cSignature, "OptionalKeywordParameter", cParameter); + cRestKeywordParameter = rb_define_class_under(cSignature, "RestKeywordParameter", cParameter); + cBlockParameter = rb_define_class_under(cSignature, "BlockParameter", cParameter); + cForwardParameter = rb_define_class_under(cSignature, "ForwardParameter", cParameter); +} diff --git a/ext/rubydex/signature.h b/ext/rubydex/signature.h new file mode 100644 index 00000000..b4e828bd --- /dev/null +++ b/ext/rubydex/signature.h @@ -0,0 +1,26 @@ +#ifndef RUBYDEX_SIGNATURE_H +#define RUBYDEX_SIGNATURE_H + +#include "ruby.h" +#include "rustbindings.h" + +extern VALUE cSignature; +extern VALUE cParameter; +extern VALUE cPositionalParameter; +extern VALUE cOptionalPositionalParameter; +extern VALUE cRestPositionalParameter; +extern VALUE cKeywordParameter; +extern VALUE cOptionalKeywordParameter; +extern VALUE cRestKeywordParameter; +extern VALUE cBlockParameter; +extern VALUE cForwardParameter; + +// Convert a SignatureArray into a Ruby array of Rubydex::Signature objects. +// If default_method_def is not Qnil, it is used as method_definition for all signatures. +// Otherwise, a new MethodDefinition handle is built from each SignatureEntry's definition_id. +// The SignatureArray is freed after conversion. +VALUE rdxi_signatures_to_ruby(SignatureArray *arr, VALUE graph_obj, VALUE default_method_def); + +void rdxi_initialize_signature(VALUE mRubydex); + +#endif // RUBYDEX_SIGNATURE_H diff --git a/lib/rubydex.rb b/lib/rubydex.rb index e44b9a28..d7903e2a 100644 --- a/lib/rubydex.rb +++ b/lib/rubydex.rb @@ -17,6 +17,7 @@ require "rubydex/failures" require "rubydex/location" require "rubydex/comment" +require "rubydex/signature" require "rubydex/diagnostic" require "rubydex/keyword" require "rubydex/keyword_parameter" diff --git a/lib/rubydex/signature.rb b/lib/rubydex/signature.rb new file mode 100644 index 00000000..31b941fa --- /dev/null +++ b/lib/rubydex/signature.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Rubydex + class Signature + class Parameter + #: Symbol + attr_reader :name + + #: Location + attr_reader :location + + #: (Symbol, Location) -> void + def initialize(name, location) + @name = name + @location = location + end + end + + #: Array[Parameter] + attr_reader :parameters + + #: MethodDefinition + attr_reader :method_definition + + #: (Array[Parameter], MethodDefinition) -> void + def initialize(parameters, method_definition) + @parameters = parameters + @method_definition = method_definition + end + end +end diff --git a/rust/rubydex-sys/src/definition_api.rs b/rust/rubydex-sys/src/definition_api.rs index b96f85af..44dfd5c0 100644 --- a/rust/rubydex-sys/src/definition_api.rs +++ b/rust/rubydex-sys/src/definition_api.rs @@ -4,7 +4,7 @@ use crate::graph_api::{GraphPointer, with_graph}; use crate::location_api::{Location, create_location_for_uri_and_offset}; use crate::reference_api::CConstantReference; use libc::c_char; -use rubydex::model::definitions::{Definition, Mixin}; +use rubydex::model::definitions::{Definition, Mixin, Parameter}; use rubydex::model::ids::DefinitionId; use std::ffi::CString; use std::ptr; @@ -441,3 +441,166 @@ pub unsafe extern "C" fn rdx_definition_mixins(pointer: GraphPointer, definition MixinsIter::new(entries.into_boxed_slice()) }) } + +/// C-compatible enum representing the kind of a parameter. +#[repr(C)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum ParameterKind { + RequiredPositional = 0, + OptionalPositional = 1, + Rest = 2, + RequiredKeyword = 3, + OptionalKeyword = 4, + RestKeyword = 5, + Block = 6, + Forward = 7, +} + +fn map_parameter_kind(param: &Parameter) -> ParameterKind { + match param { + Parameter::RequiredPositional(_) | Parameter::Post(_) => ParameterKind::RequiredPositional, + Parameter::OptionalPositional(_) => ParameterKind::OptionalPositional, + Parameter::RestPositional(_) => ParameterKind::Rest, + Parameter::RequiredKeyword(_) => ParameterKind::RequiredKeyword, + Parameter::OptionalKeyword(_) => ParameterKind::OptionalKeyword, + Parameter::RestKeyword(_) => ParameterKind::RestKeyword, + Parameter::Block(_) => ParameterKind::Block, + Parameter::Forward(_) => ParameterKind::Forward, + } +} + +/// C-compatible struct representing a single parameter with its name, kind, and location. +#[repr(C)] +pub struct ParameterEntry { + pub name: *const c_char, + pub kind: ParameterKind, + pub location: *mut Location, +} + +/// C-compatible struct representing a single method signature (a list of parameters). +#[repr(C)] +pub struct SignatureEntry { + pub definition_id: u64, + pub parameters: *mut ParameterEntry, + pub parameters_len: usize, +} + +/// C-compatible array of signatures. +#[repr(C)] +pub struct SignatureArray { + pub items: *mut SignatureEntry, + pub len: usize, +} + +/// Returns a newly allocated array of signatures for the given method definition id. +/// Returns NULL if the definition is not a method definition. +/// Caller must free the returned pointer with `rdx_definition_signatures_free`. +/// +/// # Safety +/// - `pointer` must be a valid pointer previously returned by `rdx_graph_new`. +/// - `definition_id` must be a valid definition id. +/// +/// # Panics +/// This function will panic if a definition or document cannot be found. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rdx_definition_signatures(pointer: GraphPointer, definition_id: u64) -> *mut SignatureArray { + with_graph(pointer, |graph| { + let def_id = DefinitionId::new(definition_id); + let Some(Definition::Method(method_def)) = graph.definitions().get(&def_id) else { + return ptr::null_mut(); + }; + + let mut sig_entries: Vec = Vec::new(); + collect_method_signatures(graph, method_def, definition_id, &mut sig_entries); + + let mut boxed = sig_entries.into_boxed_slice(); + let len = boxed.len(); + let items_ptr = boxed.as_mut_ptr(); + std::mem::forget(boxed); + + Box::into_raw(Box::new(SignatureArray { items: items_ptr, len })) + }) +} + +/// Helper: build signature entries from a `MethodDefinition` and append them to the output vector. +fn collect_method_signatures( + graph: &rubydex::model::graph::Graph, + method_def: &rubydex::model::definitions::MethodDefinition, + definition_id: u64, + out: &mut Vec, +) { + let uri_id = *method_def.uri_id(); + let document = graph.documents().get(&uri_id).expect("document should exist"); + + for sig in method_def.signatures().as_slice() { + let mut param_entries = sig + .iter() + .map(|param| { + let param_struct = param.inner(); + let name = graph + .strings() + .get(param_struct.str()) + .expect("parameter name string should exist"); + let name_str = CString::new(name.as_str()).unwrap().into_raw().cast_const(); + + ParameterEntry { + name: name_str, + kind: map_parameter_kind(param), + location: create_location_for_uri_and_offset(graph, document, param_struct.offset()), + } + }) + .collect::>() + .into_boxed_slice(); + + let parameters_len = param_entries.len(); + let parameters_ptr = param_entries.as_mut_ptr(); + std::mem::forget(param_entries); + + out.push(SignatureEntry { + definition_id, + parameters: parameters_ptr, + parameters_len, + }); + } +} + +/// Frees a `SignatureArray` previously returned by `rdx_definition_signatures`. +/// +/// # Safety +/// - `ptr` must be a valid pointer previously returned by `rdx_definition_signatures`. +/// - `ptr` must not be used after being freed. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rdx_definition_signatures_free(ptr: *mut SignatureArray) { + if ptr.is_null() { + return; + } + + // Take ownership of the SignatureArray + let arr = unsafe { Box::from_raw(ptr) }; + + if !arr.items.is_null() && arr.len > 0 { + // Reconstruct the boxed slice so we can drop it after freeing inner allocations + let slice_ptr = ptr::slice_from_raw_parts_mut(arr.items, arr.len); + let mut sig_slice: Box<[SignatureEntry]> = unsafe { Box::from_raw(slice_ptr) }; + + for sig_entry in &mut sig_slice { + if !sig_entry.parameters.is_null() && sig_entry.parameters_len > 0 { + let param_slice_ptr = ptr::slice_from_raw_parts_mut(sig_entry.parameters, sig_entry.parameters_len); + let mut param_slice: Box<[ParameterEntry]> = unsafe { Box::from_raw(param_slice_ptr) }; + + for param_entry in &mut param_slice { + if !param_entry.name.is_null() { + let _ = unsafe { CString::from_raw(param_entry.name.cast_mut()) }; + } + if !param_entry.location.is_null() { + unsafe { crate::location_api::rdx_location_free(param_entry.location) }; + param_entry.location = ptr::null_mut(); + } + } + // param_slice is dropped here, freeing the parameters buffer + } + } + // sig_slice is dropped here, freeing the signatures buffer + } + // arr is dropped here +} diff --git a/rust/rubydex/src/model/definitions.rs b/rust/rubydex/src/model/definitions.rs index 449ce8c3..974be5dc 100644 --- a/rust/rubydex/src/model/definitions.rs +++ b/rust/rubydex/src/model/definitions.rs @@ -1024,6 +1024,23 @@ pub enum Parameter { } assert_mem_size!(Parameter, 24); +impl Parameter { + #[must_use] + pub fn inner(&self) -> &ParameterStruct { + match self { + Parameter::RequiredPositional(s) + | Parameter::OptionalPositional(s) + | Parameter::RestPositional(s) + | Parameter::Post(s) + | Parameter::RequiredKeyword(s) + | Parameter::OptionalKeyword(s) + | Parameter::RestKeyword(s) + | Parameter::Forward(s) + | Parameter::Block(s) => s, + } + } +} + #[derive(Debug, Clone)] pub struct ParameterStruct { offset: Offset, From 3203c85f7010ddcc9afb329d0bd1f1f8e0a16692 Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Tue, 31 Mar 2026 10:01:34 +0900 Subject: [PATCH 02/27] Add MethodDefinition#signatures to Ruby API Co-Authored-By: Claude Opus 4.6 (1M context) --- ext/rubydex/definition.c | 14 +++ test/definition_test.rb | 209 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 223 insertions(+) diff --git a/ext/rubydex/definition.c b/ext/rubydex/definition.c index 887fb27e..34774671 100644 --- a/ext/rubydex/definition.c +++ b/ext/rubydex/definition.c @@ -5,6 +5,7 @@ #include "reference.h" #include "ruby/internal/scan_args.h" #include "rustbindings.h" +#include "signature.h" static VALUE mRubydex; static VALUE cInclude; @@ -239,6 +240,18 @@ static VALUE rdxr_definition_mixins(VALUE self) { return ary; } +// MethodDefinition#signatures -> [Rubydex::Signature] +static VALUE rdxr_method_definition_signatures(VALUE self) { + HandleData *data; + TypedData_Get_Struct(self, HandleData, &handle_type, data); + + void *graph; + TypedData_Get_Struct(data->graph_obj, void *, &graph_type, graph); + + SignatureArray *arr = rdx_definition_signatures(graph, data->id); + return rdxi_signatures_to_ruby(arr, data->graph_obj, self); +} + void rdxi_initialize_definition(VALUE mod) { mRubydex = mod; @@ -273,6 +286,7 @@ void rdxi_initialize_definition(VALUE mod) { cConstantVisibilityDefinition = rb_define_class_under(mRubydex, "ConstantVisibilityDefinition", cDefinition); cMethodVisibilityDefinition = rb_define_class_under(mRubydex, "MethodVisibilityDefinition", cDefinition); cMethodDefinition = rb_define_class_under(mRubydex, "MethodDefinition", cDefinition); + rb_define_method(cMethodDefinition, "signatures", rdxr_method_definition_signatures, 0); cAttrAccessorDefinition = rb_define_class_under(mRubydex, "AttrAccessorDefinition", cDefinition); cAttrReaderDefinition = rb_define_class_under(mRubydex, "AttrReaderDefinition", cDefinition); cAttrWriterDefinition = rb_define_class_under(mRubydex, "AttrWriterDefinition", cDefinition); diff --git a/test/definition_test.rb b/test/definition_test.rb index 2389bf12..bd1698d4 100644 --- a/test/definition_test.rb +++ b/test/definition_test.rb @@ -364,6 +364,215 @@ class << self end end + def test_method_definition_signatures_with_various_parameter_kinds + with_context do |context| + context.write!("file1.rb", <<~RUBY) + def foo(a, b = 1, *c, d, e:, f: 1, **g, &h); end + RUBY + + graph = Rubydex::Graph.new + graph.index_all(context.glob("**/*.rb")) + graph.resolve + + method_def = graph["Object#foo()"].definitions.first + refute_nil(method_def) + + signatures = method_def.signatures + assert_equal(1, signatures.length) + + sig = signatures.first + assert_instance_of(Rubydex::Signature, sig) + assert_same(method_def, sig.method_definition) + + params = sig.parameters + assert_equal(8, params.length) + + path = context.absolute_path_to("file1.rb") + + assert_instance_of(Rubydex::Signature::PositionalParameter, params[0]) + assert_equal(:a, params[0].name) + assert_equal("#{path}:1:9-1:10", params[0].location.to_display.to_s) # a + + assert_instance_of(Rubydex::Signature::OptionalPositionalParameter, params[1]) + assert_equal(:b, params[1].name) + assert_equal("#{path}:1:12-1:13", params[1].location.to_display.to_s) # b + + assert_instance_of(Rubydex::Signature::RestPositionalParameter, params[2]) + assert_equal(:c, params[2].name) + assert_equal("#{path}:1:20-1:21", params[2].location.to_display.to_s) # c + + assert_instance_of(Rubydex::Signature::PositionalParameter, params[3]) + assert_equal(:d, params[3].name) + assert_equal("#{path}:1:23-1:24", params[3].location.to_display.to_s) # d + + assert_instance_of(Rubydex::Signature::KeywordParameter, params[4]) + assert_equal(:e, params[4].name) + assert_equal("#{path}:1:26-1:27", params[4].location.to_display.to_s) # e + + assert_instance_of(Rubydex::Signature::OptionalKeywordParameter, params[5]) + assert_equal(:f, params[5].name) + assert_equal("#{path}:1:30-1:31", params[5].location.to_display.to_s) # f + + assert_instance_of(Rubydex::Signature::RestKeywordParameter, params[6]) + assert_equal(:g, params[6].name) + assert_equal("#{path}:1:38-1:39", params[6].location.to_display.to_s) # g + + assert_instance_of(Rubydex::Signature::BlockParameter, params[7]) + assert_equal(:h, params[7].name) + assert_equal("#{path}:1:42-1:43", params[7].location.to_display.to_s) # h + end + end + + def test_method_definition_signatures_no_parameters + with_context do |context| + context.write!("file1.rb", <<~RUBY) + def bar; end + RUBY + + graph = Rubydex::Graph.new + graph.index_all(context.glob("**/*.rb")) + graph.resolve + + method_def = graph["Object#bar()"].definitions.first + refute_nil(method_def) + + signatures = method_def.signatures + assert_equal(1, signatures.length) + assert_empty(signatures.first.parameters) + end + end + + def test_method_definition_signatures_forward + with_context do |context| + context.write!("file1.rb", <<~RUBY) + def baz(...); end + RUBY + + graph = Rubydex::Graph.new + graph.index_all(context.glob("**/*.rb")) + graph.resolve + + method_def = graph["Object#baz()"].definitions.first + refute_nil(method_def) + + path = context.absolute_path_to("file1.rb") + params = method_def.signatures.first.parameters + assert_equal(1, params.length) + assert_instance_of(Rubydex::Signature::ForwardParameter, params[0]) + assert_equal(:"...", params[0].name) + assert_equal("#{path}:1:9-1:12", params[0].location.to_display.to_s) # ... + end + end + + def test_method_definition_signatures_from_rbs + with_context do |context| + context.write!("foo.rbs", <<~RBS) + class Foo + def bar: (String a, ?String b, *String c, String d, name: String, ?mode: String, **String opts) { (String) -> void } -> void + end + RBS + + graph = Rubydex::Graph.new + graph.index_all(context.glob("**/*.rbs")) + graph.resolve + + method_def = graph["Foo#bar()"].definitions.first + refute_nil(method_def) + + signatures = method_def.signatures + assert_equal(1, signatures.length) + + params = signatures.first.parameters + assert_equal(8, params.length) + + path = context.absolute_path_to("foo.rbs") + + assert_instance_of(Rubydex::Signature::PositionalParameter, params[0]) + assert_equal(:a, params[0].name) + assert_equal("#{path}:2:20-2:21", params[0].location.to_display.to_s) # a + assert_instance_of(Rubydex::Signature::OptionalPositionalParameter, params[1]) + assert_equal(:b, params[1].name) + assert_equal("#{path}:2:31-2:32", params[1].location.to_display.to_s) # b + assert_instance_of(Rubydex::Signature::RestPositionalParameter, params[2]) + assert_equal(:c, params[2].name) + assert_equal("#{path}:2:42-2:43", params[2].location.to_display.to_s) # c + assert_instance_of(Rubydex::Signature::PositionalParameter, params[3]) + assert_equal(:d, params[3].name) + assert_equal("#{path}:2:52-2:53", params[3].location.to_display.to_s) # d + assert_instance_of(Rubydex::Signature::KeywordParameter, params[4]) + assert_equal(:name, params[4].name) + assert_equal("#{path}:2:55-2:59", params[4].location.to_display.to_s) # name + assert_instance_of(Rubydex::Signature::OptionalKeywordParameter, params[5]) + assert_equal(:mode, params[5].name) + assert_equal("#{path}:2:70-2:74", params[5].location.to_display.to_s) # mode + assert_instance_of(Rubydex::Signature::RestKeywordParameter, params[6]) + assert_equal(:opts, params[6].name) + assert_equal("#{path}:2:93-2:97", params[6].location.to_display.to_s) # opts + assert_instance_of(Rubydex::Signature::BlockParameter, params[7]) + assert_equal(:block, params[7].name) + assert_equal("#{path}:2:99-2:119", params[7].location.to_display.to_s) # { (String) -> void } + end + end + + def test_method_definition_signatures_from_rbs_with_untyped_parameters + with_context do |context| + context.write!("foo.rbs", <<~RBS) + class Foo + def baz: (?) -> void + end + RBS + + graph = Rubydex::Graph.new + graph.index_all(context.glob("**/*.rbs")) + graph.resolve + + method_def = graph["Foo#baz()"].definitions.first + refute_nil(method_def) + + signatures = method_def.signatures + assert_equal(1, signatures.length) + assert_empty(signatures.first.parameters) + end + end + + def test_method_definition_signatures_from_rbs_with_overloads + with_context do |context| + context.write!("foo.rbs", <<~RBS) + class Foo + def bar: (String name) -> void + | (Integer id, ?Symbol mode) -> String + end + RBS + + graph = Rubydex::Graph.new + graph.index_all(context.glob("**/*.rbs")) + graph.resolve + + method_def = graph["Foo#bar()"].definitions.first + refute_nil(method_def) + + signatures = method_def.signatures + assert_equal(2, signatures.length) + + path = context.absolute_path_to("foo.rbs") + + params0 = signatures[0].parameters + assert_equal(1, params0.length) + assert_instance_of(Rubydex::Signature::PositionalParameter, params0[0]) + assert_equal(:name, params0[0].name) + assert_equal("#{path}:2:20-2:24", params0[0].location.to_display.to_s) # name + + params1 = signatures[1].parameters + assert_equal(2, params1.length) + assert_instance_of(Rubydex::Signature::PositionalParameter, params1[0]) + assert_equal(:id, params1[0].name) + assert_equal("#{path}:3:21-3:23", params1[0].location.to_display.to_s) # id + assert_instance_of(Rubydex::Signature::OptionalPositionalParameter, params1[1]) + assert_equal(:mode, params1[1].name) + assert_equal("#{path}:3:33-3:37", params1[1].location.to_display.to_s) # mode + end + end + private # Comment locations on Windows include the carriage return. This means that the end column is off by one when compared From 8379cc7e7e2091ccd5d6eaad3a0ee4f2ed2f2f84 Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Tue, 31 Mar 2026 17:11:54 +0900 Subject: [PATCH 03/27] Implement `MethodAliasDefinition#signatures` --- ext/rubydex/definition.c | 13 ++ rust/rubydex-sys/src/definition_api.rs | 60 +++++++ rust/rubydex/src/query.rs | 214 ++++++++++++++++++++++++- test/definition_test.rb | 75 +++++++++ 4 files changed, 360 insertions(+), 2 deletions(-) diff --git a/ext/rubydex/definition.c b/ext/rubydex/definition.c index 34774671..538eeb93 100644 --- a/ext/rubydex/definition.c +++ b/ext/rubydex/definition.c @@ -252,6 +252,18 @@ static VALUE rdxr_method_definition_signatures(VALUE self) { return rdxi_signatures_to_ruby(arr, data->graph_obj, self); } +// MethodAliasDefinition#signatures -> [Rubydex::Signature] +static VALUE rdxr_method_alias_definition_signatures(VALUE self) { + HandleData *data; + TypedData_Get_Struct(self, HandleData, &handle_type, data); + + void *graph; + TypedData_Get_Struct(data->graph_obj, void *, &graph_type, graph); + + SignatureArray *arr = rdx_method_alias_definition_signatures(graph, data->id); + return rdxi_signatures_to_ruby(arr, data->graph_obj, Qnil); +} + void rdxi_initialize_definition(VALUE mod) { mRubydex = mod; @@ -294,5 +306,6 @@ void rdxi_initialize_definition(VALUE mod) { cInstanceVariableDefinition = rb_define_class_under(mRubydex, "InstanceVariableDefinition", cDefinition); cClassVariableDefinition = rb_define_class_under(mRubydex, "ClassVariableDefinition", cDefinition); cMethodAliasDefinition = rb_define_class_under(mRubydex, "MethodAliasDefinition", cDefinition); + rb_define_method(cMethodAliasDefinition, "signatures", rdxr_method_alias_definition_signatures, 0); cGlobalVariableAliasDefinition = rb_define_class_under(mRubydex, "GlobalVariableAliasDefinition", cDefinition); } diff --git a/rust/rubydex-sys/src/definition_api.rs b/rust/rubydex-sys/src/definition_api.rs index 44dfd5c0..1d8639b5 100644 --- a/rust/rubydex-sys/src/definition_api.rs +++ b/rust/rubydex-sys/src/definition_api.rs @@ -564,6 +564,66 @@ fn collect_method_signatures( } } +/// Returns signatures for a `MethodAliasDefinition` by following the alias chain. +/// Returns NULL if the definition is not a method alias or the chain cannot be resolved. +/// +/// # Safety +/// - `pointer` must be a valid pointer previously returned by `rdx_graph_new`. +/// - `definition_id` must be a valid definition id. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rdx_method_alias_definition_signatures( + pointer: GraphPointer, + definition_id: u64, +) -> *mut SignatureArray { + with_graph(pointer, |graph| { + let def_id = DefinitionId::new(definition_id); + let Some(Definition::MethodAlias(alias)) = graph.definitions().get(&def_id) else { + return ptr::null_mut(); + }; + + // Follow the alias chain until we find method definitions + let mut current_results = rubydex::query::dealias_method(graph, alias); + let mut visited = std::collections::HashSet::new(); + visited.insert(def_id); + + loop { + let mut sig_entries: Vec = Vec::new(); + let mut next_aliases = Vec::new(); + + for result in ¤t_results { + match result { + rubydex::query::DealiasMethodResult::Method(id) => { + if let Some(Definition::Method(method_def)) = graph.definitions().get(id) { + collect_method_signatures(graph, method_def, id.get(), &mut sig_entries); + } + } + rubydex::query::DealiasMethodResult::Alias(id) => { + if visited.insert(*id) + && let Some(Definition::MethodAlias(next_alias)) = graph.definitions().get(id) + { + next_aliases.extend(rubydex::query::dealias_method(graph, next_alias)); + } + } + } + } + + if !sig_entries.is_empty() { + let mut boxed = sig_entries.into_boxed_slice(); + let len = boxed.len(); + let items_ptr = boxed.as_mut_ptr(); + std::mem::forget(boxed); + return Box::into_raw(Box::new(SignatureArray { items: items_ptr, len })); + } + + if next_aliases.is_empty() { + return ptr::null_mut(); + } + + current_results = next_aliases; + } + }) +} + /// Frees a `SignatureArray` previously returned by `rdx_definition_signatures`. /// /// # Safety diff --git a/rust/rubydex/src/query.rs b/rust/rubydex/src/query.rs index 7c5c0fe2..5941477e 100644 --- a/rust/rubydex/src/query.rs +++ b/rust/rubydex/src/query.rs @@ -6,10 +6,10 @@ use std::thread; use url::Url; use crate::model::declaration::{Ancestor, Declaration}; -use crate::model::definitions::{Definition, Parameter}; +use crate::model::definitions::{Definition, MethodAliasDefinition, Parameter, Receiver}; use crate::model::graph::{Graph, OBJECT_ID}; use crate::model::identity_maps::IdentityHashSet; -use crate::model::ids::{DeclarationId, NameId, StringId, UriId}; +use crate::model::ids::{DeclarationId, DefinitionId, NameId, StringId, UriId}; use crate::model::keywords::{self, Keyword}; use crate::model::name::NameRef; @@ -469,6 +469,99 @@ fn method_argument_completion<'a>( Ok(candidates) } +/// Result of looking up the enclosing namespace for a definition. +#[allow(dead_code)] +enum EnclosingNamespace { + /// Found a namespace (class, module, or singleton class). + Found(DeclarationId), + /// The definition has no enclosing namespace (top-level). + NotFound, + /// The enclosing namespace could not be determined because name resolution failed. + Unresolved(DefinitionId), +} + +/// Walks up the lexical nesting chain from the given `DefinitionId` to find +/// the nearest enclosing namespace (class, module, or singleton class). +fn enclosing_namespace(graph: &Graph, starting_id: Option<&DefinitionId>) -> EnclosingNamespace { + let mut current = starting_id; + while let Some(id) = current { + let def = graph.definitions().get(id).unwrap(); + let is_namespace_def = matches!( + def, + Definition::Class(_) | Definition::Module(_) | Definition::SingletonClass(_) + ); + + let Some(decl_id) = graph.definition_id_to_declaration_id(*id) else { + if is_namespace_def { + return EnclosingNamespace::Unresolved(*id); + } + current = def.lexical_nesting_id().as_ref(); + continue; + }; + if is_namespace_def { + return EnclosingNamespace::Found(*decl_id); + } + current = def.lexical_nesting_id().as_ref(); + } + EnclosingNamespace::NotFound +} + +/// Result of dealiasing a single level of method alias. +pub enum DealiasMethodResult { + /// The alias target is a concrete method definition. + Method(DefinitionId), + /// The alias target is another alias. + Alias(DefinitionId), +} + +/// Dereferences a `MethodAliasDefinition` by one level, returning the definitions +/// found under the aliased name. Returns an empty vector if the alias target cannot +/// be found (e.g., unresolved constant receiver or missing member). +/// +/// # Panics +/// +/// Panics if a `SelfReceiver` definition cannot be resolved to a namespace with a singleton class. +#[must_use] +pub fn dealias_method(graph: &Graph, alias: &MethodAliasDefinition) -> Vec { + let owner_id = match alias.receiver() { + Some(Receiver::SelfReceiver(def_id)) => { + let decl_id = graph.definition_id_to_declaration_id(*def_id).unwrap(); + let decl = graph.declarations().get(decl_id).unwrap(); + let ns = decl.as_namespace().unwrap(); + *ns.singleton_class().unwrap() + } + Some(Receiver::ConstantReceiver(name_id)) => { + let Some(&id) = graph.name_id_to_declaration_id(*name_id) else { + return vec![]; + }; + id + } + None => match enclosing_namespace(graph, alias.lexical_nesting_id().as_ref()) { + EnclosingNamespace::Found(id) => id, + EnclosingNamespace::NotFound | EnclosingNamespace::Unresolved(_) => return vec![], + }, + }; + + let owner_decl = graph.declarations().get(&owner_id).unwrap(); + let ns = owner_decl.as_namespace().unwrap(); + + let Some(&method_decl_id) = ns.member(alias.old_name_str_id()) else { + return vec![]; + }; + let method_decl = graph.declarations().get(&method_decl_id).unwrap(); + assert!(matches!(method_decl, Declaration::Method(_))); + + method_decl + .definitions() + .iter() + .filter_map(|def_id| match graph.definitions().get(def_id) { + Some(Definition::Method(_)) => Some(DealiasMethodResult::Method(*def_id)), + Some(Definition::MethodAlias(_)) => Some(DealiasMethodResult::Alias(*def_id)), + _ => None, + }) + .collect() +} + #[cfg(test)] mod tests { use std::str::FromStr; @@ -1694,6 +1787,123 @@ mod tests { assert!(!candidates.iter().any(|c| matches!(c, CompletionCandidate::Keyword(_)))); } + fn get_method_alias_def<'a>(graph: &'a Graph, decl_name: &str) -> &'a MethodAliasDefinition { + let decl = graph.declarations().get(&DeclarationId::from(decl_name)).unwrap(); + for def_id in decl.definitions() { + if let Some(Definition::MethodAlias(alias)) = graph.definitions().get(def_id) { + return alias; + } + } + panic!("No MethodAliasDefinition found for {decl_name}"); + } + + #[test] + fn dealias_method_basic() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///foo.rb", + " + class Foo + def foo(a, b); end + alias bar foo + end + ", + ); + context.resolve(); + + let alias = get_method_alias_def(context.graph(), "Foo#bar()"); + let results = dealias_method(context.graph(), alias); + assert_eq!(results.len(), 1); + assert!(matches!(results[0], DealiasMethodResult::Method(_))); + } + + #[test] + fn dealias_method_chained() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///foo.rb", + " + class Foo + def foo(x); end + alias bar foo + alias baz bar + end + ", + ); + context.resolve(); + + // baz -> bar: one level returns the alias to bar + let alias = get_method_alias_def(context.graph(), "Foo#baz()"); + let results = dealias_method(context.graph(), alias); + assert_eq!(results.len(), 1); + assert!(matches!(results[0], DealiasMethodResult::Alias(_))); + + // bar -> foo: one more level returns the method definition + let alias = get_method_alias_def(context.graph(), "Foo#bar()"); + let results = dealias_method(context.graph(), alias); + assert_eq!(results.len(), 1); + assert!(matches!(results[0], DealiasMethodResult::Method(_))); + } + + #[test] + fn dealias_method_unresolved() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///foo.rb", + " + class Foo + alias bar nonexistent + end + ", + ); + context.resolve(); + + let alias = get_method_alias_def(context.graph(), "Foo#bar()"); + let results = dealias_method(context.graph(), alias); + assert!(results.is_empty()); + } + + #[test] + fn dealias_method_multiple_definitions() { + let mut context = GraphTest::new(); + context.index_uri( + "file:///foo.rb", + " + class Foo + def foo(a); end + end + ", + ); + context.index_uri( + "file:///foo2.rb", + " + class Foo + alias foo baz # another alias definition of #foo() + alias bar foo + end + ", + ); + context.resolve(); + + let alias = get_method_alias_def(context.graph(), "Foo#bar()"); + let results = dealias_method(context.graph(), alias); + assert_eq!(results.len(), 2); + assert_eq!( + results + .iter() + .filter(|r| matches!(r, DealiasMethodResult::Method(_))) + .count(), + 1 + ); + assert_eq!( + results + .iter() + .filter(|r| matches!(r, DealiasMethodResult::Alias(_))) + .count(), + 1 + ); + } + #[test] fn method_call_completion_excludes_keywords() { let mut context = GraphTest::new(); diff --git a/test/definition_test.rb b/test/definition_test.rb index bd1698d4..f4242b86 100644 --- a/test/definition_test.rb +++ b/test/definition_test.rb @@ -573,6 +573,81 @@ def bar: (String name) -> void end end + def test_method_alias_definition_signatures + with_context do |context| + context.write!("file1.rb", <<~RUBY) + class Foo + def foo(a, b); end + alias bar foo + end + RUBY + + graph = Rubydex::Graph.new + graph.index_all(context.glob("**/*.rb")) + graph.resolve + + alias_def = graph["Foo#bar()"].definitions.first + assert_instance_of(Rubydex::MethodAliasDefinition, alias_def) + + signatures = alias_def.signatures + assert_equal(1, signatures.length) + + params = signatures.first.parameters + assert_equal(2, params.length) + assert_equal(:a, params[0].name) + assert_equal(:b, params[1].name) + + # method_definition points to the resolved MethodDefinition, not the alias + assert_instance_of(Rubydex::MethodDefinition, signatures.first.method_definition) + end + end + + def test_method_alias_definition_signatures_chained + with_context do |context| + context.write!("file1.rb", <<~RUBY) + class Foo + def foo(x); end + alias bar foo + alias baz bar + end + RUBY + + graph = Rubydex::Graph.new + graph.index_all(context.glob("**/*.rb")) + graph.resolve + + alias_def = graph["Foo#baz()"].definitions.first + assert_instance_of(Rubydex::MethodAliasDefinition, alias_def) + + signatures = alias_def.signatures + assert_equal(1, signatures.length) + + params = signatures.first.parameters + assert_equal(1, params.length) + assert_equal(:x, params[0].name) + end + end + + def test_method_alias_definition_signatures_unresolved + with_context do |context| + context.write!("file1.rb", <<~RUBY) + class Foo + alias bar nonexistent + end + RUBY + + graph = Rubydex::Graph.new + graph.index_all(context.glob("**/*.rb")) + graph.resolve + + alias_def = graph["Foo#bar()"].definitions.first + assert_instance_of(Rubydex::MethodAliasDefinition, alias_def) + + signatures = alias_def.signatures + assert_empty(signatures) + end + end + private # Comment locations on Windows include the carriage return. This means that the end column is off by one when compared From d7681ebd0b8328f0bcf272a4fae9e1ced611dcf8 Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Tue, 31 Mar 2026 17:24:36 +0900 Subject: [PATCH 04/27] Find inherited methods --- rust/rubydex/src/query.rs | 146 +++++++++++++++++++++++++++++++++++++- 1 file changed, 143 insertions(+), 3 deletions(-) diff --git a/rust/rubydex/src/query.rs b/rust/rubydex/src/query.rs index 5941477e..e443d100 100644 --- a/rust/rubydex/src/query.rs +++ b/rust/rubydex/src/query.rs @@ -542,10 +542,9 @@ pub fn dealias_method(graph: &Graph, alias: &MethodAliasDefinition) -> Vec Vec Option { + let ns = graph.declarations().get(&namespace_id)?.as_namespace()?; + + if let Some(&decl_id) = ns.member(&str_id) { + return Some(decl_id); + } + + for ancestor in ns.ancestors() { + if let Ancestor::Complete(ancestor_id) = ancestor { + let ancestor_ns = graph.declarations().get(ancestor_id)?.as_namespace()?; + if let Some(&decl_id) = ancestor_ns.member(&str_id) { + return Some(decl_id); + } + } + } + + None +} + #[cfg(test)] mod tests { use std::str::FromStr; @@ -1904,6 +1928,122 @@ mod tests { ); } + #[test] + fn dealias_method_inherited() { + let mut context = GraphTest::new(); + context.index_uri("file:///foo.rb", " + class Parent + def foo(a); end + end + class Child < Parent + alias bar foo + end + "); + context.resolve(); + + let alias = get_method_alias_def(context.graph(), "Child#bar()"); + let results = dealias_method(context.graph(), alias); + assert_eq!(results.len(), 1); + assert!(matches!(results[0], DealiasMethodResult::Method(_))); + } + + #[test] + fn find_member_in_ancestors_direct() { + let mut context = GraphTest::new(); + context.index_uri("file:///foo.rb", " + class Foo + def bar; end + end + "); + context.resolve(); + + let result = find_member_in_ancestors( + context.graph(), + DeclarationId::from("Foo"), + StringId::from("bar()"), + ); + assert_eq!(result, Some(DeclarationId::from("Foo#bar()"))); + } + + #[test] + fn find_member_in_ancestors_inherited() { + let mut context = GraphTest::new(); + context.index_uri("file:///foo.rb", " + class Parent + def foo; end + end + class Child < Parent + end + "); + context.resolve(); + + let result = find_member_in_ancestors( + context.graph(), + DeclarationId::from("Child"), + StringId::from("foo()"), + ); + assert_eq!(result, Some(DeclarationId::from("Parent#foo()"))); + } + + #[test] + fn find_member_in_ancestors_overridden() { + let mut context = GraphTest::new(); + context.index_uri("file:///foo.rb", " + class Parent + def foo; end + end + class Child < Parent + def foo; end + end + "); + context.resolve(); + + let result = find_member_in_ancestors( + context.graph(), + DeclarationId::from("Child"), + StringId::from("foo()"), + ); + assert_eq!(result, Some(DeclarationId::from("Child#foo()"))); + } + + #[test] + fn find_member_in_ancestors_not_found() { + let mut context = GraphTest::new(); + context.index_uri("file:///foo.rb", " + class Foo + end + "); + context.resolve(); + + let result = find_member_in_ancestors( + context.graph(), + DeclarationId::from("Foo"), + StringId::from("nonexistent()"), + ); + assert_eq!(result, None); + } + + #[test] + fn find_member_in_ancestors_via_module() { + let mut context = GraphTest::new(); + context.index_uri("file:///foo.rb", " + module Greetable + def greet; end + end + class Foo + include Greetable + end + "); + context.resolve(); + + let result = find_member_in_ancestors( + context.graph(), + DeclarationId::from("Foo"), + StringId::from("greet()"), + ); + assert_eq!(result, Some(DeclarationId::from("Greetable#greet()"))); + } + #[test] fn method_call_completion_excludes_keywords() { let mut context = GraphTest::new(); From a8f4c85e208e9e8c05bc1f98a43effa838b1ca16 Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Wed, 1 Apr 2026 16:27:21 +0900 Subject: [PATCH 05/27] Delete `Signature#method_definition` --- ext/rubydex/definition.c | 4 ++-- ext/rubydex/signature.c | 15 ++------------- ext/rubydex/signature.h | 4 +--- lib/rubydex/signature.rb | 8 ++------ test/definition_test.rb | 3 --- 5 files changed, 7 insertions(+), 27 deletions(-) diff --git a/ext/rubydex/definition.c b/ext/rubydex/definition.c index 538eeb93..775a3128 100644 --- a/ext/rubydex/definition.c +++ b/ext/rubydex/definition.c @@ -249,7 +249,7 @@ static VALUE rdxr_method_definition_signatures(VALUE self) { TypedData_Get_Struct(data->graph_obj, void *, &graph_type, graph); SignatureArray *arr = rdx_definition_signatures(graph, data->id); - return rdxi_signatures_to_ruby(arr, data->graph_obj, self); + return rdxi_signatures_to_ruby(arr); } // MethodAliasDefinition#signatures -> [Rubydex::Signature] @@ -261,7 +261,7 @@ static VALUE rdxr_method_alias_definition_signatures(VALUE self) { TypedData_Get_Struct(data->graph_obj, void *, &graph_type, graph); SignatureArray *arr = rdx_method_alias_definition_signatures(graph, data->id); - return rdxi_signatures_to_ruby(arr, data->graph_obj, Qnil); + return rdxi_signatures_to_ruby(arr); } void rdxi_initialize_definition(VALUE mod) { diff --git a/ext/rubydex/signature.c b/ext/rubydex/signature.c index 68a2b8c1..78504cce 100644 --- a/ext/rubydex/signature.c +++ b/ext/rubydex/signature.c @@ -1,6 +1,4 @@ #include "signature.h" -#include "definition.h" -#include "handle.h" #include "location.h" VALUE cSignature; @@ -28,7 +26,7 @@ static VALUE parameter_class_for_kind(ParameterKind kind) { } } -VALUE rdxi_signatures_to_ruby(SignatureArray *arr, VALUE graph_obj, VALUE default_method_def) { +VALUE rdxi_signatures_to_ruby(SignatureArray *arr) { if (arr == NULL || arr->len == 0) { if (arr != NULL) { rdx_definition_signatures_free(arr); @@ -41,14 +39,6 @@ VALUE rdxi_signatures_to_ruby(SignatureArray *arr, VALUE graph_obj, VALUE defaul for (size_t i = 0; i < arr->len; i++) { SignatureEntry sig_entry = arr->items[i]; - VALUE method_def; - if (default_method_def != Qnil) { - method_def = default_method_def; - } else { - VALUE def_argv[] = {graph_obj, ULL2NUM(sig_entry.definition_id)}; - method_def = rb_class_new_instance(2, def_argv, cMethodDefinition); - } - VALUE parameters = rb_ary_new_capa((long)sig_entry.parameters_len); for (size_t j = 0; j < sig_entry.parameters_len; j++) { ParameterEntry param_entry = sig_entry.parameters[j]; @@ -62,8 +52,7 @@ VALUE rdxi_signatures_to_ruby(SignatureArray *arr, VALUE graph_obj, VALUE defaul rb_ary_push(parameters, param); } - VALUE sig_argv[] = {parameters, method_def}; - VALUE signature = rb_class_new_instance(2, sig_argv, cSignature); + VALUE signature = rb_class_new_instance(1, ¶meters, cSignature); rb_ary_push(signatures, signature); } diff --git a/ext/rubydex/signature.h b/ext/rubydex/signature.h index b4e828bd..f5e099b4 100644 --- a/ext/rubydex/signature.h +++ b/ext/rubydex/signature.h @@ -16,10 +16,8 @@ extern VALUE cBlockParameter; extern VALUE cForwardParameter; // Convert a SignatureArray into a Ruby array of Rubydex::Signature objects. -// If default_method_def is not Qnil, it is used as method_definition for all signatures. -// Otherwise, a new MethodDefinition handle is built from each SignatureEntry's definition_id. // The SignatureArray is freed after conversion. -VALUE rdxi_signatures_to_ruby(SignatureArray *arr, VALUE graph_obj, VALUE default_method_def); +VALUE rdxi_signatures_to_ruby(SignatureArray *arr); void rdxi_initialize_signature(VALUE mRubydex); diff --git a/lib/rubydex/signature.rb b/lib/rubydex/signature.rb index 31b941fa..a09e674e 100644 --- a/lib/rubydex/signature.rb +++ b/lib/rubydex/signature.rb @@ -19,13 +19,9 @@ def initialize(name, location) #: Array[Parameter] attr_reader :parameters - #: MethodDefinition - attr_reader :method_definition - - #: (Array[Parameter], MethodDefinition) -> void - def initialize(parameters, method_definition) + #: (Array[Parameter]) -> void + def initialize(parameters) @parameters = parameters - @method_definition = method_definition end end end diff --git a/test/definition_test.rb b/test/definition_test.rb index f4242b86..2cc46c1c 100644 --- a/test/definition_test.rb +++ b/test/definition_test.rb @@ -382,7 +382,6 @@ def foo(a, b = 1, *c, d, e:, f: 1, **g, &h); end sig = signatures.first assert_instance_of(Rubydex::Signature, sig) - assert_same(method_def, sig.method_definition) params = sig.parameters assert_equal(8, params.length) @@ -597,8 +596,6 @@ def foo(a, b); end assert_equal(:a, params[0].name) assert_equal(:b, params[1].name) - # method_definition points to the resolved MethodDefinition, not the alias - assert_instance_of(Rubydex::MethodDefinition, signatures.first.method_definition) end end From 7d9657aaaf768b3068c2746b9dee9ffafdadb2e7 Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Wed, 1 Apr 2026 16:37:12 +0900 Subject: [PATCH 06/27] Rename parameter subclasses --- ext/rubydex/signature.c | 11 +++++++---- ext/rubydex/signature.h | 3 ++- rust/rubydex-sys/src/definition_api.rs | 16 +++++++++------- test/definition_test.rb | 4 ++-- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/ext/rubydex/signature.c b/ext/rubydex/signature.c index 78504cce..206f22d9 100644 --- a/ext/rubydex/signature.c +++ b/ext/rubydex/signature.c @@ -6,22 +6,24 @@ VALUE cParameter; VALUE cPositionalParameter; VALUE cOptionalPositionalParameter; VALUE cRestPositionalParameter; +VALUE cPostParameter; VALUE cKeywordParameter; VALUE cOptionalKeywordParameter; VALUE cRestKeywordParameter; -VALUE cBlockParameter; VALUE cForwardParameter; +VALUE cBlockParameter; static VALUE parameter_class_for_kind(ParameterKind kind) { switch (kind) { case ParameterKind_RequiredPositional: return cPositionalParameter; case ParameterKind_OptionalPositional: return cOptionalPositionalParameter; - case ParameterKind_Rest: return cRestPositionalParameter; + case ParameterKind_RestPositional: return cRestPositionalParameter; + case ParameterKind_Post: return cPostParameter; case ParameterKind_RequiredKeyword: return cKeywordParameter; case ParameterKind_OptionalKeyword: return cOptionalKeywordParameter; case ParameterKind_RestKeyword: return cRestKeywordParameter; - case ParameterKind_Block: return cBlockParameter; case ParameterKind_Forward: return cForwardParameter; + case ParameterKind_Block: return cBlockParameter; default: rb_raise(rb_eRuntimeError, "Unknown ParameterKind: %d", kind); } } @@ -68,9 +70,10 @@ void rdxi_initialize_signature(VALUE mRubydex) { cPositionalParameter = rb_define_class_under(cSignature, "PositionalParameter", cParameter); cOptionalPositionalParameter = rb_define_class_under(cSignature, "OptionalPositionalParameter", cParameter); cRestPositionalParameter = rb_define_class_under(cSignature, "RestPositionalParameter", cParameter); + cPostParameter = rb_define_class_under(cSignature, "PostParameter", cParameter); cKeywordParameter = rb_define_class_under(cSignature, "KeywordParameter", cParameter); cOptionalKeywordParameter = rb_define_class_under(cSignature, "OptionalKeywordParameter", cParameter); cRestKeywordParameter = rb_define_class_under(cSignature, "RestKeywordParameter", cParameter); - cBlockParameter = rb_define_class_under(cSignature, "BlockParameter", cParameter); cForwardParameter = rb_define_class_under(cSignature, "ForwardParameter", cParameter); + cBlockParameter = rb_define_class_under(cSignature, "BlockParameter", cParameter); } diff --git a/ext/rubydex/signature.h b/ext/rubydex/signature.h index f5e099b4..156e4fb6 100644 --- a/ext/rubydex/signature.h +++ b/ext/rubydex/signature.h @@ -9,11 +9,12 @@ extern VALUE cParameter; extern VALUE cPositionalParameter; extern VALUE cOptionalPositionalParameter; extern VALUE cRestPositionalParameter; +extern VALUE cPostParameter; extern VALUE cKeywordParameter; extern VALUE cOptionalKeywordParameter; extern VALUE cRestKeywordParameter; -extern VALUE cBlockParameter; extern VALUE cForwardParameter; +extern VALUE cBlockParameter; // Convert a SignatureArray into a Ruby array of Rubydex::Signature objects. // The SignatureArray is freed after conversion. diff --git a/rust/rubydex-sys/src/definition_api.rs b/rust/rubydex-sys/src/definition_api.rs index 1d8639b5..e4484250 100644 --- a/rust/rubydex-sys/src/definition_api.rs +++ b/rust/rubydex-sys/src/definition_api.rs @@ -448,19 +448,21 @@ pub unsafe extern "C" fn rdx_definition_mixins(pointer: GraphPointer, definition pub enum ParameterKind { RequiredPositional = 0, OptionalPositional = 1, - Rest = 2, - RequiredKeyword = 3, - OptionalKeyword = 4, - RestKeyword = 5, - Block = 6, + RestPositional = 2, + Post = 3, + RequiredKeyword = 4, + OptionalKeyword = 5, + RestKeyword = 6, Forward = 7, + Block = 8, } fn map_parameter_kind(param: &Parameter) -> ParameterKind { match param { - Parameter::RequiredPositional(_) | Parameter::Post(_) => ParameterKind::RequiredPositional, + Parameter::RequiredPositional(_) => ParameterKind::RequiredPositional, + Parameter::Post(_) => ParameterKind::Post, Parameter::OptionalPositional(_) => ParameterKind::OptionalPositional, - Parameter::RestPositional(_) => ParameterKind::Rest, + Parameter::RestPositional(_) => ParameterKind::RestPositional, Parameter::RequiredKeyword(_) => ParameterKind::RequiredKeyword, Parameter::OptionalKeyword(_) => ParameterKind::OptionalKeyword, Parameter::RestKeyword(_) => ParameterKind::RestKeyword, diff --git a/test/definition_test.rb b/test/definition_test.rb index 2cc46c1c..634dbe92 100644 --- a/test/definition_test.rb +++ b/test/definition_test.rb @@ -400,7 +400,7 @@ def foo(a, b = 1, *c, d, e:, f: 1, **g, &h); end assert_equal(:c, params[2].name) assert_equal("#{path}:1:20-1:21", params[2].location.to_display.to_s) # c - assert_instance_of(Rubydex::Signature::PositionalParameter, params[3]) + assert_instance_of(Rubydex::Signature::PostParameter, params[3]) assert_equal(:d, params[3].name) assert_equal("#{path}:1:23-1:24", params[3].location.to_display.to_s) # d @@ -495,7 +495,7 @@ def bar: (String a, ?String b, *String c, String d, name: String, ?mode: String, assert_instance_of(Rubydex::Signature::RestPositionalParameter, params[2]) assert_equal(:c, params[2].name) assert_equal("#{path}:2:42-2:43", params[2].location.to_display.to_s) # c - assert_instance_of(Rubydex::Signature::PositionalParameter, params[3]) + assert_instance_of(Rubydex::Signature::PostParameter, params[3]) assert_equal(:d, params[3].name) assert_equal("#{path}:2:52-2:53", params[3].location.to_display.to_s) # d assert_instance_of(Rubydex::Signature::KeywordParameter, params[4]) From 555cbed1965bd1026de1c35868dc91a78f9711a0 Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Wed, 1 Apr 2026 17:19:29 +0900 Subject: [PATCH 07/27] Extract signature_api.rs --- rust/rubydex-sys/src/definition_api.rs | 227 +----------------------- rust/rubydex-sys/src/lib.rs | 1 + rust/rubydex-sys/src/signature_api.rs | 234 +++++++++++++++++++++++++ 3 files changed, 236 insertions(+), 226 deletions(-) create mode 100644 rust/rubydex-sys/src/signature_api.rs diff --git a/rust/rubydex-sys/src/definition_api.rs b/rust/rubydex-sys/src/definition_api.rs index e4484250..b96f85af 100644 --- a/rust/rubydex-sys/src/definition_api.rs +++ b/rust/rubydex-sys/src/definition_api.rs @@ -4,7 +4,7 @@ use crate::graph_api::{GraphPointer, with_graph}; use crate::location_api::{Location, create_location_for_uri_and_offset}; use crate::reference_api::CConstantReference; use libc::c_char; -use rubydex::model::definitions::{Definition, Mixin, Parameter}; +use rubydex::model::definitions::{Definition, Mixin}; use rubydex::model::ids::DefinitionId; use std::ffi::CString; use std::ptr; @@ -441,228 +441,3 @@ pub unsafe extern "C" fn rdx_definition_mixins(pointer: GraphPointer, definition MixinsIter::new(entries.into_boxed_slice()) }) } - -/// C-compatible enum representing the kind of a parameter. -#[repr(C)] -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum ParameterKind { - RequiredPositional = 0, - OptionalPositional = 1, - RestPositional = 2, - Post = 3, - RequiredKeyword = 4, - OptionalKeyword = 5, - RestKeyword = 6, - Forward = 7, - Block = 8, -} - -fn map_parameter_kind(param: &Parameter) -> ParameterKind { - match param { - Parameter::RequiredPositional(_) => ParameterKind::RequiredPositional, - Parameter::Post(_) => ParameterKind::Post, - Parameter::OptionalPositional(_) => ParameterKind::OptionalPositional, - Parameter::RestPositional(_) => ParameterKind::RestPositional, - Parameter::RequiredKeyword(_) => ParameterKind::RequiredKeyword, - Parameter::OptionalKeyword(_) => ParameterKind::OptionalKeyword, - Parameter::RestKeyword(_) => ParameterKind::RestKeyword, - Parameter::Block(_) => ParameterKind::Block, - Parameter::Forward(_) => ParameterKind::Forward, - } -} - -/// C-compatible struct representing a single parameter with its name, kind, and location. -#[repr(C)] -pub struct ParameterEntry { - pub name: *const c_char, - pub kind: ParameterKind, - pub location: *mut Location, -} - -/// C-compatible struct representing a single method signature (a list of parameters). -#[repr(C)] -pub struct SignatureEntry { - pub definition_id: u64, - pub parameters: *mut ParameterEntry, - pub parameters_len: usize, -} - -/// C-compatible array of signatures. -#[repr(C)] -pub struct SignatureArray { - pub items: *mut SignatureEntry, - pub len: usize, -} - -/// Returns a newly allocated array of signatures for the given method definition id. -/// Returns NULL if the definition is not a method definition. -/// Caller must free the returned pointer with `rdx_definition_signatures_free`. -/// -/// # Safety -/// - `pointer` must be a valid pointer previously returned by `rdx_graph_new`. -/// - `definition_id` must be a valid definition id. -/// -/// # Panics -/// This function will panic if a definition or document cannot be found. -#[unsafe(no_mangle)] -pub unsafe extern "C" fn rdx_definition_signatures(pointer: GraphPointer, definition_id: u64) -> *mut SignatureArray { - with_graph(pointer, |graph| { - let def_id = DefinitionId::new(definition_id); - let Some(Definition::Method(method_def)) = graph.definitions().get(&def_id) else { - return ptr::null_mut(); - }; - - let mut sig_entries: Vec = Vec::new(); - collect_method_signatures(graph, method_def, definition_id, &mut sig_entries); - - let mut boxed = sig_entries.into_boxed_slice(); - let len = boxed.len(); - let items_ptr = boxed.as_mut_ptr(); - std::mem::forget(boxed); - - Box::into_raw(Box::new(SignatureArray { items: items_ptr, len })) - }) -} - -/// Helper: build signature entries from a `MethodDefinition` and append them to the output vector. -fn collect_method_signatures( - graph: &rubydex::model::graph::Graph, - method_def: &rubydex::model::definitions::MethodDefinition, - definition_id: u64, - out: &mut Vec, -) { - let uri_id = *method_def.uri_id(); - let document = graph.documents().get(&uri_id).expect("document should exist"); - - for sig in method_def.signatures().as_slice() { - let mut param_entries = sig - .iter() - .map(|param| { - let param_struct = param.inner(); - let name = graph - .strings() - .get(param_struct.str()) - .expect("parameter name string should exist"); - let name_str = CString::new(name.as_str()).unwrap().into_raw().cast_const(); - - ParameterEntry { - name: name_str, - kind: map_parameter_kind(param), - location: create_location_for_uri_and_offset(graph, document, param_struct.offset()), - } - }) - .collect::>() - .into_boxed_slice(); - - let parameters_len = param_entries.len(); - let parameters_ptr = param_entries.as_mut_ptr(); - std::mem::forget(param_entries); - - out.push(SignatureEntry { - definition_id, - parameters: parameters_ptr, - parameters_len, - }); - } -} - -/// Returns signatures for a `MethodAliasDefinition` by following the alias chain. -/// Returns NULL if the definition is not a method alias or the chain cannot be resolved. -/// -/// # Safety -/// - `pointer` must be a valid pointer previously returned by `rdx_graph_new`. -/// - `definition_id` must be a valid definition id. -#[unsafe(no_mangle)] -pub unsafe extern "C" fn rdx_method_alias_definition_signatures( - pointer: GraphPointer, - definition_id: u64, -) -> *mut SignatureArray { - with_graph(pointer, |graph| { - let def_id = DefinitionId::new(definition_id); - let Some(Definition::MethodAlias(alias)) = graph.definitions().get(&def_id) else { - return ptr::null_mut(); - }; - - // Follow the alias chain until we find method definitions - let mut current_results = rubydex::query::dealias_method(graph, alias); - let mut visited = std::collections::HashSet::new(); - visited.insert(def_id); - - loop { - let mut sig_entries: Vec = Vec::new(); - let mut next_aliases = Vec::new(); - - for result in ¤t_results { - match result { - rubydex::query::DealiasMethodResult::Method(id) => { - if let Some(Definition::Method(method_def)) = graph.definitions().get(id) { - collect_method_signatures(graph, method_def, id.get(), &mut sig_entries); - } - } - rubydex::query::DealiasMethodResult::Alias(id) => { - if visited.insert(*id) - && let Some(Definition::MethodAlias(next_alias)) = graph.definitions().get(id) - { - next_aliases.extend(rubydex::query::dealias_method(graph, next_alias)); - } - } - } - } - - if !sig_entries.is_empty() { - let mut boxed = sig_entries.into_boxed_slice(); - let len = boxed.len(); - let items_ptr = boxed.as_mut_ptr(); - std::mem::forget(boxed); - return Box::into_raw(Box::new(SignatureArray { items: items_ptr, len })); - } - - if next_aliases.is_empty() { - return ptr::null_mut(); - } - - current_results = next_aliases; - } - }) -} - -/// Frees a `SignatureArray` previously returned by `rdx_definition_signatures`. -/// -/// # Safety -/// - `ptr` must be a valid pointer previously returned by `rdx_definition_signatures`. -/// - `ptr` must not be used after being freed. -#[unsafe(no_mangle)] -pub unsafe extern "C" fn rdx_definition_signatures_free(ptr: *mut SignatureArray) { - if ptr.is_null() { - return; - } - - // Take ownership of the SignatureArray - let arr = unsafe { Box::from_raw(ptr) }; - - if !arr.items.is_null() && arr.len > 0 { - // Reconstruct the boxed slice so we can drop it after freeing inner allocations - let slice_ptr = ptr::slice_from_raw_parts_mut(arr.items, arr.len); - let mut sig_slice: Box<[SignatureEntry]> = unsafe { Box::from_raw(slice_ptr) }; - - for sig_entry in &mut sig_slice { - if !sig_entry.parameters.is_null() && sig_entry.parameters_len > 0 { - let param_slice_ptr = ptr::slice_from_raw_parts_mut(sig_entry.parameters, sig_entry.parameters_len); - let mut param_slice: Box<[ParameterEntry]> = unsafe { Box::from_raw(param_slice_ptr) }; - - for param_entry in &mut param_slice { - if !param_entry.name.is_null() { - let _ = unsafe { CString::from_raw(param_entry.name.cast_mut()) }; - } - if !param_entry.location.is_null() { - unsafe { crate::location_api::rdx_location_free(param_entry.location) }; - param_entry.location = ptr::null_mut(); - } - } - // param_slice is dropped here, freeing the parameters buffer - } - } - // sig_slice is dropped here, freeing the signatures buffer - } - // arr is dropped here -} diff --git a/rust/rubydex-sys/src/lib.rs b/rust/rubydex-sys/src/lib.rs index 202dc973..b5108689 100644 --- a/rust/rubydex-sys/src/lib.rs +++ b/rust/rubydex-sys/src/lib.rs @@ -76,4 +76,5 @@ pub mod graph_api; pub mod location_api; pub mod name_api; pub mod reference_api; +pub mod signature_api; pub mod utils; diff --git a/rust/rubydex-sys/src/signature_api.rs b/rust/rubydex-sys/src/signature_api.rs new file mode 100644 index 00000000..4afb832a --- /dev/null +++ b/rust/rubydex-sys/src/signature_api.rs @@ -0,0 +1,234 @@ +//! This file provides the C API for method signature accessors + +use crate::graph_api::{GraphPointer, with_graph}; +use crate::location_api::{Location, create_location_for_uri_and_offset}; +use libc::c_char; +use rubydex::model::definitions::{Definition, Parameter}; +use rubydex::model::ids::DefinitionId; +use std::ffi::CString; +use std::ptr; + +/// C-compatible enum representing the kind of a parameter. +#[repr(C)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum ParameterKind { + RequiredPositional = 0, + OptionalPositional = 1, + RestPositional = 2, + Post = 3, + RequiredKeyword = 4, + OptionalKeyword = 5, + RestKeyword = 6, + Forward = 7, + Block = 8, +} + +fn map_parameter_kind(param: &Parameter) -> ParameterKind { + match param { + Parameter::RequiredPositional(_) => ParameterKind::RequiredPositional, + Parameter::Post(_) => ParameterKind::Post, + Parameter::OptionalPositional(_) => ParameterKind::OptionalPositional, + Parameter::RestPositional(_) => ParameterKind::RestPositional, + Parameter::RequiredKeyword(_) => ParameterKind::RequiredKeyword, + Parameter::OptionalKeyword(_) => ParameterKind::OptionalKeyword, + Parameter::RestKeyword(_) => ParameterKind::RestKeyword, + Parameter::Block(_) => ParameterKind::Block, + Parameter::Forward(_) => ParameterKind::Forward, + } +} + +/// C-compatible struct representing a single parameter with its name, kind, and location. +#[repr(C)] +pub struct ParameterEntry { + pub name: *const c_char, + pub kind: ParameterKind, + pub location: *mut Location, +} + +/// C-compatible struct representing a single method signature (a list of parameters). +#[repr(C)] +pub struct SignatureEntry { + pub definition_id: u64, + pub parameters: *mut ParameterEntry, + pub parameters_len: usize, +} + +/// C-compatible array of signatures. +#[repr(C)] +pub struct SignatureArray { + pub items: *mut SignatureEntry, + pub len: usize, +} + +/// Returns a newly allocated array of signatures for the given method definition id. +/// Returns NULL if the definition is not a method definition. +/// Caller must free the returned pointer with `rdx_definition_signatures_free`. +/// +/// # Safety +/// - `pointer` must be a valid pointer previously returned by `rdx_graph_new`. +/// - `definition_id` must be a valid definition id. +/// +/// # Panics +/// This function will panic if a definition or document cannot be found. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rdx_definition_signatures(pointer: GraphPointer, definition_id: u64) -> *mut SignatureArray { + with_graph(pointer, |graph| { + let def_id = DefinitionId::new(definition_id); + let Some(Definition::Method(method_def)) = graph.definitions().get(&def_id) else { + return ptr::null_mut(); + }; + + let mut sig_entries: Vec = Vec::new(); + collect_method_signatures(graph, method_def, definition_id, &mut sig_entries); + + let mut boxed = sig_entries.into_boxed_slice(); + let len = boxed.len(); + let items_ptr = boxed.as_mut_ptr(); + std::mem::forget(boxed); + + Box::into_raw(Box::new(SignatureArray { items: items_ptr, len })) + }) +} + +/// Helper: build signature entries from a `MethodDefinition` and append them to the output vector. +fn collect_method_signatures( + graph: &rubydex::model::graph::Graph, + method_def: &rubydex::model::definitions::MethodDefinition, + definition_id: u64, + out: &mut Vec, +) { + let uri_id = *method_def.uri_id(); + let document = graph.documents().get(&uri_id).expect("document should exist"); + + for sig in method_def.signatures().as_slice() { + let mut param_entries = sig + .iter() + .map(|param| { + let param_struct = param.inner(); + let name = graph + .strings() + .get(param_struct.str()) + .expect("parameter name string should exist"); + let name_str = CString::new(name.as_str()).unwrap().into_raw().cast_const(); + + ParameterEntry { + name: name_str, + kind: map_parameter_kind(param), + location: create_location_for_uri_and_offset(graph, document, param_struct.offset()), + } + }) + .collect::>() + .into_boxed_slice(); + + let parameters_len = param_entries.len(); + let parameters_ptr = param_entries.as_mut_ptr(); + std::mem::forget(param_entries); + + out.push(SignatureEntry { + definition_id, + parameters: parameters_ptr, + parameters_len, + }); + } +} + +/// Returns signatures for a `MethodAliasDefinition` by following the alias chain. +/// Returns NULL if the definition is not a method alias or the chain cannot be resolved. +/// +/// # Safety +/// - `pointer` must be a valid pointer previously returned by `rdx_graph_new`. +/// - `definition_id` must be a valid definition id. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rdx_method_alias_definition_signatures( + pointer: GraphPointer, + definition_id: u64, +) -> *mut SignatureArray { + with_graph(pointer, |graph| { + let def_id = DefinitionId::new(definition_id); + let Some(Definition::MethodAlias(alias)) = graph.definitions().get(&def_id) else { + return ptr::null_mut(); + }; + + // Follow the alias chain until we find method definitions + let mut current_results = rubydex::query::dealias_method(graph, alias); + let mut visited = std::collections::HashSet::new(); + visited.insert(def_id); + + loop { + let mut sig_entries: Vec = Vec::new(); + let mut next_aliases = Vec::new(); + + for result in ¤t_results { + match result { + rubydex::query::DealiasMethodResult::Method(id) => { + if let Some(Definition::Method(method_def)) = graph.definitions().get(id) { + collect_method_signatures(graph, method_def, id.get(), &mut sig_entries); + } + } + rubydex::query::DealiasMethodResult::Alias(id) => { + if visited.insert(*id) + && let Some(Definition::MethodAlias(next_alias)) = graph.definitions().get(id) + { + next_aliases.extend(rubydex::query::dealias_method(graph, next_alias)); + } + } + } + } + + if !sig_entries.is_empty() { + let mut boxed = sig_entries.into_boxed_slice(); + let len = boxed.len(); + let items_ptr = boxed.as_mut_ptr(); + std::mem::forget(boxed); + return Box::into_raw(Box::new(SignatureArray { items: items_ptr, len })); + } + + if next_aliases.is_empty() { + return ptr::null_mut(); + } + + current_results = next_aliases; + } + }) +} + +/// Frees a `SignatureArray` previously returned by `rdx_definition_signatures`. +/// +/// # Safety +/// - `ptr` must be a valid pointer previously returned by `rdx_definition_signatures`. +/// - `ptr` must not be used after being freed. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rdx_definition_signatures_free(ptr: *mut SignatureArray) { + if ptr.is_null() { + return; + } + + // Take ownership of the SignatureArray + let arr = unsafe { Box::from_raw(ptr) }; + + if !arr.items.is_null() && arr.len > 0 { + // Reconstruct the boxed slice so we can drop it after freeing inner allocations + let slice_ptr = ptr::slice_from_raw_parts_mut(arr.items, arr.len); + let mut sig_slice: Box<[SignatureEntry]> = unsafe { Box::from_raw(slice_ptr) }; + + for sig_entry in &mut sig_slice { + if !sig_entry.parameters.is_null() && sig_entry.parameters_len > 0 { + let param_slice_ptr = ptr::slice_from_raw_parts_mut(sig_entry.parameters, sig_entry.parameters_len); + let mut param_slice: Box<[ParameterEntry]> = unsafe { Box::from_raw(param_slice_ptr) }; + + for param_entry in &mut param_slice { + if !param_entry.name.is_null() { + let _ = unsafe { CString::from_raw(param_entry.name.cast_mut()) }; + } + if !param_entry.location.is_null() { + unsafe { crate::location_api::rdx_location_free(param_entry.location) }; + param_entry.location = ptr::null_mut(); + } + } + // param_slice is dropped here, freeing the parameters buffer + } + } + // sig_slice is dropped here, freeing the signatures buffer + } + // arr is dropped here +} From d625d55caf882d8a3bbd5497ad5597ed74a88bb8 Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Wed, 1 Apr 2026 17:56:42 +0900 Subject: [PATCH 08/27] Refactor `rdx_declaration_find_member` --- rust/rubydex-sys/src/declaration_api.rs | 38 ++++---------------- rust/rubydex/src/query.rs | 47 ++++++++++++++++++++----- 2 files changed, 44 insertions(+), 41 deletions(-) diff --git a/rust/rubydex-sys/src/declaration_api.rs b/rust/rubydex-sys/src/declaration_api.rs index 0b05231a..8c8decbf 100644 --- a/rust/rubydex-sys/src/declaration_api.rs +++ b/rust/rubydex-sys/src/declaration_api.rs @@ -178,42 +178,16 @@ pub unsafe extern "C" fn rdx_declaration_find_member( with_graph(pointer, |graph| { let id = DeclarationId::new(declaration_id); + let member_id = StringId::from(member_str.as_str()); - let Some(Declaration::Namespace(decl)) = graph.declarations().get(&id) else { + let Some(member_decl_id) = + rubydex::query::find_member_in_ancestors(graph, id, member_id, only_inherited) + else { return ptr::null(); }; - let member_id = StringId::from(member_str.as_str()); - let mut found_main_namespace = false; - - decl.ancestors() - .iter() - .find_map(|ancestor| match ancestor { - Ancestor::Complete(ancestor_id) => { - // When only_inherited, skip self and prepended modules - if only_inherited { - let is_self = *ancestor_id == id; - if is_self { - found_main_namespace = true; - } - if is_self || !found_main_namespace { - return None; - } - } - - let ancestor_decl = graph.declarations().get(ancestor_id).unwrap().as_namespace().unwrap(); - - if let Some(member_decl_id) = ancestor_decl.member(&member_id) { - return Some((member_decl_id, graph.declarations().get(member_decl_id).unwrap())); - } - - None - } - Ancestor::Partial(_) => None, - }) - .map_or(ptr::null(), |(member_decl_id, member_decl)| { - Box::into_raw(Box::new(CDeclaration::from_declaration(*member_decl_id, member_decl))).cast_const() - }) + let member_decl = graph.declarations().get(&member_decl_id).unwrap(); + Box::into_raw(Box::new(CDeclaration::from_declaration(member_decl_id, member_decl))).cast_const() }) } diff --git a/rust/rubydex/src/query.rs b/rust/rubydex/src/query.rs index e443d100..1b3e8564 100644 --- a/rust/rubydex/src/query.rs +++ b/rust/rubydex/src/query.rs @@ -542,7 +542,7 @@ pub fn dealias_method(graph: &Graph, alias: &MethodAliasDefinition) -> Vec Vec Option { - let ns = graph.declarations().get(&namespace_id)?.as_namespace()?; - - if let Some(&decl_id) = ns.member(&str_id) { - return Some(decl_id); - } + let ns = graph + .declarations() + .get(&namespace_id) + .expect("namespace_id should exist in declarations") + .as_namespace() + .expect("namespace_id should be a namespace declaration"); + + let ancestors: Vec<_> = ns.ancestors().iter().collect(); + + let search_start = if only_inherited { + let pos = ancestors + .iter() + .position(|a| matches!(a, Ancestor::Complete(id) if *id == namespace_id)) + .expect("namespace_id should be present in its own ancestor chain"); + pos + 1 + } else { + 0 + }; - for ancestor in ns.ancestors() { + for ancestor in &ancestors[search_start..] { if let Ancestor::Complete(ancestor_id) = ancestor { - let ancestor_ns = graph.declarations().get(ancestor_id)?.as_namespace()?; + let ancestor_decl = graph.declarations().get(ancestor_id).unwrap(); + let ancestor_ns = ancestor_decl.as_namespace().unwrap(); if let Some(&decl_id) = ancestor_ns.member(&str_id) { return Some(decl_id); } @@ -1961,6 +1985,7 @@ mod tests { context.graph(), DeclarationId::from("Foo"), StringId::from("bar()"), + false, ); assert_eq!(result, Some(DeclarationId::from("Foo#bar()"))); } @@ -1981,6 +2006,7 @@ mod tests { context.graph(), DeclarationId::from("Child"), StringId::from("foo()"), + false, ); assert_eq!(result, Some(DeclarationId::from("Parent#foo()"))); } @@ -2002,6 +2028,7 @@ mod tests { context.graph(), DeclarationId::from("Child"), StringId::from("foo()"), + false, ); assert_eq!(result, Some(DeclarationId::from("Child#foo()"))); } @@ -2019,6 +2046,7 @@ mod tests { context.graph(), DeclarationId::from("Foo"), StringId::from("nonexistent()"), + false, ); assert_eq!(result, None); } @@ -2040,6 +2068,7 @@ mod tests { context.graph(), DeclarationId::from("Foo"), StringId::from("greet()"), + false, ); assert_eq!(result, Some(DeclarationId::from("Greetable#greet()"))); } From a2aedb63de0ea8316c893f0b90de87d3dc9b550d Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Wed, 1 Apr 2026 23:12:36 +0900 Subject: [PATCH 09/27] Fix dealias_method --- ext/rubydex/signature.c | 7 - rust/rubydex-sys/src/signature_api.rs | 62 +++------ rust/rubydex/src/query.rs | 176 +++++++++++++++++++++----- 3 files changed, 166 insertions(+), 79 deletions(-) diff --git a/ext/rubydex/signature.c b/ext/rubydex/signature.c index 206f22d9..b59888ac 100644 --- a/ext/rubydex/signature.c +++ b/ext/rubydex/signature.c @@ -29,13 +29,6 @@ static VALUE parameter_class_for_kind(ParameterKind kind) { } VALUE rdxi_signatures_to_ruby(SignatureArray *arr) { - if (arr == NULL || arr->len == 0) { - if (arr != NULL) { - rdx_definition_signatures_free(arr); - } - return rb_ary_new(); - } - VALUE signatures = rb_ary_new_capa((long)arr->len); for (size_t i = 0; i < arr->len; i++) { diff --git a/rust/rubydex-sys/src/signature_api.rs b/rust/rubydex-sys/src/signature_api.rs index 4afb832a..27e24768 100644 --- a/rust/rubydex-sys/src/signature_api.rs +++ b/rust/rubydex-sys/src/signature_api.rs @@ -74,8 +74,8 @@ pub struct SignatureArray { pub unsafe extern "C" fn rdx_definition_signatures(pointer: GraphPointer, definition_id: u64) -> *mut SignatureArray { with_graph(pointer, |graph| { let def_id = DefinitionId::new(definition_id); - let Some(Definition::Method(method_def)) = graph.definitions().get(&def_id) else { - return ptr::null_mut(); + let Definition::Method(method_def) = graph.definitions().get(&def_id).expect("definition should exist") else { + panic!("expected a method definition"); }; let mut sig_entries: Vec = Vec::new(); @@ -133,11 +133,16 @@ fn collect_method_signatures( } /// Returns signatures for a `MethodAliasDefinition` by following the alias chain. -/// Returns NULL if the definition is not a method alias or the chain cannot be resolved. +/// Always returns a valid `SignatureArray` pointer (possibly with `len == 0`). +/// Errors during alias resolution (unresolved receivers, circular chains, etc.) +/// are silently ignored. /// /// # Safety /// - `pointer` must be a valid pointer previously returned by `rdx_graph_new`. /// - `definition_id` must be a valid definition id. +/// +/// # Panics +/// Panics if `definition_id` does not exist or is not a `MethodAliasDefinition`. #[unsafe(no_mangle)] pub unsafe extern "C" fn rdx_method_alias_definition_signatures( pointer: GraphPointer, @@ -145,50 +150,25 @@ pub unsafe extern "C" fn rdx_method_alias_definition_signatures( ) -> *mut SignatureArray { with_graph(pointer, |graph| { let def_id = DefinitionId::new(definition_id); - let Some(Definition::MethodAlias(alias)) = graph.definitions().get(&def_id) else { - return ptr::null_mut(); + let Definition::MethodAlias(alias) = graph.definitions().get(&def_id).expect("definition should exist") else { + panic!("expected a method alias definition"); }; - // Follow the alias chain until we find method definitions - let mut current_results = rubydex::query::dealias_method(graph, alias); - let mut visited = std::collections::HashSet::new(); - visited.insert(def_id); - - loop { - let mut sig_entries: Vec = Vec::new(); - let mut next_aliases = Vec::new(); - - for result in ¤t_results { - match result { - rubydex::query::DealiasMethodResult::Method(id) => { - if let Some(Definition::Method(method_def)) = graph.definitions().get(id) { - collect_method_signatures(graph, method_def, id.get(), &mut sig_entries); - } - } - rubydex::query::DealiasMethodResult::Alias(id) => { - if visited.insert(*id) - && let Some(Definition::MethodAlias(next_alias)) = graph.definitions().get(id) - { - next_aliases.extend(rubydex::query::dealias_method(graph, next_alias)); - } - } - } - } + let result = rubydex::query::deep_dealias_method(graph, alias, def_id); - if !sig_entries.is_empty() { - let mut boxed = sig_entries.into_boxed_slice(); - let len = boxed.len(); - let items_ptr = boxed.as_mut_ptr(); - std::mem::forget(boxed); - return Box::into_raw(Box::new(SignatureArray { items: items_ptr, len })); + let mut sig_entries: Vec = Vec::new(); + for id in &result.method_ids { + if let Some(Definition::Method(method_def)) = graph.definitions().get(id) { + collect_method_signatures(graph, method_def, id.get(), &mut sig_entries); } + } - if next_aliases.is_empty() { - return ptr::null_mut(); - } + let mut boxed = sig_entries.into_boxed_slice(); + let len = boxed.len(); + let items_ptr = boxed.as_mut_ptr(); + std::mem::forget(boxed); - current_results = next_aliases; - } + Box::into_raw(Box::new(SignatureArray { items: items_ptr, len })) }) } diff --git a/rust/rubydex/src/query.rs b/rust/rubydex/src/query.rs index 1b3e8564..d597a344 100644 --- a/rust/rubydex/src/query.rs +++ b/rust/rubydex/src/query.rs @@ -514,15 +514,33 @@ pub enum DealiasMethodResult { Alias(DefinitionId), } +/// Error type for `dealias_method`. +#[derive(Debug)] +pub enum DealiasMethodError { + /// The constant receiver (e.g., `Foo` in `def Foo.bar`) could not be resolved. + UnresolvedReceiver, + /// The enclosing namespace's name could not be resolved. + UnresolvedEnclosingNamespace(DefinitionId), + /// No enclosing namespace exists (e.g., top-level alias with no lexical nesting). + NoEnclosingNamespace, + /// The aliased method name was not found in the namespace or its ancestors. + MemberNotFound, +} + /// Dereferences a `MethodAliasDefinition` by one level, returning the definitions -/// found under the aliased name. Returns an empty vector if the alias target cannot -/// be found (e.g., unresolved constant receiver or missing member). +/// found under the aliased name. +/// +/// # Errors +/// +/// Returns a `DealiasMethodError` if the alias target cannot be resolved. /// /// # Panics /// /// Panics if a `SelfReceiver` definition cannot be resolved to a namespace with a singleton class. -#[must_use] -pub fn dealias_method(graph: &Graph, alias: &MethodAliasDefinition) -> Vec { +pub fn dealias_method( + graph: &Graph, + alias: &MethodAliasDefinition, +) -> Result, DealiasMethodError> { let owner_id = match alias.receiver() { Some(Receiver::SelfReceiver(def_id)) => { let decl_id = graph.definition_id_to_declaration_id(*def_id).unwrap(); @@ -532,25 +550,30 @@ pub fn dealias_method(graph: &Graph, alias: &MethodAliasDefinition) -> Vec { let Some(&id) = graph.name_id_to_declaration_id(*name_id) else { - return vec![]; + return Err(DealiasMethodError::UnresolvedReceiver); }; id } None => match enclosing_namespace(graph, alias.lexical_nesting_id().as_ref()) { EnclosingNamespace::Found(id) => id, - EnclosingNamespace::NotFound | EnclosingNamespace::Unresolved(_) => return vec![], + EnclosingNamespace::Unresolved(def_id) => { + return Err(DealiasMethodError::UnresolvedEnclosingNamespace(def_id)); + } + EnclosingNamespace::NotFound => { + return Err(DealiasMethodError::NoEnclosingNamespace); + } }, }; - let method_decl_id = find_member_in_ancestors(graph, owner_id, *alias.old_name_str_id(), false); - - let Some(method_decl_id) = method_decl_id else { - return vec![]; + let Some(method_decl_id) = + find_member_in_ancestors(graph, owner_id, *alias.old_name_str_id(), false) + else { + return Err(DealiasMethodError::MemberNotFound); }; let method_decl = graph.declarations().get(&method_decl_id).unwrap(); assert!(matches!(method_decl, Declaration::Method(_))); - method_decl + Ok(method_decl .definitions() .iter() .filter_map(|def_id| match graph.definitions().get(def_id) { @@ -558,7 +581,80 @@ pub fn dealias_method(graph: &Graph, alias: &MethodAliasDefinition) -> Vec Some(DealiasMethodResult::Alias(*def_id)), _ => None, }) - .collect() + .collect()) +} + +/// Result of following a `MethodAliasDefinition` chain to completion. +#[derive(Debug)] +pub struct DeepDealiasMethodResult { + /// Successfully resolved method definition IDs. + pub method_ids: Vec, + /// `DefinitionId`s where circular alias chains were detected. + pub circular_aliases: Vec, + /// Alias resolution errors (`DefinitionId` of the failing alias + the error). + pub errors: Vec<(DefinitionId, DealiasMethodError)>, +} + +/// Follows a `MethodAliasDefinition` chain to completion, returning all resolved +/// `MethodDefinition` IDs along with any errors encountered. Unlike `dealias_method` +/// which resolves one level, this function keeps following alias chains until only +/// method definitions remain. +/// +/// # Panics +/// +/// Panics if a `SelfReceiver` definition cannot be resolved to a namespace with a singleton class. +#[must_use] +pub fn deep_dealias_method( + graph: &Graph, + alias: &MethodAliasDefinition, + alias_def_id: DefinitionId, +) -> DeepDealiasMethodResult { + let mut result = DeepDealiasMethodResult { + method_ids: Vec::new(), + circular_aliases: Vec::new(), + errors: Vec::new(), + }; + + let mut current_results = match dealias_method(graph, alias) { + Ok(results) => results, + Err(err) => { + result.errors.push((alias_def_id, err)); + return result; + } + }; + + let mut visited = HashSet::new(); + visited.insert(alias_def_id); + + loop { + let mut next_aliases = Vec::new(); + + for item in ¤t_results { + match item { + DealiasMethodResult::Method(id) => { + result.method_ids.push(*id); + } + DealiasMethodResult::Alias(id) => { + if !visited.insert(*id) { + result.circular_aliases.push(*id); + continue; + } + if let Some(Definition::MethodAlias(next_alias)) = graph.definitions().get(id) { + match dealias_method(graph, next_alias) { + Ok(next) => next_aliases.extend(next), + Err(err) => result.errors.push((*id, err)), + } + } + } + } + } + + if next_aliases.is_empty() { + return result; + } + + current_results = next_aliases; + } } /// Searches for a member by `StringId` in the given namespace's ancestor chain. @@ -1860,7 +1956,7 @@ mod tests { context.resolve(); let alias = get_method_alias_def(context.graph(), "Foo#bar()"); - let results = dealias_method(context.graph(), alias); + let results = dealias_method(context.graph(), alias).unwrap(); assert_eq!(results.len(), 1); assert!(matches!(results[0], DealiasMethodResult::Method(_))); } @@ -1882,13 +1978,13 @@ mod tests { // baz -> bar: one level returns the alias to bar let alias = get_method_alias_def(context.graph(), "Foo#baz()"); - let results = dealias_method(context.graph(), alias); + let results = dealias_method(context.graph(), alias).unwrap(); assert_eq!(results.len(), 1); assert!(matches!(results[0], DealiasMethodResult::Alias(_))); // bar -> foo: one more level returns the method definition let alias = get_method_alias_def(context.graph(), "Foo#bar()"); - let results = dealias_method(context.graph(), alias); + let results = dealias_method(context.graph(), alias).unwrap(); assert_eq!(results.len(), 1); assert!(matches!(results[0], DealiasMethodResult::Method(_))); } @@ -1907,8 +2003,8 @@ mod tests { context.resolve(); let alias = get_method_alias_def(context.graph(), "Foo#bar()"); - let results = dealias_method(context.graph(), alias); - assert!(results.is_empty()); + let result = dealias_method(context.graph(), alias); + assert!(matches!(result, Err(DealiasMethodError::MemberNotFound))); } #[test] @@ -1934,7 +2030,7 @@ mod tests { context.resolve(); let alias = get_method_alias_def(context.graph(), "Foo#bar()"); - let results = dealias_method(context.graph(), alias); + let results = dealias_method(context.graph(), alias).unwrap(); assert_eq!(results.len(), 2); assert_eq!( results @@ -1955,18 +2051,21 @@ mod tests { #[test] fn dealias_method_inherited() { let mut context = GraphTest::new(); - context.index_uri("file:///foo.rb", " + context.index_uri( + "file:///foo.rb", + " class Parent def foo(a); end end class Child < Parent alias bar foo end - "); + ", + ); context.resolve(); let alias = get_method_alias_def(context.graph(), "Child#bar()"); - let results = dealias_method(context.graph(), alias); + let results = dealias_method(context.graph(), alias).unwrap(); assert_eq!(results.len(), 1); assert!(matches!(results[0], DealiasMethodResult::Method(_))); } @@ -1974,11 +2073,14 @@ mod tests { #[test] fn find_member_in_ancestors_direct() { let mut context = GraphTest::new(); - context.index_uri("file:///foo.rb", " + context.index_uri( + "file:///foo.rb", + " class Foo def bar; end end - "); + ", + ); context.resolve(); let result = find_member_in_ancestors( @@ -1993,13 +2095,16 @@ mod tests { #[test] fn find_member_in_ancestors_inherited() { let mut context = GraphTest::new(); - context.index_uri("file:///foo.rb", " + context.index_uri( + "file:///foo.rb", + " class Parent def foo; end end class Child < Parent end - "); + ", + ); context.resolve(); let result = find_member_in_ancestors( @@ -2014,14 +2119,17 @@ mod tests { #[test] fn find_member_in_ancestors_overridden() { let mut context = GraphTest::new(); - context.index_uri("file:///foo.rb", " + context.index_uri( + "file:///foo.rb", + " class Parent def foo; end end class Child < Parent def foo; end end - "); + ", + ); context.resolve(); let result = find_member_in_ancestors( @@ -2036,10 +2144,13 @@ mod tests { #[test] fn find_member_in_ancestors_not_found() { let mut context = GraphTest::new(); - context.index_uri("file:///foo.rb", " + context.index_uri( + "file:///foo.rb", + " class Foo end - "); + ", + ); context.resolve(); let result = find_member_in_ancestors( @@ -2054,14 +2165,17 @@ mod tests { #[test] fn find_member_in_ancestors_via_module() { let mut context = GraphTest::new(); - context.index_uri("file:///foo.rb", " + context.index_uri( + "file:///foo.rb", + " module Greetable def greet; end end class Foo include Greetable end - "); + ", + ); context.resolve(); let result = find_member_in_ancestors( From 406630699c613352e492d9d113c7cb0032a0b39a Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Wed, 1 Apr 2026 23:15:36 +0900 Subject: [PATCH 10/27] Add test for find_member_in_ancestors --- rust/rubydex/src/query.rs | 60 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/rust/rubydex/src/query.rs b/rust/rubydex/src/query.rs index d597a344..be098f05 100644 --- a/rust/rubydex/src/query.rs +++ b/rust/rubydex/src/query.rs @@ -2162,6 +2162,66 @@ mod tests { assert_eq!(result, None); } + #[test] + fn find_member_in_ancestors_only_inherited() { + let mut context = GraphTest::new(); + context.index_uri("file:///foo.rb", " + class Parent + def foo; end + end + class Child < Parent + def foo; end + def bar; end + end + "); + context.resolve(); + + // own method is skipped with only_inherited + let result = find_member_in_ancestors( + context.graph(), + DeclarationId::from("Child"), + StringId::from("foo()"), + true, + ); + assert_eq!(result, Some(DeclarationId::from("Parent#foo()"))); + + // method only in self returns None with only_inherited + let result = find_member_in_ancestors( + context.graph(), + DeclarationId::from("Child"), + StringId::from("bar()"), + true, + ); + assert_eq!(result, None); + } + + #[test] + fn find_member_in_ancestors_only_inherited_with_prepend() { + let mut context = GraphTest::new(); + context.index_uri("file:///foo.rb", " + module M + def foo; end + end + class Parent + def foo; end + end + class Child < Parent + prepend M + def foo; end + end + "); + context.resolve(); + + // prepended module and self are skipped, finds Parent's foo + let result = find_member_in_ancestors( + context.graph(), + DeclarationId::from("Child"), + StringId::from("foo()"), + true, + ); + assert_eq!(result, Some(DeclarationId::from("Parent#foo()"))); + } + #[test] fn find_member_in_ancestors_via_module() { let mut context = GraphTest::new(); From 783402aa8850f317e0c159f920bcd16c7c1669e3 Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Wed, 1 Apr 2026 23:21:54 +0900 Subject: [PATCH 11/27] Add test of deep_dealias_method --- rust/rubydex/src/query.rs | 46 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/rust/rubydex/src/query.rs b/rust/rubydex/src/query.rs index be098f05..e1c5bf99 100644 --- a/rust/rubydex/src/query.rs +++ b/rust/rubydex/src/query.rs @@ -1932,10 +1932,18 @@ mod tests { } fn get_method_alias_def<'a>(graph: &'a Graph, decl_name: &str) -> &'a MethodAliasDefinition { + let (_, alias) = get_method_alias_def_with_id(graph, decl_name); + alias + } + + fn get_method_alias_def_with_id<'a>( + graph: &'a Graph, + decl_name: &str, + ) -> (DefinitionId, &'a MethodAliasDefinition) { let decl = graph.declarations().get(&DeclarationId::from(decl_name)).unwrap(); for def_id in decl.definitions() { if let Some(Definition::MethodAlias(alias)) = graph.definitions().get(def_id) { - return alias; + return (*def_id, alias); } } panic!("No MethodAliasDefinition found for {decl_name}"); @@ -2070,6 +2078,42 @@ mod tests { assert!(matches!(results[0], DealiasMethodResult::Method(_))); } + #[test] + fn deep_dealias_method_mixed() { + let mut context = GraphTest::new(); + // `then` has three definitions: + // - a Method (from file1) + // - an alias to `baz` which is circular (from file2) + // - an alias to `nonexistent` which is unresolved (from file2) + // `start` aliases `then`, so deep_dealias_method(start) sees all three. + context.index_uri("file:///foo1.rb", " + class Foo + def then(a); end + alias bar baz + alias baz bar + end + "); + context.index_uri("file:///foo2.rb", " + class Foo + alias then baz + alias then nonexistent + alias start then + end + "); + context.resolve(); + + let (id, alias) = get_method_alias_def_with_id(context.graph(), "Foo#start()"); + let result = deep_dealias_method(context.graph(), alias, id); + + // Method definition of `then` is found + assert_eq!(result.method_ids.len(), 1); + // Circular chain via baz -> bar -> baz + assert_eq!(result.circular_aliases.len(), 1); + // Unresolved alias to nonexistent + assert_eq!(result.errors.len(), 1); + assert!(matches!(result.errors[0].1, DealiasMethodError::MemberNotFound)); + } + #[test] fn find_member_in_ancestors_direct() { let mut context = GraphTest::new(); From d8520b710f6b2ad4e40c07c50a39cfe6cf5b26a6 Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Wed, 1 Apr 2026 23:22:49 +0900 Subject: [PATCH 12/27] =?UTF-8?q?=F0=9F=91=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/definition_test.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/test/definition_test.rb b/test/definition_test.rb index 634dbe92..ec9e6154 100644 --- a/test/definition_test.rb +++ b/test/definition_test.rb @@ -595,7 +595,6 @@ def foo(a, b); end assert_equal(2, params.length) assert_equal(:a, params[0].name) assert_equal(:b, params[1].name) - end end From 3762ce3a4ce7ddbdd276a34303f78fd9eb494b34 Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Wed, 1 Apr 2026 23:30:08 +0900 Subject: [PATCH 13/27] lint --- rust/rubydex-sys/src/declaration_api.rs | 3 +-- rust/rubydex/src/query.rs | 32 ++++++++++++++++--------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/rust/rubydex-sys/src/declaration_api.rs b/rust/rubydex-sys/src/declaration_api.rs index 8c8decbf..52066fa6 100644 --- a/rust/rubydex-sys/src/declaration_api.rs +++ b/rust/rubydex-sys/src/declaration_api.rs @@ -180,8 +180,7 @@ pub unsafe extern "C" fn rdx_declaration_find_member( let id = DeclarationId::new(declaration_id); let member_id = StringId::from(member_str.as_str()); - let Some(member_decl_id) = - rubydex::query::find_member_in_ancestors(graph, id, member_id, only_inherited) + let Some(member_decl_id) = rubydex::query::find_member_in_ancestors(graph, id, member_id, only_inherited) else { return ptr::null(); }; diff --git a/rust/rubydex/src/query.rs b/rust/rubydex/src/query.rs index e1c5bf99..707ed3b3 100644 --- a/rust/rubydex/src/query.rs +++ b/rust/rubydex/src/query.rs @@ -565,9 +565,7 @@ pub fn dealias_method( }, }; - let Some(method_decl_id) = - find_member_in_ancestors(graph, owner_id, *alias.old_name_str_id(), false) - else { + let Some(method_decl_id) = find_member_in_ancestors(graph, owner_id, *alias.old_name_str_id(), false) else { return Err(DealiasMethodError::MemberNotFound); }; let method_decl = graph.declarations().get(&method_decl_id).unwrap(); @@ -2086,20 +2084,26 @@ mod tests { // - an alias to `baz` which is circular (from file2) // - an alias to `nonexistent` which is unresolved (from file2) // `start` aliases `then`, so deep_dealias_method(start) sees all three. - context.index_uri("file:///foo1.rb", " + context.index_uri( + "file:///foo1.rb", + " class Foo def then(a); end alias bar baz alias baz bar end - "); - context.index_uri("file:///foo2.rb", " + ", + ); + context.index_uri( + "file:///foo2.rb", + " class Foo alias then baz alias then nonexistent alias start then end - "); + ", + ); context.resolve(); let (id, alias) = get_method_alias_def_with_id(context.graph(), "Foo#start()"); @@ -2209,7 +2213,9 @@ mod tests { #[test] fn find_member_in_ancestors_only_inherited() { let mut context = GraphTest::new(); - context.index_uri("file:///foo.rb", " + context.index_uri( + "file:///foo.rb", + " class Parent def foo; end end @@ -2217,7 +2223,8 @@ mod tests { def foo; end def bar; end end - "); + ", + ); context.resolve(); // own method is skipped with only_inherited @@ -2242,7 +2249,9 @@ mod tests { #[test] fn find_member_in_ancestors_only_inherited_with_prepend() { let mut context = GraphTest::new(); - context.index_uri("file:///foo.rb", " + context.index_uri( + "file:///foo.rb", + " module M def foo; end end @@ -2253,7 +2262,8 @@ mod tests { prepend M def foo; end end - "); + ", + ); context.resolve(); // prepended module and self are skipped, finds Parent's foo From b73cb99f345c06b343a8f9664bd022a9e017a24a Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Thu, 2 Apr 2026 11:23:48 +0900 Subject: [PATCH 14/27] Create *dynamic* symbols, which are garbage collected --- ext/rubydex/signature.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/rubydex/signature.c b/ext/rubydex/signature.c index b59888ac..6019bc35 100644 --- a/ext/rubydex/signature.c +++ b/ext/rubydex/signature.c @@ -39,7 +39,7 @@ VALUE rdxi_signatures_to_ruby(SignatureArray *arr) { ParameterEntry param_entry = sig_entry.parameters[j]; VALUE param_class = parameter_class_for_kind(param_entry.kind); - VALUE name_sym = ID2SYM(rb_intern(param_entry.name)); + VALUE name_sym = rb_str_intern(rb_utf8_str_new_cstr(param_entry.name)); VALUE location = rdxi_build_location_value(param_entry.location); VALUE param_argv[] = {name_sym, location}; VALUE param = rb_class_new_instance(2, param_argv, param_class); From f5e6d8ed2a289958a9999ffafe59c4d231f115a8 Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Thu, 2 Apr 2026 11:31:39 +0900 Subject: [PATCH 15/27] =?UTF-8?q?Drop=20`=E2=80=A6`=20comment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/definition_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/definition_test.rb b/test/definition_test.rb index ec9e6154..4a270b94 100644 --- a/test/definition_test.rb +++ b/test/definition_test.rb @@ -459,7 +459,7 @@ def baz(...); end assert_equal(1, params.length) assert_instance_of(Rubydex::Signature::ForwardParameter, params[0]) assert_equal(:"...", params[0].name) - assert_equal("#{path}:1:9-1:12", params[0].location.to_display.to_s) # ... + assert_equal("#{path}:1:9-1:12", params[0].location.to_display.to_s) end end From cc547082368df54eb173a095501d06986769c60a Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Thu, 2 Apr 2026 11:32:57 +0900 Subject: [PATCH 16/27] Add `use` --- rust/rubydex-sys/src/signature_api.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/rust/rubydex-sys/src/signature_api.rs b/rust/rubydex-sys/src/signature_api.rs index 27e24768..6003bc33 100644 --- a/rust/rubydex-sys/src/signature_api.rs +++ b/rust/rubydex-sys/src/signature_api.rs @@ -3,7 +3,8 @@ use crate::graph_api::{GraphPointer, with_graph}; use crate::location_api::{Location, create_location_for_uri_and_offset}; use libc::c_char; -use rubydex::model::definitions::{Definition, Parameter}; +use rubydex::model::definitions::{Definition, MethodDefinition, Parameter}; +use rubydex::model::graph::Graph; use rubydex::model::ids::DefinitionId; use std::ffi::CString; use std::ptr; @@ -92,8 +93,8 @@ pub unsafe extern "C" fn rdx_definition_signatures(pointer: GraphPointer, defini /// Helper: build signature entries from a `MethodDefinition` and append them to the output vector. fn collect_method_signatures( - graph: &rubydex::model::graph::Graph, - method_def: &rubydex::model::definitions::MethodDefinition, + graph: &Graph, + method_def: &MethodDefinition, definition_id: u64, out: &mut Vec, ) { From 9fb0de4bf3f5a40b17dcd10a7a965a8ef16ca5a4 Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Thu, 2 Apr 2026 12:28:11 +0900 Subject: [PATCH 17/27] Get owner_id through declaration --- rust/rubydex-sys/src/signature_api.rs | 5 +- rust/rubydex/src/query.rs | 203 +++++++------------------- 2 files changed, 56 insertions(+), 152 deletions(-) diff --git a/rust/rubydex-sys/src/signature_api.rs b/rust/rubydex-sys/src/signature_api.rs index 6003bc33..24a1680b 100644 --- a/rust/rubydex-sys/src/signature_api.rs +++ b/rust/rubydex-sys/src/signature_api.rs @@ -151,11 +151,8 @@ pub unsafe extern "C" fn rdx_method_alias_definition_signatures( ) -> *mut SignatureArray { with_graph(pointer, |graph| { let def_id = DefinitionId::new(definition_id); - let Definition::MethodAlias(alias) = graph.definitions().get(&def_id).expect("definition should exist") else { - panic!("expected a method alias definition"); - }; - let result = rubydex::query::deep_dealias_method(graph, alias, def_id); + let result = rubydex::query::deep_dealias_method(graph, def_id); let mut sig_entries: Vec = Vec::new(); for id in &result.method_ids { diff --git a/rust/rubydex/src/query.rs b/rust/rubydex/src/query.rs index 707ed3b3..b8c06160 100644 --- a/rust/rubydex/src/query.rs +++ b/rust/rubydex/src/query.rs @@ -6,7 +6,7 @@ use std::thread; use url::Url; use crate::model::declaration::{Ancestor, Declaration}; -use crate::model::definitions::{Definition, MethodAliasDefinition, Parameter, Receiver}; +use crate::model::definitions::{Definition, Parameter}; use crate::model::graph::{Graph, OBJECT_ID}; use crate::model::identity_maps::IdentityHashSet; use crate::model::ids::{DeclarationId, DefinitionId, NameId, StringId, UriId}; @@ -469,43 +469,6 @@ fn method_argument_completion<'a>( Ok(candidates) } -/// Result of looking up the enclosing namespace for a definition. -#[allow(dead_code)] -enum EnclosingNamespace { - /// Found a namespace (class, module, or singleton class). - Found(DeclarationId), - /// The definition has no enclosing namespace (top-level). - NotFound, - /// The enclosing namespace could not be determined because name resolution failed. - Unresolved(DefinitionId), -} - -/// Walks up the lexical nesting chain from the given `DefinitionId` to find -/// the nearest enclosing namespace (class, module, or singleton class). -fn enclosing_namespace(graph: &Graph, starting_id: Option<&DefinitionId>) -> EnclosingNamespace { - let mut current = starting_id; - while let Some(id) = current { - let def = graph.definitions().get(id).unwrap(); - let is_namespace_def = matches!( - def, - Definition::Class(_) | Definition::Module(_) | Definition::SingletonClass(_) - ); - - let Some(decl_id) = graph.definition_id_to_declaration_id(*id) else { - if is_namespace_def { - return EnclosingNamespace::Unresolved(*id); - } - current = def.lexical_nesting_id().as_ref(); - continue; - }; - if is_namespace_def { - return EnclosingNamespace::Found(*decl_id); - } - current = def.lexical_nesting_id().as_ref(); - } - EnclosingNamespace::NotFound -} - /// Result of dealiasing a single level of method alias. pub enum DealiasMethodResult { /// The alias target is a concrete method definition. @@ -514,72 +477,39 @@ pub enum DealiasMethodResult { Alias(DefinitionId), } -/// Error type for `dealias_method`. -#[derive(Debug)] -pub enum DealiasMethodError { - /// The constant receiver (e.g., `Foo` in `def Foo.bar`) could not be resolved. - UnresolvedReceiver, - /// The enclosing namespace's name could not be resolved. - UnresolvedEnclosingNamespace(DefinitionId), - /// No enclosing namespace exists (e.g., top-level alias with no lexical nesting). - NoEnclosingNamespace, - /// The aliased method name was not found in the namespace or its ancestors. - MemberNotFound, -} - /// Dereferences a `MethodAliasDefinition` by one level, returning the definitions -/// found under the aliased name. -/// -/// # Errors -/// -/// Returns a `DealiasMethodError` if the alias target cannot be resolved. +/// found under the aliased name. Returns `None` if the aliased method name is not +/// found in the namespace or its ancestors. /// /// # Panics /// -/// Panics if a `SelfReceiver` definition cannot be resolved to a namespace with a singleton class. -pub fn dealias_method( - graph: &Graph, - alias: &MethodAliasDefinition, -) -> Result, DealiasMethodError> { - let owner_id = match alias.receiver() { - Some(Receiver::SelfReceiver(def_id)) => { - let decl_id = graph.definition_id_to_declaration_id(*def_id).unwrap(); - let decl = graph.declarations().get(decl_id).unwrap(); - let ns = decl.as_namespace().unwrap(); - *ns.singleton_class().unwrap() - } - Some(Receiver::ConstantReceiver(name_id)) => { - let Some(&id) = graph.name_id_to_declaration_id(*name_id) else { - return Err(DealiasMethodError::UnresolvedReceiver); - }; - id - } - None => match enclosing_namespace(graph, alias.lexical_nesting_id().as_ref()) { - EnclosingNamespace::Found(id) => id, - EnclosingNamespace::Unresolved(def_id) => { - return Err(DealiasMethodError::UnresolvedEnclosingNamespace(def_id)); - } - EnclosingNamespace::NotFound => { - return Err(DealiasMethodError::NoEnclosingNamespace); - } - }, +/// Panics if `alias_id` does not correspond to a `MethodAliasDefinition` +/// or has no corresponding declaration in the graph. +#[must_use] +pub fn dealias_method(graph: &Graph, alias_id: DefinitionId) -> Option> { + let Definition::MethodAlias(alias) = graph.definitions().get(&alias_id).expect("definition should exist") else { + panic!("expected a method alias definition"); }; - let Some(method_decl_id) = find_member_in_ancestors(graph, owner_id, *alias.old_name_str_id(), false) else { - return Err(DealiasMethodError::MemberNotFound); - }; + let decl_id = graph.definition_id_to_declaration_id(alias_id).unwrap(); + let alias_decl = graph.declarations().get(decl_id).unwrap(); + let owner_id = *alias_decl.owner_id(); + + let method_decl_id = find_member_in_ancestors(graph, owner_id, *alias.old_name_str_id(), false)?; let method_decl = graph.declarations().get(&method_decl_id).unwrap(); assert!(matches!(method_decl, Declaration::Method(_))); - Ok(method_decl - .definitions() - .iter() - .filter_map(|def_id| match graph.definitions().get(def_id) { - Some(Definition::Method(_)) => Some(DealiasMethodResult::Method(*def_id)), - Some(Definition::MethodAlias(_)) => Some(DealiasMethodResult::Alias(*def_id)), - _ => None, - }) - .collect()) + Some( + method_decl + .definitions() + .iter() + .filter_map(|def_id| match graph.definitions().get(def_id) { + Some(Definition::Method(_)) => Some(DealiasMethodResult::Method(*def_id)), + Some(Definition::MethodAlias(_)) => Some(DealiasMethodResult::Alias(*def_id)), + _ => None, + }) + .collect(), + ) } /// Result of following a `MethodAliasDefinition` chain to completion. @@ -589,8 +519,8 @@ pub struct DeepDealiasMethodResult { pub method_ids: Vec, /// `DefinitionId`s where circular alias chains were detected. pub circular_aliases: Vec, - /// Alias resolution errors (`DefinitionId` of the failing alias + the error). - pub errors: Vec<(DefinitionId, DealiasMethodError)>, + /// `DefinitionId`s of aliases whose target method could not be found. + pub missing_targets: Vec, } /// Follows a `MethodAliasDefinition` chain to completion, returning all resolved @@ -600,34 +530,22 @@ pub struct DeepDealiasMethodResult { /// /// # Panics /// -/// Panics if a `SelfReceiver` definition cannot be resolved to a namespace with a singleton class. +/// Panics if any alias definition in the chain has no corresponding declaration. #[must_use] -pub fn deep_dealias_method( - graph: &Graph, - alias: &MethodAliasDefinition, - alias_def_id: DefinitionId, -) -> DeepDealiasMethodResult { +pub fn deep_dealias_method(graph: &Graph, alias_id: DefinitionId) -> DeepDealiasMethodResult { let mut result = DeepDealiasMethodResult { method_ids: Vec::new(), circular_aliases: Vec::new(), - errors: Vec::new(), - }; - - let mut current_results = match dealias_method(graph, alias) { - Ok(results) => results, - Err(err) => { - result.errors.push((alias_def_id, err)); - return result; - } + missing_targets: Vec::new(), }; + let mut current_dealias_results = vec![DealiasMethodResult::Alias(alias_id)]; let mut visited = HashSet::new(); - visited.insert(alias_def_id); loop { let mut next_aliases = Vec::new(); - for item in ¤t_results { + for item in ¤t_dealias_results { match item { DealiasMethodResult::Method(id) => { result.method_ids.push(*id); @@ -637,11 +555,9 @@ pub fn deep_dealias_method( result.circular_aliases.push(*id); continue; } - if let Some(Definition::MethodAlias(next_alias)) = graph.definitions().get(id) { - match dealias_method(graph, next_alias) { - Ok(next) => next_aliases.extend(next), - Err(err) => result.errors.push((*id, err)), - } + match dealias_method(graph, *id) { + Some(next) => next_aliases.extend(next), + None => result.missing_targets.push(*id), } } } @@ -651,7 +567,7 @@ pub fn deep_dealias_method( return result; } - current_results = next_aliases; + current_dealias_results = next_aliases; } } @@ -1929,19 +1845,11 @@ mod tests { assert!(!candidates.iter().any(|c| matches!(c, CompletionCandidate::Keyword(_)))); } - fn get_method_alias_def<'a>(graph: &'a Graph, decl_name: &str) -> &'a MethodAliasDefinition { - let (_, alias) = get_method_alias_def_with_id(graph, decl_name); - alias - } - - fn get_method_alias_def_with_id<'a>( - graph: &'a Graph, - decl_name: &str, - ) -> (DefinitionId, &'a MethodAliasDefinition) { + fn get_method_alias_id(graph: &Graph, decl_name: &str) -> DefinitionId { let decl = graph.declarations().get(&DeclarationId::from(decl_name)).unwrap(); for def_id in decl.definitions() { - if let Some(Definition::MethodAlias(alias)) = graph.definitions().get(def_id) { - return (*def_id, alias); + if matches!(graph.definitions().get(def_id), Some(Definition::MethodAlias(_))) { + return *def_id; } } panic!("No MethodAliasDefinition found for {decl_name}"); @@ -1961,8 +1869,8 @@ mod tests { ); context.resolve(); - let alias = get_method_alias_def(context.graph(), "Foo#bar()"); - let results = dealias_method(context.graph(), alias).unwrap(); + let id = get_method_alias_id(context.graph(), "Foo#bar()"); + let results = dealias_method(context.graph(), id).unwrap(); assert_eq!(results.len(), 1); assert!(matches!(results[0], DealiasMethodResult::Method(_))); } @@ -1983,14 +1891,14 @@ mod tests { context.resolve(); // baz -> bar: one level returns the alias to bar - let alias = get_method_alias_def(context.graph(), "Foo#baz()"); - let results = dealias_method(context.graph(), alias).unwrap(); + let id = get_method_alias_id(context.graph(), "Foo#baz()"); + let results = dealias_method(context.graph(), id).unwrap(); assert_eq!(results.len(), 1); assert!(matches!(results[0], DealiasMethodResult::Alias(_))); // bar -> foo: one more level returns the method definition - let alias = get_method_alias_def(context.graph(), "Foo#bar()"); - let results = dealias_method(context.graph(), alias).unwrap(); + let id = get_method_alias_id(context.graph(), "Foo#bar()"); + let results = dealias_method(context.graph(), id).unwrap(); assert_eq!(results.len(), 1); assert!(matches!(results[0], DealiasMethodResult::Method(_))); } @@ -2008,9 +1916,9 @@ mod tests { ); context.resolve(); - let alias = get_method_alias_def(context.graph(), "Foo#bar()"); - let result = dealias_method(context.graph(), alias); - assert!(matches!(result, Err(DealiasMethodError::MemberNotFound))); + let id = get_method_alias_id(context.graph(), "Foo#bar()"); + let result = dealias_method(context.graph(), id); + assert!(result.is_none()); } #[test] @@ -2035,8 +1943,8 @@ mod tests { ); context.resolve(); - let alias = get_method_alias_def(context.graph(), "Foo#bar()"); - let results = dealias_method(context.graph(), alias).unwrap(); + let id = get_method_alias_id(context.graph(), "Foo#bar()"); + let results = dealias_method(context.graph(), id).unwrap(); assert_eq!(results.len(), 2); assert_eq!( results @@ -2070,8 +1978,8 @@ mod tests { ); context.resolve(); - let alias = get_method_alias_def(context.graph(), "Child#bar()"); - let results = dealias_method(context.graph(), alias).unwrap(); + let id = get_method_alias_id(context.graph(), "Child#bar()"); + let results = dealias_method(context.graph(), id).unwrap(); assert_eq!(results.len(), 1); assert!(matches!(results[0], DealiasMethodResult::Method(_))); } @@ -2106,16 +2014,15 @@ mod tests { ); context.resolve(); - let (id, alias) = get_method_alias_def_with_id(context.graph(), "Foo#start()"); - let result = deep_dealias_method(context.graph(), alias, id); + let id = get_method_alias_id(context.graph(), "Foo#start()"); + let result = deep_dealias_method(context.graph(), id); // Method definition of `then` is found assert_eq!(result.method_ids.len(), 1); // Circular chain via baz -> bar -> baz assert_eq!(result.circular_aliases.len(), 1); // Unresolved alias to nonexistent - assert_eq!(result.errors.len(), 1); - assert!(matches!(result.errors[0].1, DealiasMethodError::MemberNotFound)); + assert_eq!(result.missing_targets.len(), 1); } #[test] From fe2e52d224993af209efac16ed8a98a2735f503c Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Thu, 2 Apr 2026 12:54:28 +0900 Subject: [PATCH 18/27] Fixup tests --- rust/rubydex/src/query.rs | 43 +++++++++++++++-------- rust/rubydex/src/test_utils/graph_test.rs | 28 +++++++++++++-- 2 files changed, 54 insertions(+), 17 deletions(-) diff --git a/rust/rubydex/src/query.rs b/rust/rubydex/src/query.rs index b8c06160..156f1226 100644 --- a/rust/rubydex/src/query.rs +++ b/rust/rubydex/src/query.rs @@ -470,6 +470,7 @@ fn method_argument_completion<'a>( } /// Result of dealiasing a single level of method alias. +#[derive(Debug)] pub enum DealiasMethodResult { /// The alias target is a concrete method definition. Method(DefinitionId), @@ -1845,6 +1846,20 @@ mod tests { assert!(!candidates.iter().any(|c| matches!(c, CompletionCandidate::Keyword(_)))); } + macro_rules! assert_dealias_result_source { + ($context:expr, $result:expr, $expected:expr) => { + let def_id = match $result { + DealiasMethodResult::Method(id) | DealiasMethodResult::Alias(id) => id, + }; + assert_eq!( + $context.source_at(def_id), + $expected, + "unexpected source for {:?}", + $result + ); + }; + } + fn get_method_alias_id(graph: &Graph, decl_name: &str) -> DefinitionId { let decl = graph.declarations().get(&DeclarationId::from(decl_name)).unwrap(); for def_id in decl.definitions() { @@ -1873,6 +1888,7 @@ mod tests { let results = dealias_method(context.graph(), id).unwrap(); assert_eq!(results.len(), 1); assert!(matches!(results[0], DealiasMethodResult::Method(_))); + assert_dealias_result_source!(&context, &results[0], "def foo(a, b); end"); } #[test] @@ -1895,12 +1911,14 @@ mod tests { let results = dealias_method(context.graph(), id).unwrap(); assert_eq!(results.len(), 1); assert!(matches!(results[0], DealiasMethodResult::Alias(_))); + assert_dealias_result_source!(&context, &results[0], "alias bar foo"); // bar -> foo: one more level returns the method definition let id = get_method_alias_id(context.graph(), "Foo#bar()"); let results = dealias_method(context.graph(), id).unwrap(); assert_eq!(results.len(), 1); assert!(matches!(results[0], DealiasMethodResult::Method(_))); + assert_dealias_result_source!(&context, &results[0], "def foo(x); end"); } #[test] @@ -1946,20 +1964,12 @@ mod tests { let id = get_method_alias_id(context.graph(), "Foo#bar()"); let results = dealias_method(context.graph(), id).unwrap(); assert_eq!(results.len(), 2); - assert_eq!( - results - .iter() - .filter(|r| matches!(r, DealiasMethodResult::Method(_))) - .count(), - 1 - ); - assert_eq!( - results - .iter() - .filter(|r| matches!(r, DealiasMethodResult::Alias(_))) - .count(), - 1 - ); + + let method = results.iter().find(|r| matches!(r, DealiasMethodResult::Method(_))).unwrap(); + assert_dealias_result_source!(&context, method, "def foo(a); end"); + + let alias = results.iter().find(|r| matches!(r, DealiasMethodResult::Alias(_))).unwrap(); + assert_dealias_result_source!(&context, alias, "alias foo baz"); } #[test] @@ -1981,7 +1991,7 @@ mod tests { let id = get_method_alias_id(context.graph(), "Child#bar()"); let results = dealias_method(context.graph(), id).unwrap(); assert_eq!(results.len(), 1); - assert!(matches!(results[0], DealiasMethodResult::Method(_))); + assert_dealias_result_source!(&context, &results[0], "def foo(a); end"); } #[test] @@ -2019,10 +2029,13 @@ mod tests { // Method definition of `then` is found assert_eq!(result.method_ids.len(), 1); + assert_eq!(context.source_at(&result.method_ids[0]), "def then(a); end"); // Circular chain via baz -> bar -> baz assert_eq!(result.circular_aliases.len(), 1); + assert_eq!(context.source_at(&result.circular_aliases[0]), "alias baz bar"); // Unresolved alias to nonexistent assert_eq!(result.missing_targets.len(), 1); + assert_eq!(context.source_at(&result.missing_targets[0]), "alias then nonexistent"); } #[test] diff --git a/rust/rubydex/src/test_utils/graph_test.rs b/rust/rubydex/src/test_utils/graph_test.rs index fb2766fd..6f8c1e67 100644 --- a/rust/rubydex/src/test_utils/graph_test.rs +++ b/rust/rubydex/src/test_utils/graph_test.rs @@ -1,20 +1,27 @@ +use std::collections::HashMap; + use super::normalize_indentation; #[cfg(test)] use crate::diagnostic::Rule; use crate::indexing::{self, LanguageId}; use crate::model::graph::{Graph, NameDependent}; -use crate::model::ids::{NameId, StringId}; +use crate::model::ids::{DefinitionId, NameId, StringId}; +use crate::offset::Offset; use crate::resolution::Resolver; #[derive(Default)] pub struct GraphTest { graph: Graph, + sources: HashMap, } impl GraphTest { #[must_use] pub fn new() -> Self { - Self { graph: Graph::new() } + Self { + graph: Graph::new(), + sources: HashMap::new(), + } } #[must_use] @@ -31,12 +38,29 @@ impl GraphTest { pub fn index_uri(&mut self, uri: &str, source: &str) { let source = normalize_indentation(source); indexing::index_source(&mut self.graph, uri, &source, &LanguageId::Ruby); + self.sources.insert(uri.to_string(), source); } /// Indexes an RBS source pub fn index_rbs_uri(&mut self, uri: &str, source: &str) { let source = normalize_indentation(source); indexing::index_source(&mut self.graph, uri, &source, &LanguageId::Rbs); + self.sources.insert(uri.to_string(), source); + } + + /// Returns the normalized source for the given URI. + #[must_use] + pub fn source(&self, uri: &str) -> &str { + self.sources.get(uri).expect("source not found for URI") + } + + /// Returns the source text for a definition, sliced by its offset. + #[must_use] + pub fn source_at(&self, definition_id: &DefinitionId) -> &str { + let def = self.graph.definitions().get(definition_id).unwrap(); + let uri = self.graph.documents().get(def.uri_id()).unwrap().uri(); + let source = self.source(uri); + &source[def.offset().start() as usize..def.offset().end() as usize] } pub fn delete_uri(&mut self, uri: &str) { From 9a0fbad123bb79b53af752e1ee6e641d53c61ac9 Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Thu, 2 Apr 2026 13:02:06 +0900 Subject: [PATCH 19/27] dedup deep_dealias_method result --- rust/rubydex/src/query.rs | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/rust/rubydex/src/query.rs b/rust/rubydex/src/query.rs index 156f1226..0b4bfbe3 100644 --- a/rust/rubydex/src/query.rs +++ b/rust/rubydex/src/query.rs @@ -565,6 +565,8 @@ pub fn deep_dealias_method(graph: &Graph, alias_id: DefinitionId) -> DeepDealias } if next_aliases.is_empty() { + let mut seen = HashSet::new(); + result.method_ids.retain(|id| seen.insert(*id)); return result; } @@ -2038,6 +2040,38 @@ mod tests { assert_eq!(context.source_at(&result.missing_targets[0]), "alias then nonexistent"); } + #[test] + fn deep_dealias_method_duplicated() { + let mut context = GraphTest::new(); + // `then` has two identical method definitions in different files. Both should be returned by deep_dealias_method. + context.index_uri( + "file:///foo1.rb", + " + class Foo + def foo(a); end + end + ", + ); + context.index_uri( + "file:///foo2.rb", + " + class Foo + alias bar foo + alias bar foo + + alias start bar + end + ", + ); + context.resolve(); + + let id = get_method_alias_id(context.graph(), "Foo#start()"); + let result = deep_dealias_method(context.graph(), id); + + assert_eq!(result.method_ids.len(), 1); + assert_eq!(context.source_at(&result.method_ids[0]), "def foo(a); end"); + } + #[test] fn find_member_in_ancestors_direct() { let mut context = GraphTest::new(); From 7f41cb355064d48517f9c7609c11bdfd09798f94 Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Thu, 2 Apr 2026 13:02:45 +0900 Subject: [PATCH 20/27] cargo fmt --- rust/rubydex/src/query.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/rust/rubydex/src/query.rs b/rust/rubydex/src/query.rs index 0b4bfbe3..6b6da2b5 100644 --- a/rust/rubydex/src/query.rs +++ b/rust/rubydex/src/query.rs @@ -1967,10 +1967,16 @@ mod tests { let results = dealias_method(context.graph(), id).unwrap(); assert_eq!(results.len(), 2); - let method = results.iter().find(|r| matches!(r, DealiasMethodResult::Method(_))).unwrap(); + let method = results + .iter() + .find(|r| matches!(r, DealiasMethodResult::Method(_))) + .unwrap(); assert_dealias_result_source!(&context, method, "def foo(a); end"); - let alias = results.iter().find(|r| matches!(r, DealiasMethodResult::Alias(_))).unwrap(); + let alias = results + .iter() + .find(|r| matches!(r, DealiasMethodResult::Alias(_))) + .unwrap(); assert_dealias_result_source!(&context, alias, "alias foo baz"); } From e9942f791f372843b2efdd4eb429dd7c4912ab0e Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Thu, 2 Apr 2026 13:04:58 +0900 Subject: [PATCH 21/27] Add GraphTest.source_at_offset --- rust/rubydex/src/test_utils/graph_test.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/rust/rubydex/src/test_utils/graph_test.rs b/rust/rubydex/src/test_utils/graph_test.rs index 6f8c1e67..ed8799c5 100644 --- a/rust/rubydex/src/test_utils/graph_test.rs +++ b/rust/rubydex/src/test_utils/graph_test.rs @@ -58,9 +58,14 @@ impl GraphTest { #[must_use] pub fn source_at(&self, definition_id: &DefinitionId) -> &str { let def = self.graph.definitions().get(definition_id).unwrap(); - let uri = self.graph.documents().get(def.uri_id()).unwrap().uri(); + self.source_at_offset(self.graph.documents().get(def.uri_id()).unwrap().uri(), def.offset()) + } + + /// Returns the source text at the given URI and offset. + #[must_use] + pub fn source_at_offset(&self, uri: &str, offset: &Offset) -> &str { let source = self.source(uri); - &source[def.offset().start() as usize..def.offset().end() as usize] + &source[offset.start() as usize..offset.end() as usize] } pub fn delete_uri(&mut self, uri: &str) { From c5c9d07b7ed04f30c6b8d124a00a325c4024efa7 Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Thu, 2 Apr 2026 13:15:18 +0900 Subject: [PATCH 22/27] Add panics section --- rust/rubydex/src/test_utils/graph_test.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/rust/rubydex/src/test_utils/graph_test.rs b/rust/rubydex/src/test_utils/graph_test.rs index ed8799c5..6a450f05 100644 --- a/rust/rubydex/src/test_utils/graph_test.rs +++ b/rust/rubydex/src/test_utils/graph_test.rs @@ -49,12 +49,20 @@ impl GraphTest { } /// Returns the normalized source for the given URI. + /// + /// # Panics + /// + /// Panics if the URI has not been indexed. #[must_use] pub fn source(&self, uri: &str) -> &str { self.sources.get(uri).expect("source not found for URI") } /// Returns the source text for a definition, sliced by its offset. + /// + /// # Panics + /// + /// Panics if the definition or its document does not exist. #[must_use] pub fn source_at(&self, definition_id: &DefinitionId) -> &str { let def = self.graph.definitions().get(definition_id).unwrap(); From 4d495efc8b4b9c5b41fd24370e58cb7cf711cad2 Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Wed, 8 Apr 2026 13:53:52 +0900 Subject: [PATCH 23/27] Move `source_at` to `Offset` --- rust/rubydex/src/offset.rs | 6 ++++++ rust/rubydex/src/test_utils/graph_test.rs | 11 ++--------- rust/skills.json | 5 +++++ 3 files changed, 13 insertions(+), 9 deletions(-) create mode 100644 rust/skills.json diff --git a/rust/rubydex/src/offset.rs b/rust/rubydex/src/offset.rs index 7bef5199..a07cd027 100644 --- a/rust/rubydex/src/offset.rs +++ b/rust/rubydex/src/offset.rs @@ -74,6 +74,12 @@ impl Offset { self.end } + /// Returns the source text slice corresponding to this offset. + #[must_use] + pub fn source_at<'a>(&self, source: &'a str) -> &'a str { + &source[self.start as usize..self.end as usize] + } + /// Converts an offset to a display range like `1:1-1:5` #[must_use] pub fn to_display_range(&self, document: &Document) -> String { diff --git a/rust/rubydex/src/test_utils/graph_test.rs b/rust/rubydex/src/test_utils/graph_test.rs index 6a450f05..fdf3fd36 100644 --- a/rust/rubydex/src/test_utils/graph_test.rs +++ b/rust/rubydex/src/test_utils/graph_test.rs @@ -6,7 +6,6 @@ use crate::diagnostic::Rule; use crate::indexing::{self, LanguageId}; use crate::model::graph::{Graph, NameDependent}; use crate::model::ids::{DefinitionId, NameId, StringId}; -use crate::offset::Offset; use crate::resolution::Resolver; #[derive(Default)] @@ -66,14 +65,8 @@ impl GraphTest { #[must_use] pub fn source_at(&self, definition_id: &DefinitionId) -> &str { let def = self.graph.definitions().get(definition_id).unwrap(); - self.source_at_offset(self.graph.documents().get(def.uri_id()).unwrap().uri(), def.offset()) - } - - /// Returns the source text at the given URI and offset. - #[must_use] - pub fn source_at_offset(&self, uri: &str, offset: &Offset) -> &str { - let source = self.source(uri); - &source[offset.start() as usize..offset.end() as usize] + let uri = self.graph.documents().get(def.uri_id()).unwrap().uri(); + def.offset().source_at(self.source(uri)) } pub fn delete_uri(&mut self, uri: &str) { diff --git a/rust/skills.json b/rust/skills.json new file mode 100644 index 00000000..de8bc289 --- /dev/null +++ b/rust/skills.json @@ -0,0 +1,5 @@ +{ + "skills": [ + "talent-shopify" + ] +} From fce97329833102c9c330a52be3c152305e2162ac Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Tue, 14 Apr 2026 05:58:20 +0900 Subject: [PATCH 24/27] Add test for singleton classes --- test/definition_test.rb | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/test/definition_test.rb b/test/definition_test.rb index 4a270b94..a1704a3b 100644 --- a/test/definition_test.rb +++ b/test/definition_test.rb @@ -364,6 +364,42 @@ class << self end end + def test_method_definition_signatures_from_ruby + with_context do |context| + context.write!("file1.rb", <<~RUBY) + class Foo + def foo(x) = x + + def self.bar(y) = y + end + RUBY + + path = context.absolute_path_to("file1.rb") + + graph = Rubydex::Graph.new + graph.index_all(context.glob("**/*.rb")) + graph.resolve + + graph["Foo#foo()"].definitions.flat_map(&:signatures).tap do |signatures| + assert_equal(1, signatures.size) + signatures[0].parameters[0].tap do |param| + assert_instance_of(Rubydex::Signature::PositionalParameter, param) + assert_equal(:x, param.name) + assert_equal("#{path}:2:11-2:12", param.location.to_display.to_s) # a + end + end + + graph["Foo::#bar()"].definitions.flat_map(&:signatures).tap do |signatures| + assert_equal(1, signatures.size) + signatures[0].parameters[0].tap do |param| + assert_instance_of(Rubydex::Signature::PositionalParameter, param) + assert_equal(:y, param.name) + assert_equal("#{path}:4:16-4:17", param.location.to_display.to_s) # a + end + end + end + end + def test_method_definition_signatures_with_various_parameter_kinds with_context do |context| context.write!("file1.rb", <<~RUBY) From 536102e1a8cbda967301b137fabdcfb40be5934f Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Tue, 14 Apr 2026 05:58:28 +0900 Subject: [PATCH 25/27] It panics instead of returning NULL --- rust/rubydex-sys/src/signature_api.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/rust/rubydex-sys/src/signature_api.rs b/rust/rubydex-sys/src/signature_api.rs index 24a1680b..6b921187 100644 --- a/rust/rubydex-sys/src/signature_api.rs +++ b/rust/rubydex-sys/src/signature_api.rs @@ -62,7 +62,6 @@ pub struct SignatureArray { } /// Returns a newly allocated array of signatures for the given method definition id. -/// Returns NULL if the definition is not a method definition. /// Caller must free the returned pointer with `rdx_definition_signatures_free`. /// /// # Safety @@ -70,7 +69,7 @@ pub struct SignatureArray { /// - `definition_id` must be a valid definition id. /// /// # Panics -/// This function will panic if a definition or document cannot be found. +/// Panics if `definition_id` does not exist or is not a `MethodDefinition`. #[unsafe(no_mangle)] pub unsafe extern "C" fn rdx_definition_signatures(pointer: GraphPointer, definition_id: u64) -> *mut SignatureArray { with_graph(pointer, |graph| { From 4494f5ccc381b02be708c740e31ec1431ccf8d25 Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Tue, 14 Apr 2026 05:59:39 +0900 Subject: [PATCH 26/27] delete skills.json --- rust/skills.json | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 rust/skills.json diff --git a/rust/skills.json b/rust/skills.json deleted file mode 100644 index de8bc289..00000000 --- a/rust/skills.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "skills": [ - "talent-shopify" - ] -} From aeeebb38b4e2e6a36b5e7ba10efb4dfd998b0428 Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Tue, 14 Apr 2026 06:34:05 +0900 Subject: [PATCH 27/27] Simpler deep_dealias_method return value --- rust/rubydex-sys/src/signature_api.rs | 4 +- rust/rubydex/src/query.rs | 62 +++++++++------------------ 2 files changed, 22 insertions(+), 44 deletions(-) diff --git a/rust/rubydex-sys/src/signature_api.rs b/rust/rubydex-sys/src/signature_api.rs index 6b921187..f28fc529 100644 --- a/rust/rubydex-sys/src/signature_api.rs +++ b/rust/rubydex-sys/src/signature_api.rs @@ -151,10 +151,10 @@ pub unsafe extern "C" fn rdx_method_alias_definition_signatures( with_graph(pointer, |graph| { let def_id = DefinitionId::new(definition_id); - let result = rubydex::query::deep_dealias_method(graph, def_id); + let resolved = rubydex::query::deep_dealias_method(graph, def_id); let mut sig_entries: Vec = Vec::new(); - for id in &result.method_ids { + for id in &resolved { if let Some(Definition::Method(method_def)) = graph.definitions().get(id) { collect_method_signatures(graph, method_def, id.get(), &mut sig_entries); } diff --git a/rust/rubydex/src/query.rs b/rust/rubydex/src/query.rs index 6b6da2b5..820eced0 100644 --- a/rust/rubydex/src/query.rs +++ b/rust/rubydex/src/query.rs @@ -513,32 +513,17 @@ pub fn dealias_method(graph: &Graph, alias_id: DefinitionId) -> Option, - /// `DefinitionId`s where circular alias chains were detected. - pub circular_aliases: Vec, - /// `DefinitionId`s of aliases whose target method could not be found. - pub missing_targets: Vec, -} - -/// Follows a `MethodAliasDefinition` chain to completion, returning all resolved -/// `MethodDefinition` IDs along with any errors encountered. Unlike `dealias_method` -/// which resolves one level, this function keeps following alias chains until only -/// method definitions remain. +/// Recursively follows alias chains starting from `alias_id`, collecting all +/// resolved `MethodDefinition` IDs. Unlike `dealias_method` which resolves one +/// level, this function keeps following aliases until all chains are fully resolved. +/// Circular aliases and missing targets are silently ignored. /// /// # Panics /// /// Panics if any alias definition in the chain has no corresponding declaration. #[must_use] -pub fn deep_dealias_method(graph: &Graph, alias_id: DefinitionId) -> DeepDealiasMethodResult { - let mut result = DeepDealiasMethodResult { - method_ids: Vec::new(), - circular_aliases: Vec::new(), - missing_targets: Vec::new(), - }; +pub fn deep_dealias_method(graph: &Graph, alias_id: DefinitionId) -> Vec { + let mut method_ids = Vec::new(); let mut current_dealias_results = vec![DealiasMethodResult::Alias(alias_id)]; let mut visited = HashSet::new(); @@ -549,25 +534,24 @@ pub fn deep_dealias_method(graph: &Graph, alias_id: DefinitionId) -> DeepDealias for item in ¤t_dealias_results { match item { DealiasMethodResult::Method(id) => { - result.method_ids.push(*id); + method_ids.push(*id); } DealiasMethodResult::Alias(id) => { if !visited.insert(*id) { - result.circular_aliases.push(*id); continue; } - match dealias_method(graph, *id) { - Some(next) => next_aliases.extend(next), - None => result.missing_targets.push(*id), + if let Some(next) = dealias_method(graph, *id) { + next_aliases.extend(next); } } } } if next_aliases.is_empty() { + // Dedup the method ids let mut seen = HashSet::new(); - result.method_ids.retain(|id| seen.insert(*id)); - return result; + method_ids.retain(|id| seen.insert(*id)); + return method_ids; } current_dealias_results = next_aliases; @@ -2007,8 +1991,8 @@ mod tests { let mut context = GraphTest::new(); // `then` has three definitions: // - a Method (from file1) - // - an alias to `baz` which is circular (from file2) - // - an alias to `nonexistent` which is unresolved (from file2) + // - ignored: an alias to `baz` which is circular (from file2) + // - ignored: an alias to `nonexistent` which is unresolved (from file2) // `start` aliases `then`, so deep_dealias_method(start) sees all three. context.index_uri( "file:///foo1.rb", @@ -2033,17 +2017,11 @@ mod tests { context.resolve(); let id = get_method_alias_id(context.graph(), "Foo#start()"); - let result = deep_dealias_method(context.graph(), id); + let method_ids = deep_dealias_method(context.graph(), id); // Method definition of `then` is found - assert_eq!(result.method_ids.len(), 1); - assert_eq!(context.source_at(&result.method_ids[0]), "def then(a); end"); - // Circular chain via baz -> bar -> baz - assert_eq!(result.circular_aliases.len(), 1); - assert_eq!(context.source_at(&result.circular_aliases[0]), "alias baz bar"); - // Unresolved alias to nonexistent - assert_eq!(result.missing_targets.len(), 1); - assert_eq!(context.source_at(&result.missing_targets[0]), "alias then nonexistent"); + assert_eq!(method_ids.len(), 1); + assert_eq!(context.source_at(&method_ids[0]), "def then(a); end"); } #[test] @@ -2072,10 +2050,10 @@ mod tests { context.resolve(); let id = get_method_alias_id(context.graph(), "Foo#start()"); - let result = deep_dealias_method(context.graph(), id); + let method_ids = deep_dealias_method(context.graph(), id); - assert_eq!(result.method_ids.len(), 1); - assert_eq!(context.source_at(&result.method_ids[0]), "def foo(a); end"); + assert_eq!(method_ids.len(), 1); + assert_eq!(context.source_at(&method_ids[0]), "def foo(a); end"); } #[test]