From 988af781d2cf34bfeabee408fc3964aa74db5e6a Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Tue, 24 Mar 2026 17:54:09 +0900 Subject: [PATCH 1/8] Include trailing colon in RBS keyword parameter offset Align RBS keyword parameter offsets with Ruby indexer behavior. The RBS indexer now includes the trailing colon in the offset for required and optional keyword parameters (e.g. `name:` instead of `name`). Adds `Offset::extend_end` helper to adjust byte ranges. Co-Authored-By: Claude Opus 4.6 (1M context) --- rust/rubydex/src/indexing/rbs_indexer.rs | 14 ++++++++------ rust/rubydex/src/offset.rs | 9 +++++++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/rust/rubydex/src/indexing/rbs_indexer.rs b/rust/rubydex/src/indexing/rbs_indexer.rs index 239934af..b5823a3d 100644 --- a/rust/rubydex/src/indexing/rbs_indexer.rs +++ b/rust/rubydex/src/indexing/rbs_indexer.rs @@ -271,14 +271,16 @@ impl<'a> RBSIndexer<'a> { for (key, _value) in function_node.required_keywords().iter() { let name = self.source_at(&key.location()); - let offset = Offset::from_rbs_location(&key.location()); + // Extend offset by 1 to include the trailing colon, matching Ruby indexer behavior + let offset = Offset::from_rbs_location(&key.location()).extend_end(1); let str_id = self.local_graph.intern_string(name); parameters.push(Parameter::RequiredKeyword(ParameterStruct::new(offset, str_id))); } for (key, _value) in function_node.optional_keywords().iter() { let name = self.source_at(&key.location()); - let offset = Offset::from_rbs_location(&key.location()); + // Extend offset by 1 to include the trailing colon, matching Ruby indexer behavior + let offset = Offset::from_rbs_location(&key.location()).extend_end(1); let str_id = self.local_graph.intern_string(name); parameters.push(Parameter::OptionalKeyword(ParameterStruct::new(offset, str_id))); } @@ -1257,12 +1259,12 @@ mod tests { assert_parameter!(&def.signatures().as_slice()[0][4], RequiredKeyword, |param| { assert_string_eq!(context, param.str(), "name"); - assert_offset_string!(context, param.offset(), "name"); + assert_offset_string!(context, param.offset(), "name:"); }); assert_parameter!(&def.signatures().as_slice()[0][5], OptionalKeyword, |param| { assert_string_eq!(context, param.str(), "age"); - assert_offset_string!(context, param.offset(), "age"); + assert_offset_string!(context, param.offset(), "age:"); }); assert_parameter!(&def.signatures().as_slice()[0][6], RestKeyword, |param| { @@ -1298,12 +1300,12 @@ mod tests { assert_parameter!(&def.signatures().as_slice()[0][4], RequiredKeyword, |param| { assert_string_eq!(context, param.str(), "name"); - assert_offset_string!(context, param.offset(), "name"); + assert_offset_string!(context, param.offset(), "name:"); }); assert_parameter!(&def.signatures().as_slice()[0][5], OptionalKeyword, |param| { assert_string_eq!(context, param.str(), "age"); - assert_offset_string!(context, param.offset(), "age"); + assert_offset_string!(context, param.offset(), "age:"); }); assert_parameter!(&def.signatures().as_slice()[0][6], RestKeyword, |param| { diff --git a/rust/rubydex/src/offset.rs b/rust/rubydex/src/offset.rs index 7bef5199..efdff4ca 100644 --- a/rust/rubydex/src/offset.rs +++ b/rust/rubydex/src/offset.rs @@ -74,6 +74,15 @@ impl Offset { self.end } + /// Returns a new offset with the end extended by the given number of bytes. + #[must_use] + pub const fn extend_end(self, bytes: u32) -> Self { + Self { + start: self.start, + end: self.end + bytes, + } + } + /// Converts an offset to a display range like `1:1-1:5` #[must_use] pub fn to_display_range(&self, document: &Document) -> String { From 17371dd9a0fcf3d597c9486328753a5c8e8db1b4 Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Tue, 24 Mar 2026 17:54:29 +0900 Subject: [PATCH 2/8] Add MethodDefinition#signatures to Ruby API Expose method signature information through the Ruby C extension. Each signature contains parameters as [kind, name, location] tuples, following the same format as Method#parameters with an additional location element. - Add Parameter::inner() to access ParameterStruct from any variant - Add Rust C API: ParameterKind, SignatureEntry, SignatureArray structs and rdx_definition_signatures/rdx_definition_signatures_free functions - Add Rubydex::Signature Ruby class with #parameters and #method_definition - Add rdxi_signatures_to_ruby shared C helper for SignatureArray conversion - Register MethodDefinition#signatures in the C extension Co-Authored-By: Claude Opus 4.6 (1M context) --- ext/rubydex/definition.c | 14 ++ ext/rubydex/rubydex.c | 2 + ext/rubydex/signature.c | 68 ++++++++++ ext/rubydex/signature.h | 17 +++ lib/rubydex.rb | 1 + lib/rubydex/signature.rb | 37 ++++++ rust/rubydex-sys/src/definition_api.rs | 169 +++++++++++++++++++++++- rust/rubydex/src/model/definitions.rs | 17 +++ test/definition_test.rb | 174 +++++++++++++++++++++++++ 9 files changed, 498 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/definition.c b/ext/rubydex/definition.c index 68402bd9..d283ba89 100644 --- a/ext/rubydex/definition.c +++ b/ext/rubydex/definition.c @@ -4,6 +4,7 @@ #include "location.h" #include "ruby/internal/scan_args.h" #include "rustbindings.h" +#include "signature.h" static VALUE mRubydex; VALUE cComment; @@ -161,6 +162,18 @@ static VALUE rdxr_definition_name_location(VALUE self) { return location; } +// 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; @@ -182,6 +195,7 @@ void rdxi_initialize_definition(VALUE mod) { cConstantDefinition = rb_define_class_under(mRubydex, "ConstantDefinition", cDefinition); cConstantAliasDefinition = rb_define_class_under(mRubydex, "ConstantAliasDefinition", 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/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..9e4f9d2e --- /dev/null +++ b/ext/rubydex/signature.c @@ -0,0 +1,68 @@ +#include "signature.h" +#include "definition.h" +#include "handle.h" +#include "location.h" + +VALUE cSignature; + +static VALUE parameter_kind_to_symbol(ParameterKind kind) { + switch (kind) { + case ParameterKind_Req: return ID2SYM(rb_intern("req")); + case ParameterKind_Opt: return ID2SYM(rb_intern("opt")); + case ParameterKind_Rest: return ID2SYM(rb_intern("rest")); + case ParameterKind_Keyreq: return ID2SYM(rb_intern("keyreq")); + case ParameterKind_Key: return ID2SYM(rb_intern("key")); + case ParameterKind_Keyrest: return ID2SYM(rb_intern("keyrest")); + case ParameterKind_Block: return ID2SYM(rb_intern("block")); + case ParameterKind_Forward: return ID2SYM(rb_intern("forward")); + 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 kind_sym = parameter_kind_to_symbol(param_entry.kind); + VALUE name_sym = ID2SYM(rb_intern(param_entry.name)); + VALUE location = rdxi_build_location_value(param_entry.location); + + rb_ary_push(parameters, rb_ary_new_from_args(3, kind_sym, name_sym, location)); + } + + VALUE sig_kwargs = rb_hash_new(); + rb_hash_aset(sig_kwargs, ID2SYM(rb_intern("parameters")), parameters); + rb_hash_aset(sig_kwargs, ID2SYM(rb_intern("method_definition")), method_def); + VALUE signature = rb_class_new_instance_kw(1, &sig_kwargs, cSignature, RB_PASS_KEYWORDS); + + 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); +} diff --git a/ext/rubydex/signature.h b/ext/rubydex/signature.h new file mode 100644 index 00000000..dced3122 --- /dev/null +++ b/ext/rubydex/signature.h @@ -0,0 +1,17 @@ +#ifndef RUBYDEX_SIGNATURE_H +#define RUBYDEX_SIGNATURE_H + +#include "ruby.h" +#include "rustbindings.h" + +extern VALUE cSignature; + +// 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 17d8e9f0..2a1e6d31 100644 --- a/lib/rubydex.rb +++ b/lib/rubydex.rb @@ -16,5 +16,6 @@ require "rubydex/failures" require "rubydex/location" require "rubydex/comment" +require "rubydex/signature" require "rubydex/diagnostic" require "rubydex/graph" diff --git a/lib/rubydex/signature.rb b/lib/rubydex/signature.rb new file mode 100644 index 00000000..1dfe119b --- /dev/null +++ b/lib/rubydex/signature.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Rubydex + # Represents a single signature of a method definition. + # + # A method definition may have multiple signatures when defined with overloads in RBS. + class Signature + # Returns the parameters of the signature. + # + # Each parameter is a 3-element array of `[kind, name, location]`, + # following the same format as `Method#parameters` with an additional location element. + # + # The kind symbol is one of: + # - `:req` — required positional parameter (`a`) or post-rest positional parameter (`d` in `def foo(*c, d)`) + # - `:opt` — optional positional parameter (`b = 1`) + # - `:rest` — rest positional parameter (`*c`) + # - `:keyreq` — required keyword parameter (`e:`) + # - `:key` — optional keyword parameter (`f: 1`) + # - `:keyrest` — rest keyword parameter (`**g`) + # - `:block` — block parameter (`&h`) + # - `:forward` — forward parameter (`...`), Rubydex-specific (Ruby expands this to `:rest`, `:keyrest`, `:block`) + # + #: Array[[Symbol, Symbol, Location]] + attr_reader :parameters + + # Returns the method definition this signature belongs to. + # + #: MethodDefinition + attr_reader :method_definition + + #: (parameters: Array[[Symbol, Symbol, Location]], method_definition: 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 c852322c..e1de8fee 100644 --- a/rust/rubydex-sys/src/definition_api.rs +++ b/rust/rubydex-sys/src/definition_api.rs @@ -3,7 +3,7 @@ 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; +use rubydex::model::definitions::{Definition, Parameter}; use rubydex::model::ids::DefinitionId; use std::ffi::CString; use std::ptr; @@ -348,3 +348,170 @@ pub unsafe extern "C" fn rdx_definition_name_location(pointer: GraphPointer, def create_location_for_uri_and_offset(graph, document, name_offset) }) } + +/// C-compatible enum representing the kind of a parameter. +#[repr(C)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum ParameterKind { + Req = 0, + Opt = 1, + Rest = 2, + Keyreq = 3, + Key = 4, + Keyrest = 5, + Block = 6, + Forward = 7, +} + +fn map_parameter_kind(param: &Parameter) -> ParameterKind { + match param { + Parameter::RequiredPositional(_) | Parameter::Post(_) => ParameterKind::Req, + Parameter::OptionalPositional(_) => ParameterKind::Opt, + Parameter::RestPositional(_) => ParameterKind::Rest, + Parameter::RequiredKeyword(_) => ParameterKind::Keyreq, + Parameter::OptionalKeyword(_) => ParameterKind::Key, + Parameter::RestKeyword(_) => ParameterKind::Keyrest, + 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(defn) = graph.definitions().get(&def_id) else { + panic!("Definition not found: {definition_id:?}"); + }; + + let Definition::Method(method_def) = defn 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. +pub(crate) 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 e64793ab..8287cad3 100644 --- a/rust/rubydex/src/model/definitions.rs +++ b/rust/rubydex/src/model/definitions.rs @@ -866,6 +866,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, diff --git a/test/definition_test.rb b/test/definition_test.rb index dc729c03..afd9d0a3 100644 --- a/test/definition_test.rb +++ b/test/definition_test.rb @@ -204,6 +204,180 @@ class NotDeprecated; end 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_equal([:req, :a], params[0][0..1]) + assert_equal("#{path}:1:9-1:10", params[0][2].to_display.to_s) # a + + assert_equal([:opt, :b], params[1][0..1]) + assert_equal("#{path}:1:12-1:13", params[1][2].to_display.to_s) # b + + assert_equal([:rest, :c], params[2][0..1]) + assert_equal("#{path}:1:20-1:21", params[2][2].to_display.to_s) # c + + assert_equal([:req, :d], params[3][0..1]) + assert_equal("#{path}:1:23-1:24", params[3][2].to_display.to_s) # d + + assert_equal([:keyreq, :e], params[4][0..1]) + assert_equal("#{path}:1:26-1:28", params[4][2].to_display.to_s) # e: + + assert_equal([:key, :f], params[5][0..1]) + assert_equal("#{path}:1:30-1:32", params[5][2].to_display.to_s) # f: + + assert_equal([:keyrest, :g], params[6][0..1]) + assert_equal("#{path}:1:38-1:39", params[6][2].to_display.to_s) # g + + assert_equal([:block, :h], params[7][0..1]) + assert_equal("#{path}:1:42-1:43", params[7][2].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_equal(:forward, params[0][0]) + assert_equal(:"...", params[0][1]) + assert_equal("#{path}:1:9-1:12", params[0][2].to_display.to_s) # ... + end + end + + def test_method_definition_signatures_from_rbs + graph = Rubydex::Graph.new + graph.index_source("file:///foo.rbs", <<~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.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) + + assert_equal([:req, :a], params[0][0..1]) + assert_equal("/foo.rbs:2:20-2:21", params[0][2].to_display.to_s) # a + assert_equal([:opt, :b], params[1][0..1]) + assert_equal("/foo.rbs:2:31-2:32", params[1][2].to_display.to_s) # b + assert_equal([:rest, :c], params[2][0..1]) + assert_equal("/foo.rbs:2:42-2:43", params[2][2].to_display.to_s) # c + assert_equal([:req, :d], params[3][0..1]) + assert_equal("/foo.rbs:2:52-2:53", params[3][2].to_display.to_s) # d + assert_equal([:keyreq, :name], params[4][0..1]) + assert_equal("/foo.rbs:2:55-2:60", params[4][2].to_display.to_s) # name: + assert_equal([:key, :mode], params[5][0..1]) + assert_equal("/foo.rbs:2:70-2:75", params[5][2].to_display.to_s) # mode: + assert_equal([:keyrest, :opts], params[6][0..1]) + assert_equal("/foo.rbs:2:93-2:97", params[6][2].to_display.to_s) # opts + assert_equal([:block, :block], params[7][0..1]) + assert_equal("/foo.rbs:2:99-2:119", params[7][2].to_display.to_s) # { (String) -> void } + end + + def test_method_definition_signatures_from_rbs_with_untyped_parameters + graph = Rubydex::Graph.new + graph.index_source("file:///foo.rbs", <<~RBS, "rbs") + class Foo + def baz: (?) -> void + end + 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 + + def test_method_definition_signatures_from_rbs_with_overloads + graph = Rubydex::Graph.new + graph.index_source("file:///foo.rbs", <<~RBS, "rbs") + class Foo + def bar: (String name) -> void + | (Integer id, ?Symbol mode) -> String + end + RBS + graph.resolve + + method_def = graph["Foo#bar()"].definitions.first + refute_nil(method_def) + + signatures = method_def.signatures + assert_equal(2, signatures.length) + + params0 = signatures[0].parameters + assert_equal(1, params0.length) + assert_equal([:req, :name], params0[0][0..1]) + assert_equal("/foo.rbs:2:20-2:24", params0[0][2].to_display.to_s) # name + + params1 = signatures[1].parameters + assert_equal(2, params1.length) + assert_equal([:req, :id], params1[0][0..1]) + assert_equal("/foo.rbs:3:21-3:23", params1[0][2].to_display.to_s) # id + assert_equal([:opt, :mode], params1[1][0..1]) + assert_equal("/foo.rbs:3:33-3:37", params1[1][2].to_display.to_s) # mode + end + private # Comment locations on Windows include the carriage return. This means that the end column is off by one when compared From d1f4139681d3a1fbf9e1e9f53ea606e8adbf5846 Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Tue, 24 Mar 2026 17:54:46 +0900 Subject: [PATCH 3/8] Add Rubydex::Method#signatures with alias resolution Add signatures method to method declarations (Rubydex::Method) that aggregates signatures from all definitions, resolving method aliases to the original method's signatures. - Add query::method_definitions() in Rust for alias-aware lookup - Add rdx_declaration_method_signatures C API function - Register Method#signatures in the C extension Co-Authored-By: Claude Opus 4.6 (1M context) --- ext/rubydex/declaration.c | 14 ++ rust/rubydex-sys/src/declaration_api.rs | 43 +++++- rust/rubydex-sys/src/definition_api.rs | 2 +- rust/rubydex/src/query.rs | 171 +++++++++++++++++++++++- test/declaration_test.rb | 86 ++++++++++++ 5 files changed, 312 insertions(+), 4 deletions(-) diff --git a/ext/rubydex/declaration.c b/ext/rubydex/declaration.c index 46529ec5..e0543cb8 100644 --- a/ext/rubydex/declaration.c +++ b/ext/rubydex/declaration.c @@ -4,6 +4,7 @@ #include "handle.h" #include "reference.h" #include "rustbindings.h" +#include "signature.h" #include "utils.h" VALUE cDeclaration; @@ -331,6 +332,18 @@ static VALUE rdxr_declaration_references(VALUE self) { return self; } +// Method#signatures -> [Rubydex::Signature] +static VALUE rdxr_method_declaration_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_declaration_method_signatures(graph, data->id); + return rdxi_signatures_to_ruby(arr, data->graph_obj, Qnil); +} + void rdxi_initialize_declaration(VALUE mRubydex) { cDeclaration = rb_define_class_under(mRubydex, "Declaration", rb_cObject); cNamespace = rb_define_class_under(mRubydex, "Namespace", cDeclaration); @@ -341,6 +354,7 @@ void rdxi_initialize_declaration(VALUE mRubydex) { cConstant = rb_define_class_under(mRubydex, "Constant", cDeclaration); cConstantAlias = rb_define_class_under(mRubydex, "ConstantAlias", cDeclaration); cMethod = rb_define_class_under(mRubydex, "Method", cDeclaration); + rb_define_method(cMethod, "signatures", rdxr_method_declaration_signatures, 0); cGlobalVariable = rb_define_class_under(mRubydex, "GlobalVariable", cDeclaration); cInstanceVariable = rb_define_class_under(mRubydex, "InstanceVariable", cDeclaration); cClassVariable = rb_define_class_under(mRubydex, "ClassVariable", cDeclaration); diff --git a/rust/rubydex-sys/src/declaration_api.rs b/rust/rubydex-sys/src/declaration_api.rs index cc0308ce..4ee640aa 100644 --- a/rust/rubydex-sys/src/declaration_api.rs +++ b/rust/rubydex-sys/src/declaration_api.rs @@ -5,7 +5,9 @@ use rubydex::model::declaration::{Ancestor, Declaration, Namespace}; use std::ffi::CString; use std::ptr; -use crate::definition_api::{DefinitionsIter, rdx_definitions_iter_new_from_ids}; +use crate::definition_api::{ + DefinitionsIter, SignatureArray, SignatureEntry, collect_method_signatures, rdx_definitions_iter_new_from_ids, +}; use crate::graph_api::{GraphPointer, with_graph}; use crate::reference_api::{CReference, ReferenceKind, ReferencesIter}; use crate::utils; @@ -439,3 +441,42 @@ pub unsafe extern "C" fn rdx_declaration_references_iter_new( ReferencesIter::new(entries.into_boxed_slice()) }) } + +/// Returns a newly allocated array of signatures for the given method declaration id. +/// Aggregates signatures from all definitions. For alias definitions, resolves to the +/// original method's signatures. +/// Returns NULL if the declaration is not a method declaration. +/// Caller must free the returned pointer with `rdx_definition_signatures_free`. +/// +/// # Safety +/// - `pointer` must be a valid pointer previously returned by `rdx_graph_new`. +/// - `declaration_id` must be a valid declaration id. +/// +/// # Panics +/// This function will panic if declarations or definitions cannot be found. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn rdx_declaration_method_signatures( + pointer: GraphPointer, + declaration_id: u64, +) -> *mut SignatureArray { + with_graph(pointer, |graph| { + let decl_id = DeclarationId::new(declaration_id); + let method_defs = rubydex::query::method_definitions(graph, decl_id); + + if method_defs.is_empty() { + return ptr::null_mut(); + } + + let mut sig_entries: Vec = Vec::new(); + for (def_id, method_def) in &method_defs { + collect_method_signatures(graph, method_def, **def_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 })) + }) +} diff --git a/rust/rubydex-sys/src/definition_api.rs b/rust/rubydex-sys/src/definition_api.rs index e1de8fee..7199967b 100644 --- a/rust/rubydex-sys/src/definition_api.rs +++ b/rust/rubydex-sys/src/definition_api.rs @@ -433,7 +433,7 @@ 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. +/// Helper: build signature entries from a `MethodDefinition` and append them to the output vector. pub(crate) fn collect_method_signatures( graph: &rubydex::model::graph::Graph, method_def: &rubydex::model::definitions::MethodDefinition, diff --git a/rust/rubydex/src/query.rs b/rust/rubydex/src/query.rs index 2859b604..d31992b7 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, MethodDefinition, Parameter}; 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; @@ -207,6 +207,78 @@ macro_rules! collect_candidates { }; } +/// Returns the method definitions associated with a method declaration, resolving aliases. +/// +/// For regular method declarations, returns the `MethodDefinition`s directly. +/// For alias declarations, follows the alias to the original method and returns its definitions. +/// +/// # Panics +/// +/// Panics if: +/// - the `declaration_id` does not exist in the graph +/// - the declaration is not a method declaration +/// - any definition or owner declaration referenced by the method is missing from the graph +#[must_use] +pub fn method_definitions(graph: &Graph, declaration_id: DeclarationId) -> Vec<(DefinitionId, &MethodDefinition)> { + let decl = graph + .declarations() + .get(&declaration_id) + .expect("declaration should exist in graph"); + assert!( + matches!(decl, Declaration::Method(_)), + "expected a method declaration, got {:?}", + decl.kind() + ); + + let owner_id = *decl.owner_id(); + let mut result = Vec::new(); + + for def_id in decl.definitions() { + let defn = graph + .definitions() + .get(def_id) + .expect("definition should exist in graph"); + + match defn { + Definition::Method(method_def) => { + result.push((*def_id, method_def.as_ref())); + } + Definition::MethodAlias(alias_def) => { + // Resolve alias: look up the original method in the owner's members + let owner = graph + .declarations() + .get(&owner_id) + .expect("owner declaration should exist") + .as_namespace() + .expect("owner should be a namespace"); + + // Alias target may not exist (e.g. aliasing an undefined method) + let Some(original_decl_id) = owner.member(alias_def.old_name_str_id()) else { + continue; + }; + + let original_decl = graph + .declarations() + .get(original_decl_id) + .expect("original declaration should exist"); + + for original_def_id in original_decl.definitions() { + let original_defn = graph + .definitions() + .get(original_def_id) + .expect("original definition should exist in graph"); + if let Definition::Method(original_method_def) = original_defn { + result.push((*original_def_id, original_method_def.as_ref())); + } + } + } + _ => {} + } + } + + result +} + /// Determines all possible completion candidates based on the current context of the cursor. There are multiple cases /// that change what has to be collected for completion: /// @@ -1621,4 +1693,99 @@ mod tests { assert!(!candidates.iter().any(|c| matches!(c, CompletionCandidate::Keyword(_)))); } + + /// Helper to get source text at an offset + fn source_at<'a>(source: &'a str, offset: &crate::offset::Offset) -> &'a str { + &source[offset.start() as usize..offset.end() as usize] + } + + #[test] + fn test_method_definitions_returns_method_definitions() { + let mut context = GraphTest::new(); + // 0123456789... + let source = "class Foo\n def bar(a, b); end\nend\n"; + context.index_uri("file:///foo.rb", source); + context.resolve(); + + let defs = method_definitions(context.graph(), DeclarationId::from("Foo#bar()")); + assert_eq!(1, defs.len()); + assert_eq!("def bar(a, b); end", source_at(source, defs[0].1.offset())); + } + + #[test] + fn test_method_definitions_resolves_alias() { + let mut context = GraphTest::new(); + let source = "class Foo\n def bar(a, b); end\n alias_method :baz, :bar\nend\n"; + context.index_uri("file:///foo.rb", source); + context.resolve(); + + let defs = method_definitions(context.graph(), DeclarationId::from("Foo#baz()")); + assert_eq!(1, defs.len()); + // Returns the original method's definition + assert_eq!("def bar(a, b); end", source_at(source, defs[0].1.offset())); + } + + #[test] + fn test_method_definitions_alias_to_undefined_method() { + let mut context = GraphTest::new(); + context.index_uri("file:///foo.rb", "class Foo\n alias_method :baz, :nonexistent\nend\n"); + context.resolve(); + + let defs = method_definitions(context.graph(), DeclarationId::from("Foo#baz()")); + assert!(defs.is_empty()); + } + + #[test] + fn test_method_definitions_with_override() { + let mut context = GraphTest::new(); + let source = "class Foo\n def bar(a); end\n def bar(a, b); end\nend\n"; + context.index_uri("file:///foo.rb", source); + context.resolve(); + + let defs = method_definitions(context.graph(), DeclarationId::from("Foo#bar()")); + assert_eq!(2, defs.len()); + + let mut texts: Vec<&str> = defs.iter().map(|(_, d)| source_at(source, d.offset())).collect(); + texts.sort(); + assert_eq!(vec!["def bar(a); end", "def bar(a, b); end"], texts); + } + + #[test] + #[should_panic(expected = "expected a method declaration")] + fn test_method_definitions_panics_for_non_method() { + let mut context = GraphTest::new(); + context.index_uri("file:///foo.rb", "class Foo; end"); + context.resolve(); + + method_definitions(context.graph(), DeclarationId::from("Foo")); + } + + #[test] + fn test_method_definitions_from_rbs() { + let mut context = GraphTest::new(); + let source = "class Foo\n def bar: (String name) -> void\n | (Integer id) -> String\nend\n"; + context.index_rbs_uri("file:///foo.rbs", source); + context.resolve(); + + let defs = method_definitions(context.graph(), DeclarationId::from("Foo#bar()")); + assert_eq!(1, defs.len()); + assert_eq!( + "def bar: (String name) -> void\n | (Integer id) -> String", + source_at(source, defs[0].1.offset()) + ); + } + + #[test] + fn test_method_definitions_from_rbs_alias() { + let mut context = GraphTest::new(); + let rb_source = "class Foo\n def bar(a, b); end\nend\n"; + let rbs_source = "class Foo\n alias baz bar\nend\n"; + context.index_uri("file:///foo.rb", rb_source); + context.index_rbs_uri("file:///foo.rbs", rbs_source); + context.resolve(); + + let defs = method_definitions(context.graph(), DeclarationId::from("Foo#baz()")); + assert_eq!(1, defs.len()); + assert_eq!("def bar(a, b); end", source_at(rb_source, defs[0].1.offset())); + } } diff --git a/test/declaration_test.rb b/test/declaration_test.rb index 61ef15c9..bcdd5159 100644 --- a/test/declaration_test.rb +++ b/test/declaration_test.rb @@ -538,4 +538,90 @@ def initialize assert_equal("Foo#initialize()", decl.name) end end + + def test_method_declaration_signatures + with_context do |context| + context.write!("file1.rb", <<~RUBY) + class Foo + def bar(a, b = 1); end + end + RUBY + + graph = Rubydex::Graph.new + graph.index_all(context.glob("**/*.rb")) + graph.resolve + + decl = graph["Foo#bar()"] + assert_instance_of(Rubydex::Method, decl) + + signatures = decl.signatures + assert_equal(1, signatures.length) + + sig = signatures.first + assert_instance_of(Rubydex::Signature, sig) + assert_instance_of(Rubydex::MethodDefinition, sig.method_definition) + + params = sig.parameters + assert_equal(2, params.length) + assert_equal([:req, :a], params[0][0..1]) + assert_equal([:opt, :b], params[1][0..1]) + end + end + + def test_method_declaration_signatures_with_alias + with_context do |context| + context.write!("file1.rb", <<~RUBY) + class Foo + def bar(a, b); end + alias_method :baz, :bar + end + RUBY + + graph = Rubydex::Graph.new + graph.index_all(context.glob("**/*.rb")) + graph.resolve + + # Alias resolves to original method's signatures + decl = graph["Foo#baz()"] + assert_instance_of(Rubydex::Method, decl) + + signatures = decl.signatures + assert_equal(1, signatures.length) + + params = signatures.first.parameters + assert_equal(2, params.length) + assert_equal([:req, :a], params[0][0..1]) + assert_equal([:req, :b], params[1][0..1]) + + # method_definition points to the original MethodDefinition + assert_instance_of(Rubydex::MethodDefinition, signatures.first.method_definition) + assert_equal("bar()", signatures.first.method_definition.name) + end + end + + def test_method_declaration_signatures_with_override + with_context do |context| + context.write!("file1.rb", <<~RUBY) + class Foo + def bar(a); end + def bar(a, b); end + end + RUBY + + graph = Rubydex::Graph.new + graph.index_all(context.glob("**/*.rb")) + graph.resolve + + decl = graph["Foo#bar()"] + signatures = decl.signatures + assert_equal(2, signatures.length) + + # Each signature comes from a different definition + params0 = signatures[0].parameters + params1 = signatures[1].parameters + + param_counts = [params0.length, params1.length].sort + assert_equal([1, 2], param_counts) + end + end end From f7fe522390cae54db79c80e82469d3ff5616fc4b Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Wed, 25 Mar 2026 16:28:40 +0900 Subject: [PATCH 4/8] Update rust/rubydex-sys/src/declaration_api.rs Co-authored-by: Alexandre Terrasa <583144+Morriar@users.noreply.github.com> --- rust/rubydex-sys/src/declaration_api.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/rubydex-sys/src/declaration_api.rs b/rust/rubydex-sys/src/declaration_api.rs index 4ee640aa..61b29f0a 100644 --- a/rust/rubydex-sys/src/declaration_api.rs +++ b/rust/rubydex-sys/src/declaration_api.rs @@ -469,7 +469,7 @@ pub unsafe extern "C" fn rdx_declaration_method_signatures( let mut sig_entries: Vec = Vec::new(); for (def_id, method_def) in &method_defs { - collect_method_signatures(graph, method_def, **def_id, &mut sig_entries); + collect_method_signatures(graph, method_def, def_id.get(), &mut sig_entries); } let mut boxed = sig_entries.into_boxed_slice(); From 8eb33921ad3b2927b73cb50c357144173cb4cf32 Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Wed, 25 Mar 2026 17:03:58 +0900 Subject: [PATCH 5/8] Returns `Vec<&MethodDefinition>` --- rust/rubydex-sys/src/declaration_api.rs | 4 ++-- rust/rubydex/src/query.rs | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/rust/rubydex-sys/src/declaration_api.rs b/rust/rubydex-sys/src/declaration_api.rs index 61b29f0a..0288d371 100644 --- a/rust/rubydex-sys/src/declaration_api.rs +++ b/rust/rubydex-sys/src/declaration_api.rs @@ -468,8 +468,8 @@ pub unsafe extern "C" fn rdx_declaration_method_signatures( } let mut sig_entries: Vec = Vec::new(); - for (def_id, method_def) in &method_defs { - collect_method_signatures(graph, method_def, def_id.get(), &mut sig_entries); + for method_def in &method_defs { + collect_method_signatures(graph, method_def, method_def.id().get(), &mut sig_entries); } let mut boxed = sig_entries.into_boxed_slice(); diff --git a/rust/rubydex/src/query.rs b/rust/rubydex/src/query.rs index d31992b7..62395a92 100644 --- a/rust/rubydex/src/query.rs +++ b/rust/rubydex/src/query.rs @@ -9,7 +9,7 @@ use crate::model::declaration::{Ancestor, Declaration}; use crate::model::definitions::{Definition, MethodDefinition, Parameter}; use crate::model::graph::{Graph, OBJECT_ID}; use crate::model::identity_maps::IdentityHashSet; -use crate::model::ids::{DeclarationId, DefinitionId, NameId, StringId, UriId}; +use crate::model::ids::{DeclarationId, NameId, StringId, UriId}; use crate::model::keywords::{self, Keyword}; use crate::model::name::NameRef; @@ -219,7 +219,7 @@ macro_rules! collect_candidates { /// - the declaration is not a method declaration /// - any definition or owner declaration referenced by the method is missing from the graph #[must_use] -pub fn method_definitions(graph: &Graph, declaration_id: DeclarationId) -> Vec<(DefinitionId, &MethodDefinition)> { +pub fn method_definitions(graph: &Graph, declaration_id: DeclarationId) -> Vec<&MethodDefinition> { let decl = graph .declarations() .get(&declaration_id) @@ -241,7 +241,7 @@ pub fn method_definitions(graph: &Graph, declaration_id: DeclarationId) -> Vec<( match defn { Definition::Method(method_def) => { - result.push((*def_id, method_def.as_ref())); + result.push(method_def.as_ref()); } Definition::MethodAlias(alias_def) => { // Resolve alias: look up the original method in the owner's members @@ -268,7 +268,7 @@ pub fn method_definitions(graph: &Graph, declaration_id: DeclarationId) -> Vec<( .get(original_def_id) .expect("original definition should exist in graph"); if let Definition::Method(original_method_def) = original_defn { - result.push((*original_def_id, original_method_def.as_ref())); + result.push(original_method_def.as_ref()); } } } @@ -1709,7 +1709,7 @@ mod tests { let defs = method_definitions(context.graph(), DeclarationId::from("Foo#bar()")); assert_eq!(1, defs.len()); - assert_eq!("def bar(a, b); end", source_at(source, defs[0].1.offset())); + assert_eq!("def bar(a, b); end", source_at(source, defs[0].offset())); } #[test] @@ -1722,7 +1722,7 @@ mod tests { let defs = method_definitions(context.graph(), DeclarationId::from("Foo#baz()")); assert_eq!(1, defs.len()); // Returns the original method's definition - assert_eq!("def bar(a, b); end", source_at(source, defs[0].1.offset())); + assert_eq!("def bar(a, b); end", source_at(source, defs[0].offset())); } #[test] @@ -1745,7 +1745,7 @@ mod tests { let defs = method_definitions(context.graph(), DeclarationId::from("Foo#bar()")); assert_eq!(2, defs.len()); - let mut texts: Vec<&str> = defs.iter().map(|(_, d)| source_at(source, d.offset())).collect(); + let mut texts: Vec<&str> = defs.iter().map(|d| source_at(source, d.offset())).collect(); texts.sort(); assert_eq!(vec!["def bar(a); end", "def bar(a, b); end"], texts); } @@ -1757,7 +1757,7 @@ mod tests { context.index_uri("file:///foo.rb", "class Foo; end"); context.resolve(); - method_definitions(context.graph(), DeclarationId::from("Foo")); + let _ = method_definitions(context.graph(), DeclarationId::from("Foo")); } #[test] @@ -1771,7 +1771,7 @@ mod tests { assert_eq!(1, defs.len()); assert_eq!( "def bar: (String name) -> void\n | (Integer id) -> String", - source_at(source, defs[0].1.offset()) + source_at(source, defs[0].offset()) ); } @@ -1786,6 +1786,6 @@ mod tests { let defs = method_definitions(context.graph(), DeclarationId::from("Foo#baz()")); assert_eq!(1, defs.len()); - assert_eq!("def bar(a, b); end", source_at(rb_source, defs[0].1.offset())); + assert_eq!("def bar(a, b); end", source_at(rb_source, defs[0].offset())); } } From 3463eed81d4a7a57d1dcde83429f501fd89f6890 Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Wed, 25 Mar 2026 17:12:56 +0900 Subject: [PATCH 6/8] Fix test for windows --- test/definition_test.rb | 162 ++++++++++++++++++++++------------------ 1 file changed, 89 insertions(+), 73 deletions(-) diff --git a/test/definition_test.rb b/test/definition_test.rb index afd9d0a3..fa59b600 100644 --- a/test/definition_test.rb +++ b/test/definition_test.rb @@ -297,85 +297,101 @@ def baz(...); end end def test_method_definition_signatures_from_rbs - graph = Rubydex::Graph.new - graph.index_source("file:///foo.rbs", <<~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.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) - - assert_equal([:req, :a], params[0][0..1]) - assert_equal("/foo.rbs:2:20-2:21", params[0][2].to_display.to_s) # a - assert_equal([:opt, :b], params[1][0..1]) - assert_equal("/foo.rbs:2:31-2:32", params[1][2].to_display.to_s) # b - assert_equal([:rest, :c], params[2][0..1]) - assert_equal("/foo.rbs:2:42-2:43", params[2][2].to_display.to_s) # c - assert_equal([:req, :d], params[3][0..1]) - assert_equal("/foo.rbs:2:52-2:53", params[3][2].to_display.to_s) # d - assert_equal([:keyreq, :name], params[4][0..1]) - assert_equal("/foo.rbs:2:55-2:60", params[4][2].to_display.to_s) # name: - assert_equal([:key, :mode], params[5][0..1]) - assert_equal("/foo.rbs:2:70-2:75", params[5][2].to_display.to_s) # mode: - assert_equal([:keyrest, :opts], params[6][0..1]) - assert_equal("/foo.rbs:2:93-2:97", params[6][2].to_display.to_s) # opts - assert_equal([:block, :block], params[7][0..1]) - assert_equal("/foo.rbs:2:99-2:119", params[7][2].to_display.to_s) # { (String) -> void } + 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_equal([:req, :a], params[0][0..1]) + assert_equal("#{path}:2:20-2:21", params[0][2].to_display.to_s) # a + assert_equal([:opt, :b], params[1][0..1]) + assert_equal("#{path}:2:31-2:32", params[1][2].to_display.to_s) # b + assert_equal([:rest, :c], params[2][0..1]) + assert_equal("#{path}:2:42-2:43", params[2][2].to_display.to_s) # c + assert_equal([:req, :d], params[3][0..1]) + assert_equal("#{path}:2:52-2:53", params[3][2].to_display.to_s) # d + assert_equal([:keyreq, :name], params[4][0..1]) + assert_equal("#{path}:2:55-2:60", params[4][2].to_display.to_s) # name: + assert_equal([:key, :mode], params[5][0..1]) + assert_equal("#{path}:2:70-2:75", params[5][2].to_display.to_s) # mode: + assert_equal([:keyrest, :opts], params[6][0..1]) + assert_equal("#{path}:2:93-2:97", params[6][2].to_display.to_s) # opts + assert_equal([:block, :block], params[7][0..1]) + assert_equal("#{path}:2:99-2:119", params[7][2].to_display.to_s) # { (String) -> void } + end end def test_method_definition_signatures_from_rbs_with_untyped_parameters - graph = Rubydex::Graph.new - graph.index_source("file:///foo.rbs", <<~RBS, "rbs") - class Foo - def baz: (?) -> void - end - 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) + 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 - graph = Rubydex::Graph.new - graph.index_source("file:///foo.rbs", <<~RBS, "rbs") - class Foo - def bar: (String name) -> void - | (Integer id, ?Symbol mode) -> String - end - RBS - graph.resolve - - method_def = graph["Foo#bar()"].definitions.first - refute_nil(method_def) - - signatures = method_def.signatures - assert_equal(2, signatures.length) - - params0 = signatures[0].parameters - assert_equal(1, params0.length) - assert_equal([:req, :name], params0[0][0..1]) - assert_equal("/foo.rbs:2:20-2:24", params0[0][2].to_display.to_s) # name - - params1 = signatures[1].parameters - assert_equal(2, params1.length) - assert_equal([:req, :id], params1[0][0..1]) - assert_equal("/foo.rbs:3:21-3:23", params1[0][2].to_display.to_s) # id - assert_equal([:opt, :mode], params1[1][0..1]) - assert_equal("/foo.rbs:3:33-3:37", params1[1][2].to_display.to_s) # mode + 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_equal([:req, :name], params0[0][0..1]) + assert_equal("#{path}:2:20-2:24", params0[0][2].to_display.to_s) # name + + params1 = signatures[1].parameters + assert_equal(2, params1.length) + assert_equal([:req, :id], params1[0][0..1]) + assert_equal("#{path}:3:21-3:23", params1[0][2].to_display.to_s) # id + assert_equal([:opt, :mode], params1[1][0..1]) + assert_equal("#{path}:3:33-3:37", params1[1][2].to_display.to_s) # mode + end end private From f741daa405e54b7ec860756a4d06765f7c0589a5 Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Wed, 25 Mar 2026 17:20:09 +0900 Subject: [PATCH 7/8] Add tests with RBS --- test/declaration_test.rb | 55 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/test/declaration_test.rb b/test/declaration_test.rb index bcdd5159..ca05bae4 100644 --- a/test/declaration_test.rb +++ b/test/declaration_test.rb @@ -599,6 +599,61 @@ def bar(a, b); end end end + def test_method_declaration_signatures_from_rbs + with_context do |context| + context.write!("foo.rbs", <<~RBS) + class Foo + def bar: (String a, ?Integer b) -> void + end + RBS + + graph = Rubydex::Graph.new + graph.index_all(context.glob("**/*.rbs")) + graph.resolve + + decl = graph["Foo#bar()"] + assert_instance_of(Rubydex::Method, decl) + + signatures = decl.signatures + assert_equal(1, signatures.length) + + params = signatures.first.parameters + assert_equal(2, params.length) + assert_equal([:req, :a], params[0][0..1]) + assert_equal([:opt, :b], params[1][0..1]) + end + end + + def test_method_declaration_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 + + decl = graph["Foo#bar()"] + assert_instance_of(Rubydex::Method, decl) + + signatures = decl.signatures + assert_equal(2, signatures.length) + + params0 = signatures[0].parameters + assert_equal(1, params0.length) + assert_equal([:req, :name], params0[0][0..1]) + + params1 = signatures[1].parameters + assert_equal(2, params1.length) + assert_equal([:req, :id], params1[0][0..1]) + assert_equal([:opt, :mode], params1[1][0..1]) + end + end + def test_method_declaration_signatures_with_override with_context do |context| context.write!("file1.rb", <<~RUBY) From ffd8461613a6dab43080922527b6e39da6fcd601 Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Wed, 25 Mar 2026 17:26:20 +0900 Subject: [PATCH 8/8] fix query.rs --- rust/rubydex/src/query.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rust/rubydex/src/query.rs b/rust/rubydex/src/query.rs index 62395a92..69bddd4a 100644 --- a/rust/rubydex/src/query.rs +++ b/rust/rubydex/src/query.rs @@ -1746,7 +1746,7 @@ mod tests { assert_eq!(2, defs.len()); let mut texts: Vec<&str> = defs.iter().map(|d| source_at(source, d.offset())).collect(); - texts.sort(); + texts.sort_unstable(); assert_eq!(vec!["def bar(a); end", "def bar(a, b); end"], texts); } @@ -1778,14 +1778,14 @@ mod tests { #[test] fn test_method_definitions_from_rbs_alias() { let mut context = GraphTest::new(); - let rb_source = "class Foo\n def bar(a, b); end\nend\n"; + let ruby_source = "class Foo\n def bar(a, b); end\nend\n"; let rbs_source = "class Foo\n alias baz bar\nend\n"; - context.index_uri("file:///foo.rb", rb_source); + context.index_uri("file:///foo.rb", ruby_source); context.index_rbs_uri("file:///foo.rbs", rbs_source); context.resolve(); let defs = method_definitions(context.graph(), DeclarationId::from("Foo#baz()")); assert_eq!(1, defs.len()); - assert_eq!("def bar(a, b); end", source_at(rb_source, defs[0].offset())); + assert_eq!("def bar(a, b); end", source_at(ruby_source, defs[0].offset())); } }