Skip to content

Commit 299b305

Browse files
committed
Resolve singleton method visibility
1 parent 0bc23a3 commit 299b305

5 files changed

Lines changed: 301 additions & 1 deletion

File tree

rust/rubydex/src/diagnostic.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,4 +110,5 @@ rules! {
110110
// Resolution
111111
UndefinedMethodVisibilityTarget;
112112
UndefinedConstantVisibilityTarget;
113+
UndefinedSingletonMethodVisibilityTarget;
113114
}

rust/rubydex/src/model/graph.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,7 @@ impl Graph {
656656
};
657657
let visibility = match definition {
658658
Definition::MethodVisibility(vis) => Some(*vis.visibility()),
659+
Definition::SingletonMethodVisibility(vis) => Some(*vis.visibility()),
659660
Definition::Method(method) => Some(*method.visibility()),
660661
Definition::AttrAccessor(attr) => Some(*attr.visibility()),
661662
Definition::AttrReader(attr) => Some(*attr.visibility()),

rust/rubydex/src/resolution.rs

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,7 @@ impl<'a> Resolver<'a> {
274274
#[allow(clippy::too_many_lines)]
275275
fn handle_remaining_definitions(&mut self, other_ids: Vec<DefinitionId>) {
276276
let mut method_visibility_ids = Vec::new();
277+
let mut singleton_method_visibility_ids = Vec::new();
277278

278279
for id in other_ids {
279280
match self.graph.definitions().get(&id).unwrap() {
@@ -607,7 +608,9 @@ impl<'a> Resolver<'a> {
607608
Definition::MethodVisibility(_) => {
608609
method_visibility_ids.push(id);
609610
}
610-
Definition::SingletonMethodVisibility(_) => {}
611+
Definition::SingletonMethodVisibility(_) => {
612+
singleton_method_visibility_ids.push(id);
613+
}
611614
Definition::Class(_)
612615
| Definition::SingletonClass(_)
613616
| Definition::Module(_)
@@ -619,6 +622,98 @@ impl<'a> Resolver<'a> {
619622
}
620623

621624
self.resolve_method_visibilities(method_visibility_ids);
625+
self.resolve_singleton_method_visibilities(singleton_method_visibility_ids);
626+
}
627+
628+
/// Resolves `private_class_method` / `public_class_method` calls
629+
fn resolve_singleton_method_visibilities(&mut self, visibility_ids: Vec<DefinitionId>) {
630+
let mut pending_work = Vec::new();
631+
632+
for id in visibility_ids {
633+
let Definition::SingletonMethodVisibility(singleton_visibility) =
634+
self.graph.definitions().get(&id).unwrap()
635+
else {
636+
unreachable!()
637+
};
638+
639+
let str_id = *singleton_visibility.target();
640+
let uri_id = *singleton_visibility.uri_id();
641+
let offset = singleton_visibility.offset().clone();
642+
let lexical_nesting_id = *singleton_visibility.lexical_nesting_id();
643+
let receiver = *singleton_visibility.receiver();
644+
645+
let attached_namespace_id = if let Some(receiver_name_id) = receiver {
646+
let NameRef::Resolved(resolved) = self.graph.names().get(&receiver_name_id).unwrap() else {
647+
pending_work.push(Unit::Definition(id));
648+
continue;
649+
};
650+
let Some(namespace_id) = self.resolve_to_namespace(*resolved.declaration_id()) else {
651+
continue;
652+
};
653+
namespace_id
654+
} else {
655+
let Some(decl_id) = self.resolve_lexical_owner(lexical_nesting_id, id) else {
656+
continue;
657+
};
658+
decl_id
659+
};
660+
661+
let Some(singleton_id) = self.get_or_create_singleton_class(attached_namespace_id, true) else {
662+
continue;
663+
};
664+
665+
let Some(Declaration::Namespace(namespace)) = self.graph.declarations().get(&singleton_id) else {
666+
continue;
667+
};
668+
669+
let mut visibility_applied = false;
670+
let mut has_partial = false;
671+
672+
let ancestor_ids: Vec<Ancestor> = namespace.ancestors().iter().copied().collect();
673+
674+
for ancestor in ancestor_ids {
675+
match ancestor {
676+
Ancestor::Complete(ancestor_id) => {
677+
let has_member = self
678+
.graph
679+
.declarations()
680+
.get(&ancestor_id)
681+
.and_then(|decl| decl.as_namespace())
682+
.and_then(|ns| ns.member(&str_id))
683+
.is_some();
684+
685+
if has_member {
686+
self.create_declaration(str_id, id, singleton_id, |name| {
687+
Declaration::Method(Box::new(MethodDeclaration::new(name, singleton_id)))
688+
});
689+
visibility_applied = true;
690+
break;
691+
}
692+
}
693+
Ancestor::Partial(_) => has_partial = true,
694+
}
695+
}
696+
697+
if visibility_applied {
698+
continue;
699+
}
700+
701+
if has_partial {
702+
pending_work.push(Unit::Definition(id));
703+
} else {
704+
let method_name = self.graph.strings().get(&str_id).unwrap().as_str().to_string();
705+
let owner_name = self.graph.declarations().get(&singleton_id).unwrap().name().to_string();
706+
let diagnostic = Diagnostic::new(
707+
Rule::UndefinedSingletonMethodVisibilityTarget,
708+
uri_id,
709+
offset,
710+
format!("undefined singleton method `{method_name}` for visibility change in `{owner_name}`"),
711+
);
712+
self.graph.add_document_diagnostic(uri_id, diagnostic);
713+
}
714+
}
715+
716+
self.graph.extend_work(pending_work);
622717
}
623718

624719
/// Resolves retroactive method visibility changes (`private :foo`, `protected :foo`, `public :foo`).

rust/rubydex/src/resolution_tests.rs

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5306,4 +5306,155 @@ mod visibility_resolution_tests {
53065306

53075307
assert_visibility_eq!(context, "Foo::BAR", Visibility::Private);
53085308
}
5309+
5310+
#[test]
5311+
fn retroactive_singleton_method_visibility_on_direct_member() {
5312+
let mut context = GraphTest::new();
5313+
context.index_uri(
5314+
"file:///foo.rb",
5315+
r"
5316+
class Foo
5317+
def self.bar; end
5318+
def self.baz; end
5319+
5320+
private_class_method :bar
5321+
public_class_method :baz
5322+
end
5323+
",
5324+
);
5325+
context.resolve();
5326+
5327+
assert_no_diagnostics!(&context);
5328+
assert_visibility_eq!(context, "Foo::<Foo>#bar()", Visibility::Private);
5329+
assert_visibility_eq!(context, "Foo::<Foo>#baz()", Visibility::Public);
5330+
}
5331+
5332+
#[test]
5333+
fn retroactive_singleton_method_visibility_via_qualified_receiver() {
5334+
let mut context = GraphTest::new();
5335+
context.index_uri(
5336+
"file:///foo.rb",
5337+
r"
5338+
class Foo
5339+
def self.bar; end
5340+
end
5341+
5342+
Foo.private_class_method :bar
5343+
",
5344+
);
5345+
context.resolve();
5346+
5347+
assert_no_diagnostics!(&context);
5348+
assert_visibility_eq!(context, "Foo::<Foo>#bar()", Visibility::Private);
5349+
}
5350+
5351+
#[test]
5352+
fn retroactive_singleton_method_visibility_on_inherited_method() {
5353+
let mut context = GraphTest::new();
5354+
context.index_uri(
5355+
"file:///foo.rb",
5356+
r"
5357+
class Parent
5358+
def self.foo; end
5359+
end
5360+
5361+
class Child < Parent
5362+
private_class_method :foo
5363+
end
5364+
",
5365+
);
5366+
context.resolve();
5367+
5368+
assert_no_diagnostics!(&context);
5369+
assert_declaration_exists!(context, "Child::<Child>#foo()");
5370+
assert_visibility_eq!(context, "Child::<Child>#foo()", Visibility::Private);
5371+
assert_visibility_eq!(context, "Parent::<Parent>#foo()", Visibility::Public);
5372+
}
5373+
5374+
#[test]
5375+
fn retroactive_singleton_method_visibility_on_undefined_method_emits_diagnostic() {
5376+
let mut context = GraphTest::new();
5377+
context.index_uri(
5378+
"file:///foo.rb",
5379+
r"
5380+
class Foo
5381+
private_class_method :nonexistent
5382+
end
5383+
",
5384+
);
5385+
context.resolve();
5386+
5387+
assert_diagnostics_eq!(
5388+
context,
5389+
&[
5390+
"undefined-singleton-method-visibility-target: undefined singleton method `nonexistent()` for visibility change in `Foo::<Foo>` (2:25-2:36)"
5391+
]
5392+
);
5393+
}
5394+
5395+
#[test]
5396+
fn retroactive_singleton_method_visibility_clears_when_call_removed() {
5397+
let mut context = GraphTest::new();
5398+
context.index_uri(
5399+
"file:///foo.rb",
5400+
r"
5401+
class Parent
5402+
def self.foo; end
5403+
end
5404+
5405+
class Child < Parent; end
5406+
",
5407+
);
5408+
context.index_uri(
5409+
"file:///vis.rb",
5410+
r"
5411+
Child.private_class_method :foo
5412+
",
5413+
);
5414+
context.resolve();
5415+
5416+
assert_no_diagnostics!(&context);
5417+
assert_declaration_exists!(context, "Child::<Child>#foo()");
5418+
assert_visibility_eq!(context, "Child::<Child>#foo()", Visibility::Private);
5419+
5420+
context.delete_uri("file:///vis.rb");
5421+
context.resolve();
5422+
5423+
assert_no_diagnostics!(&context);
5424+
assert_declaration_does_not_exist!(context, "Child::<Child>#foo()");
5425+
assert_visibility_eq!(context, "Parent::<Parent>#foo()", Visibility::Public);
5426+
}
5427+
5428+
#[test]
5429+
fn retroactive_singleton_method_visibility_undefined_target_diagnostic_clears_when_file_deleted() {
5430+
let mut context = GraphTest::new();
5431+
context.index_uri(
5432+
"file:///foo.rb",
5433+
r"
5434+
class Foo
5435+
end
5436+
",
5437+
);
5438+
context.index_uri(
5439+
"file:///bad.rb",
5440+
r"
5441+
class Foo
5442+
private_class_method :missing
5443+
end
5444+
",
5445+
);
5446+
context.resolve();
5447+
5448+
assert_diagnostics_eq!(
5449+
context,
5450+
&[
5451+
"undefined-singleton-method-visibility-target: undefined singleton method `missing()` for visibility change in `Foo::<Foo>` (2:25-2:32)"
5452+
]
5453+
);
5454+
5455+
context.delete_uri("file:///bad.rb");
5456+
context.resolve();
5457+
5458+
assert_no_diagnostics!(&context);
5459+
}
53095460
}

test/declaration_test.rb

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -789,6 +789,58 @@ class Foo
789789
end
790790
end
791791

792+
def test_class_method_visibility_via_private_class_method
793+
with_context do |context|
794+
context.write!("file1.rb", <<~RUBY)
795+
class Foo
796+
def self.bar; end
797+
private_class_method :bar
798+
end
799+
RUBY
800+
801+
graph = Rubydex::Graph.new
802+
graph.index_all(context.glob("**/*.rb"))
803+
graph.resolve
804+
805+
assert_equal(:private, graph["Foo::<Foo>#bar()"].visibility)
806+
end
807+
end
808+
809+
def test_class_method_visibility_via_public_class_method
810+
with_context do |context|
811+
context.write!("file1.rb", <<~RUBY)
812+
class Foo
813+
def self.bar; end
814+
public_class_method :bar
815+
end
816+
RUBY
817+
818+
graph = Rubydex::Graph.new
819+
graph.index_all(context.glob("**/*.rb"))
820+
graph.resolve
821+
822+
assert_equal(:public, graph["Foo::<Foo>#bar()"].visibility)
823+
end
824+
end
825+
826+
def test_class_method_visibility_via_qualified_receiver
827+
with_context do |context|
828+
context.write!("file1.rb", <<~RUBY)
829+
class Foo
830+
def self.bar; end
831+
end
832+
833+
Foo.private_class_method :bar
834+
RUBY
835+
836+
graph = Rubydex::Graph.new
837+
graph.index_all(context.glob("**/*.rb"))
838+
graph.resolve
839+
840+
assert_equal(:private, graph["Foo::<Foo>#bar()"].visibility)
841+
end
842+
end
843+
792844
def test_constant_alias_visibility
793845
with_context do |context|
794846
context.write!("file1.rb", <<~RUBY)

0 commit comments

Comments
 (0)