diff --git a/lib/rdoc/parser/ruby.rb b/lib/rdoc/parser/ruby.rb index ab0544e0b4..d79e72cfee 100644 --- a/lib/rdoc/parser/ruby.rb +++ b/lib/rdoc/parser/ruby.rb @@ -133,9 +133,13 @@ class Ruby < Parser # Matches an RBS inline type annotation line: #: followed by whitespace RBS_SIG_LINE = /\A#:\s/ # :nodoc: - attr_accessor :visibility + attr_writer :visibility attr_reader :container, :singleton, :in_proc_block + def visibility + @visibility == :module_function ? :private : @visibility + end + def initialize(top_level, content, options, stats) super @@ -414,7 +418,7 @@ def handle_meta_method_comment(comment, directives, node) handle_code_object_directives(@container, directives) is_call_node = node.is_a?(Prism::CallNode) singleton_method = false - visibility = @visibility + visibility = self.visibility attributes = rw = line_no = method_name = nil directives.each do |directive, (param, line)| case directive @@ -589,24 +593,22 @@ def change_method_to_module_function(names) # changes but the singleton method copies must not be documented return if document_suppressed? - new_methods = [] - @container.methods_matching(names) do |m| - s_m = m.dup - record_location(s_m) - s_m.singleton = true - new_methods << s_m - end - new_methods.each do |method| - method.visibility = :public - case method - when AnyMethod - @container.add_method(method) - when Attr - @container.add_attribute(method) - end - end + @container.methods_matching(names) { |method| add_module_function_copy(method) } end + private def add_module_function_copy(method) # :nodoc: + method = method.dup + record_location(method) + method.singleton = true + method.visibility = :public + case method + when AnyMethod + @container.add_method(method) + when Attr + @container.add_attribute(method) + end + end + def handle_code_object_directives(code_object, directives) # :nodoc: directives.each do |directive, (param)| # startdoc/stopdoc/enddoc are handled by apply_document_control_directive. @@ -718,6 +720,7 @@ def add_method(method_name, receiver_name:, receiver_fallback_type:, visibility: end private def internal_add_method(method_name, container, comment:, directives:, modifier_comment_lines: nil, line_no:, visibility:, singleton:, params:, calls_super:, block_params:, node_id:, type_signature_lines: nil) # :nodoc: + module_function = @visibility == :module_function && !singleton && container.equal?(@container) meth = AnyMethod.new(method_name, singleton: singleton) meth.comment = comment handle_code_object_directives(meth, directives) if directives @@ -743,7 +746,7 @@ def add_method(method_name, receiver_name:, receiver_fallback_type:, visibility: meth.type_signature_lines = type_signature_lines # An instance method `initialize` is documented as `::new` unless the # :notnew: directive is given - if method_name == 'initialize' && !singleton + if method_name == 'initialize' && !singleton && !module_function if meth.dont_rename_initialize meth.visibility = :protected else @@ -757,6 +760,8 @@ def add_method(method_name, receiver_name:, receiver_fallback_type:, visibility: container.add_method(meth) token_stream_loader = @colorizer_context.token_stream_loader(node_id) if node_id meth.start_collecting_tokens(:ruby, loader: token_stream_loader) + + add_module_function_copy(meth) if module_function end # Find or create module or class from a given module name using Ruby lexical @@ -1277,8 +1282,12 @@ def _visit_call_require(call_node) def _visit_call_module_function(call_node) return if @scanner.in_proc_block || @scanner.singleton - names = visibility_method_arguments(call_node, singleton: false)&.map(&:to_s) - @scanner.change_method_to_module_function(names) if names + if call_node.arguments.nil? + @scanner.visibility = :module_function + else + names = visibility_method_arguments(call_node, singleton: false)&.map(&:to_s) + @scanner.change_method_to_module_function(names) if names + end end def _visit_call_public_private_class_method(call_node, visibility) diff --git a/test/rdoc/parser/ruby_test.rb b/test/rdoc/parser/ruby_test.rb index 4ac663abb4..660aeae3d9 100644 --- a/test/rdoc/parser/ruby_test.rb +++ b/test/rdoc/parser/ruby_test.rb @@ -1403,6 +1403,176 @@ def m3; end assert_equal [:public, :public, :public], singleton_methods.map(&:visibility) end + def test_module_function_without_arguments + util_parser <<~RUBY + module JSON + def before; end + module_function + + # :call-seq: + # JSON.parse(source, opts = {}) -> object + # + # Parses a source. + def parse(source, opts = {}); end + def generate(object); end + end + RUBY + mod = @store.find_module_named 'JSON' + singleton_methods, instance_methods = mod.method_list.partition(&:singleton) + + assert_equal ['before', 'parse', 'generate'], instance_methods.map(&:name) + assert_equal [:public, :private, :private], instance_methods.map(&:visibility) + assert_equal ['parse', 'generate'], singleton_methods.map(&:name) + assert_equal [:public, :public], singleton_methods.map(&:visibility) + + parse = mod.find_method('parse', false) + singleton_parse = mod.find_method('parse', true) + assert_not_same parse, singleton_parse + assert_equal '(source, opts = {})', singleton_parse.params + assert_equal 'Parses a source.', singleton_parse.comment.text.strip + assert_equal 'JSON.parse(source, opts = {}) -> object', singleton_parse.call_seq + assert_equal 'JSON::parse', singleton_parse.full_name + assert_equal 'method-c-parse', singleton_parse.aref + assert_equal parse.token_stream, singleton_parse.token_stream + end + + def test_module_function_without_arguments_reset_by_visibility + util_parser <<~RUBY + module M + module_function + def first; end + public + def pub; end + module_function + def second; end + private + def pri; end + module_function + def third; end + protected + def pro; end + end + RUBY + mod = @store.find_module_named 'M' + instance_methods = mod.method_list.reject(&:singleton) + + assert_equal ['first', 'pub', 'second', 'pri', 'third', 'pro'], instance_methods.map(&:name) + assert_equal [:private, :public, :private, :private, :private, :protected], instance_methods.map(&:visibility) + assert_equal ['first', 'second', 'third'], mod.class_method_list.map(&:name) + end + + def test_module_function_without_arguments_preserved_by_named_visibility + util_parser <<~RUBY + module M + def before; end + module_function + def first; end + public :first + private :before + def second; end + module_function :before + public def third; end + def fourth; end + end + RUBY + mod = @store.find_module_named 'M' + instance_methods = mod.method_list.reject(&:singleton) + + assert_equal ['before', 'first', 'second', 'third', 'fourth'], instance_methods.map(&:name) + assert_equal [:private, :public, :private, :public, :private], instance_methods.map(&:visibility) + assert_equal ['first', 'second', 'before', 'third', 'fourth'], mod.class_method_list.map(&:name) + assert_equal [:public] * 5, mod.class_method_list.map(&:visibility) + end + + def test_module_function_without_arguments_scoped + util_parser <<~RUBY + module M + module_function + module Nested + def ordinary; end + module_function + def nested; end + end + class C + def ordinary; end + end + class << self + private + def singleton; end + end + def outer; end + end + module M + def reopened; end + end + module Other + def ordinary; end + end + RUBY + mod = @store.find_module_named 'M' + nested = @store.find_module_named 'M::Nested' + klass = @store.find_class_named 'M::C' + other = @store.find_module_named 'Other' + + assert_equal ['singleton', 'outer'], mod.class_method_list.map(&:name) + assert_equal [:private, :public], mod.class_method_list.map(&:visibility) + assert_equal :private, mod.find_method('outer', false).visibility + assert_equal :public, mod.find_method('reopened', false).visibility + assert_equal ['nested'], nested.class_method_list.map(&:name) + assert_equal :public, nested.find_method('ordinary', false).visibility + assert_equal :private, nested.find_method('nested', false).visibility + assert_equal [false], klass.method_list.map(&:singleton) + assert_equal [:public], klass.method_list.map(&:visibility) + assert_equal [false], other.method_list.map(&:singleton) + assert_equal [:public], other.method_list.map(&:visibility) + end + + def test_module_function_without_arguments_explicit_receivers_and_attributes + util_parser <<~RUBY + class NilClass; end + module M + module_function + def self.explicit; end + def M.named; end + def nil.literal; end + attr_reader :reader + def implicit; end + end + RUBY + mod = @store.find_module_named 'M' + nil_class = @store.find_class_named 'NilClass' + + assert_equal ['explicit', 'named', 'implicit'], mod.class_method_list.map(&:name) + assert_equal [:public] * 3, mod.class_method_list.map(&:visibility) + assert_equal ['implicit'], mod.method_list.reject(&:singleton).map(&:name) + assert_equal ['reader'], mod.attributes.map(&:name) + assert_equal [false], mod.attributes.map(&:singleton) + assert_equal [:private], mod.attributes.map(&:visibility) + assert_equal [false], nil_class.method_list.map(&:singleton) + assert_equal [:public], nil_class.method_list.map(&:visibility) + end + + def test_module_function_without_arguments_initialize_and_documentation_directives + util_parser <<~RUBY + module M + module_function + def initialize(value); end + def omitted; end # :nodoc: + # :stopdoc: + def hidden; end + # :startdoc: + def resumed; end + # :enddoc: + def ended; end + end + RUBY + mod = @store.find_module_named 'M' + + assert_equal ['initialize', 'initialize', 'resumed', 'resumed'], mod.method_list.map(&:name) + assert_equal [false, true, false, true], mod.method_list.map(&:singleton) + assert_equal [:private, :public, :private, :public], mod.method_list.map(&:visibility) + end + def test_class_method_visibility util_parser <<~RUBY class A @@ -2729,6 +2899,7 @@ def foo; end tap do end def foo; end module_function :foo + module_function def foo; end end def bar; end