Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions ext/rubydex/definition.c
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include "definition.h"
#include "declaration.h"
#include "graph.h"
#include "handle.h"
#include "location.h"
Expand Down Expand Up @@ -264,6 +265,36 @@ static VALUE rdxr_method_alias_definition_signatures(VALUE self) {
return rdxi_signatures_to_ruby(arr);
}

// MethodAliasDefinition#target -> Rubydex::Method?
// Returns the resolved target method declaration by following the alias chain, or nil if the chain could not be
// resolved (the target name doesn't exist on the owner, or the owner itself never resolved). Raises
// Rubydex::AliasCycleError when the alias chain forms a cycle.
static VALUE rdxr_method_alias_definition_target(VALUE self) {
HandleData *data;
TypedData_Get_Struct(self, HandleData, &handle_type, data);

void *graph;
TypedData_Get_Struct(data->graph_obj, void *, &graph_type, graph);

CMethodAliasTargetResult result = rdx_method_alias_definition_target(graph, data->id);

switch (result.status) {
case CMethodAliasResolution_Resolved: {
VALUE decl_class = rdxi_declaration_class_for_kind(result.declaration->kind);
VALUE argv[] = {data->graph_obj, ULL2NUM(result.declaration->id)};

free_c_declaration(result.declaration);
return rb_class_new_instance(2, argv, decl_class);
}
case CMethodAliasResolution_NotFound:
return Qnil;
case CMethodAliasResolution_Cycle:
rb_raise(rb_const_get(mRubydex, rb_intern("AliasCycleError")), "method alias chain forms a cycle");
default:
rb_raise(rb_eRuntimeError, "Unknown CMethodAliasResolution: %d", result.status);
}
}

void rdxi_initialize_definition(VALUE mod) {
mRubydex = mod;

Expand Down Expand Up @@ -307,5 +338,6 @@ void rdxi_initialize_definition(VALUE mod) {
cClassVariableDefinition = rb_define_class_under(mRubydex, "ClassVariableDefinition", cDefinition);
cMethodAliasDefinition = rb_define_class_under(mRubydex, "MethodAliasDefinition", cDefinition);
rb_define_method(cMethodAliasDefinition, "signatures", rdxr_method_alias_definition_signatures, 0);
rb_define_method(cMethodAliasDefinition, "target", rdxr_method_alias_definition_target, 0);
cGlobalVariableAliasDefinition = rb_define_class_under(mRubydex, "GlobalVariableAliasDefinition", cDefinition);
}
1 change: 1 addition & 0 deletions lib/rubydex.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
require "rubydex/rubydex"
end

require "rubydex/errors"
require "rubydex/failures"
require "rubydex/location"
require "rubydex/comment"
Expand Down
8 changes: 8 additions & 0 deletions lib/rubydex/errors.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# frozen_string_literal: true

module Rubydex
class Error < StandardError; end

# Raised when `MethodAliasDefinition#target` walks an alias chain that loops back on itself.
class AliasCycleError < Error; end
end
6 changes: 5 additions & 1 deletion rbi/rubydex.rbi
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,10 @@ class Rubydex::ConstantDefinition < Rubydex::Definition; end
class Rubydex::GlobalVariableAliasDefinition < Rubydex::Definition; end
class Rubydex::GlobalVariableDefinition < Rubydex::Definition; end
class Rubydex::InstanceVariableDefinition < Rubydex::Definition; end
class Rubydex::MethodAliasDefinition < Rubydex::Definition; end
class Rubydex::MethodAliasDefinition < Rubydex::Definition
sig { returns(T.nilable(Rubydex::Method)) }
def target; end
end
class Rubydex::MethodDefinition < Rubydex::Definition; end

class Rubydex::ModuleDefinition < Rubydex::Definition
Expand Down Expand Up @@ -231,6 +234,7 @@ class Rubydex::Document
end

class Rubydex::Error < StandardError; end
class Rubydex::AliasCycleError < Rubydex::Error; end

class Rubydex::Failure
sig { params(message: String).void }
Expand Down
71 changes: 71 additions & 0 deletions rust/rubydex-sys/src/definition_api.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
//! This file provides the C API for Definition accessors

use crate::declaration_api::CDeclaration;
use crate::graph_api::{GraphPointer, with_graph};
use crate::location_api::{Location, create_location_for_uri_and_offset};
use crate::reference_api::CConstantReference;
use libc::c_char;
use rubydex::model::definitions::{Definition, Mixin};
use rubydex::model::ids::DefinitionId;
use rubydex::query::AliasResolutionError;
use std::ffi::CString;
use std::ptr;

Expand Down Expand Up @@ -441,3 +443,72 @@ pub unsafe extern "C" fn rdx_definition_mixins(pointer: GraphPointer, definition
MixinsIter::new(entries.into_boxed_slice())
})
}

/// Status of a `MethodAliasDefinition#target` resolution.
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CMethodAliasResolution {
/// The alias chain resolved successfully; `declaration` is valid.
Resolved = 0,
/// The chain could not be resolved because the target name does not exist on the owner, or the owner itself was
/// never resolved. Treated as `nil` on the Ruby side.
NotFound = 1,
/// The alias chain forms a cycle. Surfaced as a `Rubydex::AliasCycleError` on the Ruby side.
Cycle = 2,
}

#[repr(C)]
#[derive(Debug)]
pub struct CMethodAliasTargetResult {
pub status: CMethodAliasResolution,
pub declaration: *const CDeclaration,
}

/// Resolves a `MethodAliasDefinition` to its target method declaration via `query::follow_method_alias` and reports the
/// outcome as a tagged status. The `declaration` pointer is non-null only when `status == Resolved`; the caller is
/// responsible for freeing it with `free_c_declaration`.
///
/// # Safety
/// - `pointer` must be a valid pointer previously returned by `rdx_graph_new`.
/// - `definition_id` must be a valid definition id for a `MethodAliasDefinition`.
///
/// # Panics
/// Panics on graph inconsistencies (the definition is not a method alias, or the alias resolved to a non-method
/// declaration).
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rdx_method_alias_definition_target(
pointer: GraphPointer,
definition_id: u64,
) -> CMethodAliasTargetResult {
with_graph(pointer, |graph| {
let def_id = DefinitionId::new(definition_id);

match rubydex::query::follow_method_alias(graph, def_id) {
Ok(target_id) => {
let target_decl = graph
.declarations()
.get(&target_id)
.expect("target declaration must exist");
let boxed = Box::new(CDeclaration::from_declaration(target_id, target_decl));

CMethodAliasTargetResult {
status: CMethodAliasResolution::Resolved,
declaration: Box::into_raw(boxed).cast_const(),
}
}
Err(AliasResolutionError::TargetNotFound | AliasResolutionError::UnresolvedOwner) => {
CMethodAliasTargetResult {
status: CMethodAliasResolution::NotFound,
declaration: ptr::null(),
}
}
Err(AliasResolutionError::Cycle) => CMethodAliasTargetResult {
status: CMethodAliasResolution::Cycle,
declaration: ptr::null(),
},
Err(err @ (AliasResolutionError::NotAnAlias | AliasResolutionError::TargetNotMethod)) => {
panic!("graph inconsistency in method alias resolution: {err:?}")
}
}
})
}
83 changes: 83 additions & 0 deletions test/definition_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -668,6 +668,89 @@ class Foo
end
end

def test_method_alias_definition_target
with_context do |context|
context.write!("file1.rb", <<~RUBY)
class Foo
def foo(a, b); end
alias bar foo
end
RUBY

graph = Rubydex::Graph.new
graph.index_all(context.glob("**/*.rb"))
graph.resolve

alias_def = graph["Foo#bar()"].definitions.first
assert_instance_of(Rubydex::MethodAliasDefinition, alias_def)

target = alias_def.target
assert_instance_of(Rubydex::Method, target)
assert_equal("Foo#foo()", target.name)
end
end

def test_method_alias_definition_target_through_chain
with_context do |context|
context.write!("file1.rb", <<~RUBY)
class Foo
def foo; end
alias bar foo
alias baz bar
end
RUBY

graph = Rubydex::Graph.new
graph.index_all(context.glob("**/*.rb"))
graph.resolve

alias_def = graph["Foo#baz()"].definitions.first
assert_instance_of(Rubydex::MethodAliasDefinition, alias_def)

target = alias_def.target
assert_instance_of(Rubydex::Method, target)
assert_equal("Foo#foo()", target.name)
end
end

def test_method_alias_definition_target_unresolved_returns_nil
with_context do |context|
context.write!("file1.rb", <<~RUBY)
class Foo
alias bar nonexistent
end
RUBY

graph = Rubydex::Graph.new
graph.index_all(context.glob("**/*.rb"))
graph.resolve

alias_def = graph["Foo#bar()"].definitions.first
assert_instance_of(Rubydex::MethodAliasDefinition, alias_def)
assert_nil(alias_def.target)
end
end

def test_method_alias_definition_target_raises_on_cycle
with_context do |context|
context.write!("file1.rb", <<~RUBY)
class Foo
alias a b
alias b a
end
RUBY

graph = Rubydex::Graph.new
graph.index_all(context.glob("**/*.rb"))
graph.resolve

alias_def = graph["Foo#a()"].definitions.first
assert_instance_of(Rubydex::MethodAliasDefinition, alias_def)

assert_raises(Rubydex::AliasCycleError) { alias_def.target }
end
end

private

# Comment locations on Windows include the carriage return. This means that the end column is off by one when compared
Expand Down
Loading