Skip to content

Commit 80fcdca

Browse files
authored
Expose Declaration#references to the Ruby API (#635)
This PR exposes the `Declaration#references` Ruby API, which is what the LSP will use to implement find references and rename. Similar to the `declarations` iterator, I extracted the base infrastructure for iterating over references to `utils.c`, so that we can more easily reuse.
2 parents 14a656f + 9bfae11 commit 80fcdca

6 files changed

Lines changed: 176 additions & 27 deletions

File tree

ext/rubydex/declaration.c

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#include "definition.h"
33
#include "graph.h"
44
#include "handle.h"
5+
#include "reference.h"
56
#include "rustbindings.h"
67
#include "utils.h"
78

@@ -291,6 +292,42 @@ static VALUE rdxr_declaration_descendants(VALUE self) {
291292
return self;
292293
}
293294

295+
// Size function for the Declaration#references enumerator
296+
static VALUE declaration_references_size(VALUE self, VALUE _args, VALUE _eobj) {
297+
HandleData *data;
298+
TypedData_Get_Struct(self, HandleData, &handle_type, data);
299+
300+
void *graph;
301+
TypedData_Get_Struct(data->graph_obj, void *, &graph_type, graph);
302+
303+
struct ReferencesIter *iter = rdx_declaration_references_iter_new(graph, data->id);
304+
size_t len = rdx_references_iter_len(iter);
305+
rdx_references_iter_free(iter);
306+
307+
return SIZET2NUM(len);
308+
}
309+
310+
// Returns an enumerator for all references to this declaration
311+
//
312+
// Declaration#references: () -> Enumerator[Reference]
313+
static VALUE rdxr_declaration_references(VALUE self) {
314+
if (!rb_block_given_p()) {
315+
return rb_enumeratorize_with_size(self, rb_str_new2("references"), 0, NULL, declaration_references_size);
316+
}
317+
318+
HandleData *data;
319+
TypedData_Get_Struct(self, HandleData, &handle_type, data);
320+
321+
void *graph;
322+
TypedData_Get_Struct(data->graph_obj, void *, &graph_type, graph);
323+
324+
void *iter = rdx_declaration_references_iter_new(graph, data->id);
325+
VALUE args = rb_ary_new_from_args(2, data->graph_obj, ULL2NUM((uintptr_t)iter));
326+
rb_ensure(rdxi_references_yield, args, rdxi_references_ensure, args);
327+
328+
return self;
329+
}
330+
294331
void rdxi_initialize_declaration(VALUE mRubydex) {
295332
cDeclaration = rb_define_class_under(mRubydex, "Declaration", rb_cObject);
296333
cNamespace = rb_define_class_under(mRubydex, "Namespace", cDeclaration);
@@ -309,6 +346,7 @@ void rdxi_initialize_declaration(VALUE mRubydex) {
309346
rb_define_method(cDeclaration, "name", rdxr_declaration_name, 0);
310347
rb_define_method(cDeclaration, "unqualified_name", rdxr_declaration_unqualified_name, 0);
311348
rb_define_method(cDeclaration, "definitions", rdxr_declaration_definitions, 0);
349+
rb_define_method(cDeclaration, "references", rdxr_declaration_references, 0);
312350
rb_define_method(cDeclaration, "owner", rdxr_declaration_owner, 0);
313351

314352
// Namespace only methods

ext/rubydex/graph.c

Lines changed: 2 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -228,31 +228,6 @@ static VALUE rdxr_graph_aref(VALUE self, VALUE key) {
228228
return rb_class_new_instance(2, argv, decl_class);
229229
}
230230

231-
// Body function for rb_ensure for the reference enumerators
232-
static VALUE graph_references_yield(VALUE args) {
233-
VALUE self = rb_ary_entry(args, 0);
234-
void *iter = (void *)(uintptr_t)NUM2ULL(rb_ary_entry(args, 1));
235-
236-
uint64_t id = 0;
237-
ReferenceKind kind;
238-
while (rdx_references_iter_next(iter, &id, &kind)) {
239-
VALUE ref_class = rdxi_reference_class_for_kind(kind);
240-
VALUE argv[] = {self, ULL2NUM(id)};
241-
VALUE obj = rb_class_new_instance(2, argv, ref_class);
242-
rb_yield(obj);
243-
}
244-
245-
return Qnil;
246-
}
247-
248-
// Ensure function for rb_ensure for the reference enumerators to always free the iterator
249-
static VALUE graph_references_ensure(VALUE args) {
250-
void *iter = (void *)(uintptr_t)NUM2ULL(rb_ary_entry(args, 1));
251-
rdx_references_iter_free(iter);
252-
253-
return Qnil;
254-
}
255-
256231
// Size function for the constant_references enumerator
257232
static VALUE graph_constant_references_size(VALUE self, VALUE _args, VALUE _eobj) {
258233
void *graph;
@@ -278,7 +253,7 @@ static VALUE rdxr_graph_constant_references(VALUE self) {
278253

279254
void *iter = rdx_graph_constant_references_iter_new(graph);
280255
VALUE args = rb_ary_new_from_args(2, self, ULL2NUM((uintptr_t)iter));
281-
rb_ensure(graph_references_yield, args, graph_references_ensure, args);
256+
rb_ensure(rdxi_references_yield, args, rdxi_references_ensure, args);
282257

283258
return self;
284259
}
@@ -308,7 +283,7 @@ static VALUE rdxr_graph_method_references(VALUE self) {
308283

309284
void *iter = rdx_graph_method_references_iter_new(graph);
310285
VALUE args = rb_ary_new_from_args(2, self, ULL2NUM((uintptr_t)iter));
311-
rb_ensure(graph_references_yield, args, graph_references_ensure, args);
286+
rb_ensure(rdxi_references_yield, args, rdxi_references_ensure, args);
312287

313288
return self;
314289
}

ext/rubydex/utils.c

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#include "utils.h"
22
#include "declaration.h"
3+
#include "reference.h"
34
#include "rustbindings.h"
45

56
// Convert a Ruby array of strings into a double char pointer so that we can pass that to Rust.
@@ -50,3 +51,28 @@ VALUE rdxi_declarations_ensure(VALUE args) {
5051
rdx_graph_declarations_iter_free(iter);
5152
return Qnil;
5253
}
54+
55+
// Yield body for iterating over references
56+
VALUE rdxi_references_yield(VALUE args) {
57+
VALUE graph_obj = rb_ary_entry(args, 0);
58+
void *iter = (void *)(uintptr_t)NUM2ULL(rb_ary_entry(args, 1));
59+
60+
uint64_t id = 0;
61+
ReferenceKind kind;
62+
63+
while (rdx_references_iter_next(iter, &id, &kind)) {
64+
VALUE ref_class = rdxi_reference_class_for_kind(kind);
65+
VALUE argv[] = {graph_obj, ULL2NUM(id)};
66+
VALUE obj = rb_class_new_instance(2, argv, ref_class);
67+
rb_yield(obj);
68+
}
69+
70+
return Qnil;
71+
}
72+
73+
// Ensure function for iterating over references to always free the iterator
74+
VALUE rdxi_references_ensure(VALUE args) {
75+
void *iter = (void *)(uintptr_t)NUM2ULL(rb_ary_entry(args, 1));
76+
rdx_references_iter_free(iter);
77+
return Qnil;
78+
}

ext/rubydex/utils.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,10 @@ VALUE rdxi_declarations_yield(VALUE args);
1616
// Ensure function for iterating over declarations to always free the iterator
1717
VALUE rdxi_declarations_ensure(VALUE args);
1818

19+
// Yield body for iterating over references
20+
VALUE rdxi_references_yield(VALUE args);
21+
22+
// Ensure function for iterating over references to always free the iterator
23+
VALUE rdxi_references_ensure(VALUE args);
24+
1925
#endif // RUBYDEX_UTILS_H

rust/rubydex-sys/src/declaration_api.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use std::ptr;
77

88
use crate::definition_api::{DefinitionsIter, rdx_definitions_iter_new_from_ids};
99
use crate::graph_api::{GraphPointer, with_graph};
10+
use crate::reference_api::{ReferenceKind, ReferencesIter};
1011
use crate::utils;
1112
use rubydex::model::ids::{DeclarationId, StringId};
1213

@@ -399,3 +400,36 @@ pub unsafe extern "C" fn rdx_declaration_descendants(pointer: GraphPointer, decl
399400

400401
Box::into_raw(Box::new(DeclarationsIter::new(declarations.into_boxed_slice())))
401402
}
403+
404+
/// Creates a new iterator over references for a given declaration by snapshotting the current set of IDs.
405+
///
406+
/// # Safety
407+
///
408+
/// - `pointer` must be a valid `GraphPointer` previously returned by this crate.
409+
/// - The returned pointer must be freed with `rdx_references_iter_free`.
410+
#[unsafe(no_mangle)]
411+
pub unsafe extern "C" fn rdx_declaration_references_iter_new(
412+
pointer: GraphPointer,
413+
decl_id: u64,
414+
) -> *mut ReferencesIter {
415+
with_graph(pointer, |graph| {
416+
let decl_id = DeclarationId::new(decl_id);
417+
418+
let Some(decl) = graph.declarations().get(&decl_id) else {
419+
return ReferencesIter::new(Vec::new().into_boxed_slice());
420+
};
421+
422+
let kind = match decl {
423+
Declaration::Namespace(_) | Declaration::Constant(_) | Declaration::ConstantAlias(_) => {
424+
ReferenceKind::Constant
425+
}
426+
Declaration::Method(_) => ReferenceKind::Method,
427+
Declaration::GlobalVariable(_) | Declaration::InstanceVariable(_) | Declaration::ClassVariable(_) => {
428+
return ReferencesIter::new(Vec::new().into_boxed_slice());
429+
}
430+
};
431+
432+
let entries: Vec<_> = decl.references().iter().map(|ref_id| (**ref_id, kind)).collect();
433+
ReferencesIter::new(entries.into_boxed_slice())
434+
})
435+
}

test/declaration_test.rb

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,76 @@ class Child < Parent; end
380380
end
381381
end
382382

383+
def test_references_enumerator
384+
with_context do |context|
385+
context.write!("file1.rb", <<~RUBY)
386+
class A; end
387+
class B < A; end
388+
A.new
389+
RUBY
390+
391+
graph = Rubydex::Graph.new
392+
graph.index_all(context.glob("**/*.rb"))
393+
graph.resolve
394+
395+
declaration = graph["A"]
396+
enumerator = declaration.references
397+
assert_equal(2, enumerator.size)
398+
assert_equal(2, enumerator.count)
399+
assert_equal(2, enumerator.to_a.size)
400+
end
401+
end
402+
403+
def test_references_with_block
404+
with_context do |context|
405+
context.write!("file1.rb", <<~RUBY)
406+
class A; end
407+
class B < A; end
408+
A.new
409+
RUBY
410+
411+
graph = Rubydex::Graph.new
412+
graph.index_all(context.glob("**/*.rb"))
413+
graph.resolve
414+
415+
declaration = graph["A"]
416+
references = []
417+
declaration.references do |ref|
418+
references << ref
419+
end
420+
421+
assert_equal(2, references.size)
422+
423+
references.each do |ref|
424+
assert_instance_of(Rubydex::ConstantReference, ref)
425+
assert_equal("A", ref.name)
426+
refute_nil(ref.location)
427+
end
428+
end
429+
end
430+
431+
def test_method_references_are_not_associated_with_declaration
432+
# This test documents current behavior. We can only determine all method references with type inference, so we
433+
# currently do not try to make the connection
434+
435+
with_context do |context|
436+
context.write!("file1.rb", <<~RUBY)
437+
class A
438+
def self.foo; end
439+
end
440+
441+
A.foo
442+
RUBY
443+
444+
graph = Rubydex::Graph.new
445+
graph.index_all(context.glob("**/*.rb"))
446+
graph.resolve
447+
448+
declaration = graph["A::<A>#foo()"]
449+
assert_empty(declaration.references.to_a)
450+
end
451+
end
452+
383453
def test_find_member_returns_inherited_members
384454
with_context do |context|
385455
context.write!("file1.rb", <<~RUBY)

0 commit comments

Comments
 (0)