Skip to content

Add MethodDefinition#signatures to Ruby API#697

Open
soutaro wants to merge 23 commits intomainfrom
soutaro-definition-signatures
Open

Add MethodDefinition#signatures to Ruby API#697
soutaro wants to merge 23 commits intomainfrom
soutaro-definition-signatures

Conversation

@soutaro
Copy link
Copy Markdown
Contributor

@soutaro soutaro commented Mar 26, 2026

Summary

  • Expose method signature information through MethodDefinition#signatures, returning an array of Rubydex::Signature objects
  • Each Signature holds an array of typed Parameter subclasses (PositionalParameter, KeywordParameter, etc.) with name and location attributes
  • Add Rust C API (ParameterKind, SignatureEntry, SignatureArray) and C extension glue for conversion

Extracted from #686.

Test plan

  • Tests cover all 8 parameter kinds (positional, optional, rest, keyword, optional keyword, rest keyword, block, forward)
  • Tests cover RBS-defined signatures, overloads, untyped parameters, and no-parameter methods

🤖 Generated with Claude Code

@soutaro soutaro requested a review from a team as a code owner March 26, 2026 05:34
@soutaro soutaro self-assigned this Mar 26, 2026
#: (parameters: Array[Parameter], method_definition: MethodDefinition) -> void
def initialize(parameters:, method_definition:)
@parameters = parameters
@method_definition = method_definition
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this back link? Considering that consumers will interact with the API like this:

declaration.definitions.first.signatures

I think we can remove this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is for MethodDeclaration#signatures. It will return an array of Signature, and then we should need the backlink to retrieve docs and something associated to the method definition.

We can drop the backlink and add a new class or use tuple for #signatures method though?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's discuss in the team sync, but I think exposing signatures at the declaration level has the potential to be a bit confusing.

Maybe we can handle the alias case in some other way.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on what we discussed, let's remove method_definition from the initializer and only accept the parameters. It will also simplify rdxi_signatures_to_ruby a bit.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. Will remove the method_definition.

soutaro added a commit that referenced this pull request Mar 27, 2026
- 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>
@soutaro soutaro force-pushed the soutaro-definition-signatures branch from 79877ae to 39084db Compare March 31, 2026 08:58
}

VALUE rdxi_signatures_to_ruby(SignatureArray *arr, VALUE graph_obj, VALUE default_method_def) {
if (arr == NULL || arr->len == 0) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think the design is weird. Will take a look at this tonight.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

#: (parameters: Array[Parameter], method_definition: MethodDefinition) -> void
def initialize(parameters:, method_definition:)
@parameters = parameters
@method_definition = method_definition
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on what we discussed, let's remove method_definition from the initializer and only accept the parameters. It will also simplify rdxi_signatures_to_ruby a bit.

let mut boxed = sig_entries.into_boxed_slice();
let len = boxed.len();
let items_ptr = boxed.as_mut_ptr();
std::mem::forget(boxed);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to forget here if we're ultimately passing this to Box::into_raw?

Copy link
Copy Markdown
Contributor Author

@soutaro soutaro Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

create_location_for_uri_and_offset(graph, document, name_offset)
})
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From this point on, everything is about signatures. What do you think about creating a signature_api.rs file to house these?


/// Result of following a `MethodAliasDefinition` chain to completion.
#[derive(Debug)]
pub struct DeepDealiasMethodResult {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if we really need the details of the failure. It keeps every details the implementation has in this layer, and C API layer ignores them.

The implementation would be a bit simpler if we can ignore the error details.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think differentiating the error message is probably worth it, but I think this should be an enum instead of a struct.

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));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
///
/// Panics if a `SelfReceiver` definition cannot be resolved to a namespace with a singleton class.
pub fn dealias_method(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems a bit more complicated than what I would've expected. It's a requirement that the graph must be resolved before performing queries like this one. To dealias a method, I would've expected something like this:

fn dealias(graph: &Graph, alias: &MethodAliasDefinition) {
  let decl_id = graph.definition_id_to_declaration_id(*def_id).unwrap();
  let alias_decl = graph.declarations().get(decl_id).unwrap();
  
  // Get the already resolved alias owner. No need to worry about the receiver
  // that HAS to have been resolved during resolution already
  let Some(method_decl_id) = find_member_in_ancestors(graph, alias_decl.owner_id(), *alias.old_name_str_id(), false) else {
        return Err(DealiasMethodError::MemberNotFound);
    };
}

I don't think we need the EnclosingNamespace either.

///
/// Panics if the definition or its document does not exist.
#[must_use]
pub fn source_at(&self, definition_id: &DefinitionId) -> &str {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@soutaro soutaro force-pushed the soutaro-definition-signatures branch from b33e8c6 to e0b3e8a Compare April 8, 2026 04:48
}

/// Returns a newly allocated array of signatures for the given method definition id.
/// Returns NULL if the definition is not a method definition.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it panics in this case?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be included?

end
end

def test_method_definition_signatures_with_various_parameter_kinds
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we include test cases for singleton methods?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants