diff --git a/ext/rubydex/definition.c b/ext/rubydex/definition.c index 5a608c38..e6d7a804 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; @@ -164,6 +165,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; @@ -186,6 +199,7 @@ void rdxi_initialize_definition(VALUE mod) { cConstantAliasDefinition = rb_define_class_under(mRubydex, "ConstantAliasDefinition", cDefinition); cConstantVisibilityDefinition = rb_define_class_under(mRubydex, "ConstantVisibilityDefinition", 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..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 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..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 732ecac8..86d56ed1 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; @@ -350,3 +350,166 @@ 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 { + 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 55876ebb..73530493 100644 --- a/rust/rubydex/src/model/definitions.rs +++ b/rust/rubydex/src/model/definitions.rs @@ -944,6 +944,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..9ce0306e 100644 --- a/test/definition_test.rb +++ b/test/definition_test.rb @@ -204,6 +204,215 @@ 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_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:28", 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:32", 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