Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 30 additions & 21 deletions lib/rdoc/parser/ruby.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
171 changes: 171 additions & 0 deletions test/rdoc/parser/ruby_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading