diff --git a/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb b/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb index 4d977e317..4789a5c22 100644 --- a/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb +++ b/lib/ruby_indexer/lib/ruby_indexer/declaration_listener.rb @@ -282,6 +282,9 @@ def on_call_node_enter(node) @visibility_stack.push(Entry::Visibility::PRIVATE) when :module_function handle_module_function(node) + when :private_class_method + @visibility_stack.push(Entry::Visibility::PRIVATE) + handle_private_class_method(node) end @enhancements.each do |enhancement| @@ -297,7 +300,7 @@ def on_call_node_enter(node) def on_call_node_leave(node) message = node.name case message - when :public, :protected, :private + when :public, :protected, :private, :private_class_method # We want to restore the visibility stack when we leave a method definition with a visibility modifier # e.g. `private def foo; end` if node.arguments&.arguments&.first&.is_a?(Prism::DefNode) @@ -875,6 +878,45 @@ def handle_module_function(node) end end + sig { params(node: Prism::CallNode).void } + def handle_private_class_method(node) + node.arguments&.arguments&.each do |argument| + string_or_symbol_nodes = case argument + when Prism::StringNode, Prism::SymbolNode + [argument] + when Prism::ArrayNode + argument.elements + else + [] + end + + unless string_or_symbol_nodes.empty? + # pop the visibility off since there isn't a method definition following `private_class_method` + @visibility_stack.pop + end + + string_or_symbol_nodes.each do |string_or_symbol_node| + method_name = case string_or_symbol_node + when Prism::StringNode + string_or_symbol_node.content + when Prism::SymbolNode + string_or_symbol_node.value + end + next unless method_name + + owner_name = @owner_stack.last&.name + next unless owner_name + + entries = @index.resolve_method(method_name, @index.existing_or_new_singleton_class(owner_name).name) + next unless entries + + entries.each do |entry| + entry.visibility = Entry::Visibility::PRIVATE + end + end + end + end + sig { returns(Entry::Visibility) } def current_visibility T.must(@visibility_stack.last) diff --git a/lib/ruby_indexer/test/method_test.rb b/lib/ruby_indexer/test/method_test.rb index 8e45a54b5..c0fc4bcf1 100644 --- a/lib/ruby_indexer/test/method_test.rb +++ b/lib/ruby_indexer/test/method_test.rb @@ -149,6 +149,86 @@ def bar; end end end + def test_private_class_method_visibility_tracking_string_symbol_arguments + index(<<~RUBY) + class Test + def self.foo + end + + def self.bar + end + + private_class_method("foo", :bar) + + def self.baz + end + end + RUBY + + ["foo", "bar"].each do |keyword| + entries = T.must(@index[keyword]) + assert_equal(1, entries.size) + entry = entries.first + assert_predicate(entry, :private?) + end + + entries = T.must(@index["baz"]) + assert_equal(1, entries.size) + entry = entries.first + assert_predicate(entry, :public?) + end + + def test_private_class_method_visibility_tracking_array_argument + index(<<~RUBY) + class Test + def self.foo + end + + def self.bar + end + + private_class_method(["foo", :bar]) + + def self.baz + end + end + RUBY + + ["foo", "bar"].each do |keyword| + entries = T.must(@index[keyword]) + assert_equal(1, entries.size) + entry = entries.first + assert_predicate(entry, :private?) + end + + entries = T.must(@index["baz"]) + assert_equal(1, entries.size) + entry = entries.first + assert_predicate(entry, :public?) + end + + def test_private_class_method_visibility_tracking_method_argument + index(<<~RUBY) + class Test + private_class_method def self.foo + end + + def self.bar + end + end + RUBY + + entries = T.must(@index["foo"]) + assert_equal(1, entries.size) + entry = entries.first + assert_predicate(entry, :private?) + + entries = T.must(@index["bar"]) + assert_equal(1, entries.size) + entry = entries.first + assert_predicate(entry, :public?) + end + def test_comments_documentation index(<<~RUBY) # Documentation for Foo