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/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/declaration_api.rs b/rust/rubydex-sys/src/declaration_api.rs index cc0308ce..0288d371 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 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(); + 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 c852322c..7199967b 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/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/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/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 { diff --git a/rust/rubydex/src/query.rs b/rust/rubydex/src/query.rs index 2859b604..69bddd4a 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, 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}; @@ -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<&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(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_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].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].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_unstable(); + 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(); + + let _ = 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].offset()) + ); + } + + #[test] + fn test_method_definitions_from_rbs_alias() { + let mut context = GraphTest::new(); + 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", 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(ruby_source, defs[0].offset())); + } } diff --git a/test/declaration_test.rb b/test/declaration_test.rb index 61ef15c9..ca05bae4 100644 --- a/test/declaration_test.rb +++ b/test/declaration_test.rb @@ -538,4 +538,145 @@ 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_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) + 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 diff --git a/test/definition_test.rb b/test/definition_test.rb index dc729c03..fa59b600 100644 --- a/test/definition_test.rb +++ b/test/definition_test.rb @@ -204,6 +204,196 @@ 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 + 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 + 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_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 # Comment locations on Windows include the carriage return. This means that the end column is off by one when compared