Skip to content

Commit 84fdfb6

Browse files
authored
Merge pull request #778 from Shopify/Expose_declaration_visibility_in_the_Ruby_API
Expose declaration visibility in the Ruby API
2 parents 2fa9482 + 080c51c commit 84fdfb6

4 files changed

Lines changed: 345 additions & 0 deletions

File tree

ext/rubydex/declaration.c

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,38 @@ static VALUE rdxr_variable_declaration_references(VALUE self) {
407407
return rb_ary_new();
408408
}
409409

410+
static VALUE rdxi_visibility_to_symbol(CVisibility visibility) {
411+
switch (visibility) {
412+
case CVisibility_Public:
413+
return ID2SYM(rb_intern("public"));
414+
case CVisibility_Protected:
415+
return ID2SYM(rb_intern("protected"));
416+
case CVisibility_Private:
417+
return ID2SYM(rb_intern("private"));
418+
default:
419+
rb_raise(rb_eRuntimeError, "Unknown CVisibility: %d", visibility);
420+
}
421+
}
422+
423+
// Declaration#visibility -> Symbol
424+
static VALUE rdxr_declaration_visibility(VALUE self) {
425+
HandleData *data;
426+
TypedData_Get_Struct(self, HandleData, &handle_type, data);
427+
428+
void *graph;
429+
TypedData_Get_Struct(data->graph_obj, void *, &graph_type, graph);
430+
431+
const CVisibility *visibility = rdx_graph_visibility(graph, data->id);
432+
if (visibility == NULL) {
433+
rb_raise(rb_eRuntimeError, "declaration has no visibility");
434+
}
435+
436+
VALUE symbol = rdxi_visibility_to_symbol(*visibility);
437+
free_c_visibility(visibility);
438+
439+
return symbol;
440+
}
441+
410442
// ConstantAlias#target -> Declaration?
411443
// Returns the first resolved target declaration for this constant alias, or nil if none of its definitions resolved to
412444
// a target
@@ -459,13 +491,19 @@ void rdxi_initialize_declaration(VALUE mRubydex) {
459491
rb_define_method(cNamespace, "descendants", rdxr_declaration_descendants, 0);
460492
rb_define_method(cNamespace, "members", rdxr_declaration_members, 0);
461493

494+
rb_define_method(cClass, "visibility", rdxr_declaration_visibility, 0);
495+
rb_define_method(cModule, "visibility", rdxr_declaration_visibility, 0);
496+
462497
// Constant and ConstantAlias have constant references
463498
rb_define_method(cConstant, "references", rdxr_constant_declaration_references, 0);
499+
rb_define_method(cConstant, "visibility", rdxr_declaration_visibility, 0);
464500
rb_define_method(cConstantAlias, "references", rdxr_constant_declaration_references, 0);
465501
rb_define_method(cConstantAlias, "target", rdxr_constant_alias_target, 0);
502+
rb_define_method(cConstantAlias, "visibility", rdxr_declaration_visibility, 0);
466503

467504
// Method has method references
468505
rb_define_method(cMethod, "references", rdxr_method_declaration_references, 0);
506+
rb_define_method(cMethod, "visibility", rdxr_declaration_visibility, 0);
469507

470508
// Variable declarations don't yet support references
471509
rb_define_method(cGlobalVariable, "references", rdxr_variable_declaration_references, 0);

lib/rubydex/declaration.rb

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,42 @@
11
# frozen_string_literal: true
22

33
module Rubydex
4+
module Visibility
5+
#: () -> bool
6+
def public? = visibility == :public
7+
8+
#: () -> bool
9+
def private? = visibility == :private
10+
11+
#: () -> bool
12+
def protected? = visibility == :protected
13+
end
14+
415
class Declaration
516
# @abstract
617
#: () -> Enumerable[Reference]
718
def references
819
raise NotImplementedError, "Subclasses must implement #references"
920
end
1021
end
22+
23+
class Class < Namespace
24+
include Visibility
25+
end
26+
27+
class Module < Namespace
28+
include Visibility
29+
end
30+
31+
class Constant < Declaration
32+
include Visibility
33+
end
34+
35+
class ConstantAlias < Declaration
36+
include Visibility
37+
end
38+
39+
class Method < Declaration
40+
include Visibility
41+
end
1142
end

rust/rubydex-sys/src/graph_api.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use rubydex::model::graph::Graph;
1313
use rubydex::model::ids::{DeclarationId, NameId, UriId};
1414
use rubydex::model::keywords;
1515
use rubydex::model::name::NameRef;
16+
use rubydex::model::visibility::Visibility;
1617
use rubydex::query::{CompletionCandidate, CompletionContext, CompletionReceiver};
1718
use rubydex::resolution::Resolver;
1819
use rubydex::{indexing, integrity, listing, query};
@@ -962,6 +963,53 @@ pub unsafe extern "C" fn rdx_keyword_get(name: *const c_char) -> *const CKeyword
962963
}
963964
}
964965

