Conversation
- Rename ParameterKind variants to full names (e.g. Req -> RequiredPositional) - Merge two let-else bindings into one in rdx_definition_signatures - Make collect_method_signatures private - Change Signature#initialize from keyword arguments to positional arguments Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
79877ae to
39084db
Compare
ext/rubydex/signature.c
Outdated
| } | ||
|
|
||
| VALUE rdxi_signatures_to_ruby(SignatureArray *arr, VALUE graph_obj, VALUE default_method_def) { | ||
| if (arr == NULL || arr->len == 0) { |
There was a problem hiding this comment.
How about we raise in rdxr_method_definition_signatures and rdxr_method_alias_definition_signatures if the pointer is NULL?
This should never really happen, so I'm trying to avoid having it fail silently.
There was a problem hiding this comment.
Yeah, I think the design is weird. Will take a look at this tonight.
There was a problem hiding this comment.
Fixed the implementation. rdx_method_definition_signatures and rdx_method_alias_definition_signatures always return an array. rdxi_signatures_to_ruby can safely skips checking if arr is NULL.
| let mut boxed = sig_entries.into_boxed_slice(); | ||
| let len = boxed.len(); | ||
| let items_ptr = boxed.as_mut_ptr(); | ||
| std::mem::forget(boxed); |
There was a problem hiding this comment.
Do we need to forget here if we're ultimately passing this to Box::into_raw?
There was a problem hiding this comment.
If we don't use forget, the contents of the Box will be deallocated by the destructor.
We could use Box::into_raw instead, but I don't think it's worth the rewrite since the mem::forget pattern is already used elsewhere in the codebase.
let items_ptr = Box::into_raw(boxed) as *mut SignatureEntry;
ext/rubydex/signature.c
Outdated
| 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)); |
There was a problem hiding this comment.
Does rb_intern make a copy of the string we pass? I'm asking because we free the signature array object further below. If Ruby interning just keeps a pointer, it could lead to a seg fault.
There was a problem hiding this comment.
I believe rb_intern copies the string, but I couldn't find any documentation that explicitly says so. Changed to rb_str_intern with rb_utf8_str_new_cstr, which creates dynamic symbols (subject to GC) from a copied C string.
| /// | ||
| /// Panics if the definition or its document does not exist. | ||
| #[must_use] | ||
| pub fn source_at(&self, definition_id: &DefinitionId) -> &str { |
There was a problem hiding this comment.
I think we can move this to Offset and pass the document as we do with https://github.com/Shopify/rubydex/blob/main/rust/rubydex/src/offset.rs#L90-L103
b33e8c6 to
e0b3e8a
Compare
| } | ||
|
|
||
| /// Returns a newly allocated array of signatures for the given method definition id. | ||
| /// Returns NULL if the definition is not a method definition. |
| end | ||
| end | ||
|
|
||
| def test_method_definition_signatures_with_various_parameter_kinds |
There was a problem hiding this comment.
Can we include test cases for singleton methods?
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
eef4e19 to
aeeebb3
Compare
| /// C-compatible struct representing a single method signature (a list of parameters). | ||
| #[repr(C)] | ||
| pub struct SignatureEntry { | ||
| pub definition_id: u64, |
There was a problem hiding this comment.
Looks like this is never used. Do we keep it for future updates?
If that's the case, please add a comment for that.
If not, let's remove it and simplify related API.
There was a problem hiding this comment.
Yeah, I think we can remove this one.
| /// C-compatible struct representing a single method signature (a list of parameters). | ||
| #[repr(C)] | ||
| pub struct SignatureEntry { | ||
| pub definition_id: u64, |
There was a problem hiding this comment.
Yeah, I think we can remove this one.
| self.sources.insert(uri.to_string(), source); | ||
| } | ||
|
|
||
| /// Returns the normalized source for the given URI. | ||
| /// | ||
| /// # Panics | ||
| /// | ||
| /// Panics if the URI has not been indexed. | ||
| #[must_use] | ||
| pub fn source(&self, uri: &str) -> &str { | ||
| self.sources.get(uri).expect("source not found for URI") | ||
| } | ||
|
|
||
| /// Returns the source text for a definition, sliced by its offset. | ||
| /// | ||
| /// # Panics |
There was a problem hiding this comment.
Personally, I wouldn't include these changes as part of this PR. It's already pretty large and we can ship test improvements separately.
| let ancestors: Vec<_> = ns.ancestors().iter().collect(); | ||
|
|
||
| let search_start = if only_inherited { | ||
| let pos = ancestors | ||
| .iter() | ||
| .position(|a| matches!(a, Ancestor::Complete(id) if *id == namespace_id)) | ||
| .expect("namespace_id should be present in its own ancestor chain"); | ||
| pos + 1 | ||
| } else { | ||
| 0 | ||
| }; | ||
|
|
||
| for ancestor in &ancestors[search_start..] { |
There was a problem hiding this comment.
This refactor is switching 1 loop for 3. Before, we were iterating on ancestors and performing the checks inline.
Now, we're eagerly collecting ancestors using iter().collect() (which I think is not necessary? ancestors already returns a collection). Then we iterate to find the main namespace position and iterate again to search the ancestors.
I'd vote for us to keep the current implementation. At least for now.
| /// | ||
| /// Panics if any alias definition in the chain has no corresponding declaration. | ||
| #[must_use] | ||
| pub fn deep_dealias_method(graph: &Graph, alias_id: DefinitionId) -> Vec<DefinitionId> { |
There was a problem hiding this comment.
I think part of the test related changes are a result of this method returning definitions.
Should this return a DeclarationId instead? Then we return the declaration that an alias points to and, if desired, the consumer of the API can fetch the related definitions.
| case ParameterKind_RequiredPositional: return cPositionalParameter; | ||
| case ParameterKind_OptionalPositional: return cOptionalPositionalParameter; | ||
| case ParameterKind_RestPositional: return cRestPositionalParameter; | ||
| case ParameterKind_Post: return cPostParameter; |
There was a problem hiding this comment.
| case ParameterKind_Post: return cPostParameter; | |
| case ParameterKind_Post: return cPostParameter; |
| for (size_t i = 0; i < arr->len; i++) { | ||
| SignatureEntry sig_entry = arr->items[i]; | ||
|
|
||
| VALUE parameters = rb_ary_new_capa((long)sig_entry.parameters_len); |
There was a problem hiding this comment.
For a follow-up PR, potential performance optimization:
Parameter-less functions are pretty common. We might see some nice memory gains if we reuse the same empty array between them all.
| attr_reader :parameters | ||
|
|
||
| #: (Array[Parameter]) -> void | ||
| def initialize(parameters) |
| # frozen_string_literal: true | ||
|
|
||
| module Rubydex | ||
| class Signature |
There was a problem hiding this comment.
(Not for this PR, but in a follow-up)
In my experience, having all the parameters in a single array ends up making callers need to do lots of filtering (e.g. to find just positional args, just keyword args, pull out the block param, etc.).
As the need arises, I would encourage adding convenience methods like positional_parameters, keyword_parameters, block, etc.
| Block = 8, | ||
| } | ||
|
|
||
| fn map_parameter_kind(param: &Parameter) -> ParameterKind { |
There was a problem hiding this comment.
I think this would be nicer to have in the impl Parameter block, so you can just call like p.kind
| use std::ptr; | ||
|
|
||
| /// C-compatible enum representing the kind of a parameter. | ||
| #[repr(C)] |
There was a problem hiding this comment.
We can be explicit about the size, too
| #[repr(C)] | |
| #[repr(C, u8)] |
| 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 })) |
There was a problem hiding this comment.
We're already using Box::into_raw, we can apply it here to remove the need for this std::mem::forget(boxed).
| 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 })) | |
| let len = sig_entries.len(); | |
| let items_ptr = Box::into_raw(sig_entries.into_boxed_slice()) as *mut _; | |
| Box::into_raw(Box::new(SignatureArray { items: items_ptr, len })) |
There was a problem hiding this comment.
(or we can use cast_const(), depending on the convention this repo follows
|
|
||
| let parameters_len = param_entries.len(); | ||
| let parameters_ptr = param_entries.as_mut_ptr(); | ||
| std::mem::forget(param_entries); |
|
|
||
| for param_entry in &mut param_slice { | ||
| if !param_entry.name.is_null() { | ||
| let _ = unsafe { CString::from_raw(param_entry.name.cast_mut()) }; |
There was a problem hiding this comment.
Equivalent, but more clear intent IMO
| let _ = unsafe { CString::from_raw(param_entry.name.cast_mut()) }; | |
| drop(unsafe { CString::from_raw(param_entry.name.cast_mut()) }); |
| // Take ownership of the SignatureArray | ||
| let arr = unsafe { Box::from_raw(ptr) }; | ||
|
|
||
| if !arr.items.is_null() && arr.len > 0 { |
There was a problem hiding this comment.
Can this be inverted into an early-return, similar to the ptr.is_null() check above?
Summary
MethodDefinition#signatures, returning an array ofRubydex::SignatureobjectsSignatureholds an array of typedParametersubclasses (PositionalParameter,KeywordParameter, etc.) withnameandlocationattributesParameterKind,SignatureEntry,SignatureArray) and C extension glue for conversionExtracted from #686.
Test plan
🤖 Generated with Claude Code