966+
#[repr(u8)]
967+
#[derive(Debug, Clone, Copy)]
968+
pub enum CVisibility {
969+
Public = 0,
970+
Protected = 1,
971+
Private = 2,
972+
}
973+
974+
/// Returns the visibility of a declaration (method, constant, class, or module) as a heap-allocated
975+
/// `CVisibility`, or NULL when the declaration carries no visibility (e.g. variables, singleton
976+
/// classes, todos). Caller must free the returned pointer with `free_c_visibility`.
977+
///
978+
/// # Safety
979+
///
980+
/// - `pointer` must be a valid `GraphPointer` previously returned by this crate.
981+
#[unsafe(no_mangle)]
982+
pub unsafe extern "C" fn rdx_graph_visibility(pointer: GraphPointer, declaration_id: u64) -> *const CVisibility {
983+
with_graph(pointer, |graph| {
984+
let Some(visibility) = graph.visibility(&DeclarationId::new(declaration_id)) else {
985+
return ptr::null();
986+
};
987+
988+
let c_visibility = match visibility {
989+
Visibility::Public => CVisibility::Public,
990+
Visibility::Protected => CVisibility::Protected,
991+
Visibility::Private => CVisibility::Private,
992+
Visibility::ModuleFunction => {
993+
unimplemented!("module_function visibility translation is not implemented yet")
994+
}
995+
};
996+
997+
Box::into_raw(Box::new(c_visibility)).cast_const()
998+
})
999+
}
1000+
1001+
/// Frees a `CVisibility` previously returned by `rdx_graph_visibility`.
1002+
///
1003+
/// # Safety
1004+
///
1005+
/// - `ptr` must be a valid pointer previously returned by `rdx_graph_visibility`.
1006+
#[unsafe(no_mangle)]
1007+
pub unsafe extern "C" fn free_c_visibility(ptr: *const CVisibility) {
1008+
unsafe {
1009+
let _ = Box::from_raw(ptr.cast_mut());
1010+
}
1011+
}
1012+
9651013
/// Frees a `CKeyword` previously returned by `rdx_keyword_get`.
9661014
///
9671015
/// # Safety

test/declaration_test.rb

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,234 @@ def test_circular_constant_alias_target
691691
end
692692
end
693693

694+
def test_method_visibility_defaults_to_public
695+
with_context do |context|
696+
context.write!("file1.rb", <<~RUBY)
697+
class Foo
698+
def bare; end
699+
end
700+
RUBY
701+
702+
graph = Rubydex::Graph.new
703+
graph.index_all(context.glob("**/*.rb"))
704+
graph.resolve
705+
706+
assert_equal(:public, graph["Foo#bare()"].visibility)
707+
end
708+
end
709+
710+
def test_method_visibility_via_scope_flag
711+
with_context do |context|
712+
context.write!("file1.rb", <<~RUBY)
713+
class Foo
714+
private
715+
716+
def hidden; end
717+
718+
protected
719+
720+
def guarded; end
721+
722+
public
723+
724+
def visible; end
725+
end
726+
RUBY
727+
728+
graph = Rubydex::Graph.new
729+
graph.index_all(context.glob("**/*.rb"))
730+
graph.resolve
731+
732+
assert_equal(:private, graph["Foo#hidden()"].visibility)
733+
assert_equal(:protected, graph["Foo#guarded()"].visibility)
734+
assert_equal(:public, graph["Foo#visible()"].visibility)
735+
end
736+
end
737+
738+
def test_method_visibility_via_retroactive_call
739+
with_context do |context|
740+
context.write!("file1.rb", <<~RUBY)
741+
class Foo
742+
def hidden; end
743+
private :hidden
744+
745+
def guarded; end
746+
protected :guarded
747+
end
748+
RUBY
749+
750+
graph = Rubydex::Graph.new
751+
graph.index_all(context.glob("**/*.rb"))
752+
graph.resolve
753+
754+
assert_equal(:private, graph["Foo#hidden()"].visibility)
755+
assert_equal(:protected, graph["Foo#guarded()"].visibility)
756+
end
757+
end
758+
759+
def test_constant_visibility_defaults_to_public
760+
with_context do |context|
761+
context.write!("file1.rb", <<~RUBY)
762+
class Foo
763+
BAR = 1
764+
end
765+
RUBY
766+
767+
graph = Rubydex::Graph.new
768+
graph.index_all(context.glob("**/*.rb"))
769+
graph.resolve
770+
771+
assert_equal(:public, graph["Foo::BAR"].visibility)
772+
end
773+
end
774+
775+
def test_constant_visibility_via_private_constant
776+
with_context do |context|
777+
context.write!("file1.rb", <<~RUBY)
778+
class Foo
779+
BAR = 1
780+
private_constant :BAR
781+
end
782+
RUBY
783+
784+
graph = Rubydex::Graph.new
785+
graph.index_all(context.glob("**/*.rb"))
786+
graph.resolve
787+
788+
assert_equal(:private, graph["Foo::BAR"].visibility)
789+
end
790+
end
791+
792+
def test_constant_alias_visibility
793+
with_context do |context|
794+
context.write!("file1.rb", <<~RUBY)
795+
class Foo
796+
BAR = 1
797+
ALIAS = BAR
798+
private_constant :ALIAS
799+
end
800+
RUBY
801+
802+
graph = Rubydex::Graph.new
803+
graph.index_all(context.glob("**/*.rb"))
804+
graph.resolve
805+
806+
assert_equal(:private, graph["Foo::ALIAS"].visibility)
807+
end
808+
end
809+
810+
def test_class_and_module_visibility_via_private_constant
811+
with_context do |context|
812+
context.write!("file1.rb", <<~RUBY)
813+
class Outer
814+
class Inner; end
815+
module Helpers; end
816+
private_constant :Inner, :Helpers
817+
end
818+
RUBY
819+
820+
graph = Rubydex::Graph.new
821+
graph.index_all(context.glob("**/*.rb"))
822+
graph.resolve
823+
824+
assert_equal(:private, graph["Outer::Inner"].visibility)
825+
assert_equal(:private, graph["Outer::Helpers"].visibility)
826+
end
827+
end
828+
829+
def test_visibility_is_undefined_for_declarations_without_visibility
830+
with_context do |context|
831+
context.write!("file1.rb", <<~RUBY)
832+
class Foo
833+
@@class_var = 1
834+
835+
def initialize
836+
@ivar = 1
837+
end
838+
839+
class << self; end
840+
end
841+
842+
$global = 1
843+
RUBY
844+
845+
graph = Rubydex::Graph.new
846+
graph.index_all(context.glob("**/*.rb"))
847+
graph.resolve
848+
849+
refute_respond_to(graph["Foo::<Foo>"], :visibility)
850+
refute_respond_to(graph["Foo\#@ivar"], :visibility)
851+
refute_respond_to(graph["Foo\#@@class_var"], :visibility)
852+
refute_respond_to(graph["$global"], :visibility)
853+
end
854+
end
855+
856+
def test_inline_module_function_visibility
857+
with_context do |context|
858+
context.write!("file1.rb", <<~RUBY)
859+
module Foo
860+
module_function
861+
862+
def bar; end
863+
end
864+
RUBY
865+
866+
graph = Rubydex::Graph.new
867+
graph.index_all(context.glob("**/*.rb"))
868+
graph.resolve
869+
870+
assert_equal(:private, graph["Foo#bar()"].visibility)
871+
assert_equal(:public, graph["Foo::<Foo>#bar()"].visibility)
872+
end
873+
end
874+
875+
def test_visibility_predicates
876+
with_context do |context|
877+
context.write!("file1.rb", <<~RUBY)
878+
class Foo
879+
def visible; end
880+
881+
def hidden; end
882+
private :hidden
883+
884+
def guarded; end
885+
protected :guarded
886+
887+
BAR = 1
888+
PRIVATE_BAR = 2
889+
private_constant :PRIVATE_BAR
890+
end
891+
RUBY
892+
893+
graph = Rubydex::Graph.new
894+
graph.index_all(context.glob("**/*.rb"))
895+
graph.resolve
896+
897+
visible = graph["Foo#visible()"]
898+
assert_predicate(visible, :public?)
899+
refute_predicate(visible, :private?)
900+
refute_predicate(visible, :protected?)
901+
902+
hidden = graph["Foo#hidden()"]
903+
assert_predicate(hidden, :private?)
904+
refute_predicate(hidden, :public?)
905+
refute_predicate(hidden, :protected?)
906+
907+
guarded = graph["Foo#guarded()"]
908+
assert_predicate(guarded, :protected?)
909+
refute_predicate(guarded, :public?)
910+
refute_predicate(guarded, :private?)
911+
912+
bar = graph["Foo::BAR"]
913+
assert_predicate(bar, :public?)
914+
refute_predicate(bar, :private?)
915+
916+
private_bar = graph["Foo::PRIVATE_BAR"]
917+
assert_predicate(private_bar, :private?)
918+
refute_predicate(private_bar, :public?)
919+
end
920+
end
921+
694922
def test_constant_alias_with_multiple_definitions_returns_one_resolved_target
695923
with_context do |context|
696924
context.write!("file1.rb", <<~RUBY)

0 commit comments

Comments
 (0)