diff --git a/lib/rdoc/parser/prism_ruby.rb b/lib/rdoc/parser/prism_ruby.rb
index 1ae1fa199a..42842d49a5 100644
--- a/lib/rdoc/parser/prism_ruby.rb
+++ b/lib/rdoc/parser/prism_ruby.rb
@@ -12,11 +12,16 @@
# RDoc::Parser::PrismRuby is compatible with RDoc::Parser::Ruby and aims to replace it.
class RDoc::Parser::PrismRuby < RDoc::Parser
-
parse_files_matching(/\.rbw?$/) if ENV['RDOC_USE_PRISM_PARSER']
- attr_accessor :visibility
- attr_reader :container, :singleton, :in_proc_block
+ # Nesting information
+ # container: ClassModule or TopLevel
+ # singleton: true(container is a singleton class) or false
+ # nodoc: true(in shallow nodoc) or false
+ # state: :startdoc, :stopdoc, :enddoc
+ # visibility: :public, :private, :protected
+ # block_level: block nesting level within current container. > 0 means in block
+ Nesting = Struct.new(:container, :singleton, :block_level, :visibility, :nodoc, :doc_state)
def initialize(top_level, content, options, stats)
super
@@ -31,11 +36,66 @@ def initialize(top_level, content, options, stats)
@track_visibility = :nodoc != @options.visibility
@encoding = @options.encoding
- @module_nesting = [[top_level, false]]
- @container = top_level
- @visibility = :public
- @singleton = false
- @in_proc_block = false
+ # Names of constant/class/module marked as nodoc in this file local scope
+ @file_local_nodoc_names = Set.new
+ # Represent module_nesting, visibility, block nesting level and startdoc/stopdoc/enddoc/nodoc for each module_nesting
+ @nestings = [Nesting.new(top_level, false, 0, :public, false, :startdoc)]
+ end
+
+ # Mark the given const/class/module full name as nodoc in current file local scope.
+ def locally_mark_const_name_as_nodoc(const_name)
+ @file_local_nodoc_names << const_name
+ end
+
+ # Returns true if the given container is marked as nodoc in current file local scope.
+ def locally_marked_as_nodoc?(container)
+ @file_local_nodoc_names.include?(container.full_name)
+ end
+
+ def current_nesting # :nodoc:
+ @nestings.last
+ end
+
+ # Current container code object (ClassModule or TopLevel) being processed
+ def current_container
+ current_nesting.container
+ end
+
+ # Returns true if current container is a singleton class
+ # False when in a normal class/module class A; end, true when in a singleton class class << A; end
+ def singleton?
+ current_nesting.singleton
+ end
+
+ # Returns true if currently inside a proc or block
+ # When true, `self` may not be the current container
+ def in_proc_block?
+ current_nesting.block_level > 0
+ end
+
+ # Current method visibility (:public, :private, :protected)
+ def current_visibility
+ current_nesting.visibility
+ end
+
+ def current_visibility=(v)
+ current_nesting.visibility = v
+ end
+
+ # Mark this container as documentable.
+ # When creating a container within nodoc scope, or creating intermediate modules when reached `class A::Intermediate::D`,
+ # the created container is marked as ignored. Documentable or not will be determined later.
+ # It may be undocumented if the container doesn't have any comment or documentable children,
+ # and will be documentable when receiving comment or documentable children later.
+ def mark_container_documentable(container)
+ return if container.received_nodoc || !container.ignored?
+ record_location(container)
+ container.start_doc
+ mark_container_documentable(container.parent) if container.parent.is_a?(RDoc::ClassModule)
+ end
+
+ def container_accept_document?(container) # :nodoc:
+ !current_nesting.nodoc && current_nesting.doc_state == :startdoc && !container.received_nodoc && !locally_marked_as_nodoc?(container)
end
# Suppress `extend` and `include` within block
@@ -43,31 +103,27 @@ def initialize(top_level, content, options, stats)
# example: `Module.new { include M }` `M.module_eval { include N }`
def with_in_proc_block
- in_proc_block = @in_proc_block
- @in_proc_block = true
+ current_nesting.block_level += 1
yield
- @in_proc_block = in_proc_block
+ current_nesting.block_level -= 1
end
# Dive into another container
def with_container(container, singleton: false)
- old_container = @container
- old_visibility = @visibility
- old_singleton = @singleton
- old_in_proc_block = @in_proc_block
- @visibility = :public
- @container = container
- @singleton = singleton
- @in_proc_block = false
- @module_nesting.push([container, singleton])
+ nesting = current_nesting
+ nodoc = locally_marked_as_nodoc?(container) || container.received_nodoc
+ @nestings << Nesting.new(
+ container,
+ singleton,
+ 0,
+ :public,
+ nodoc, # Set to true if container is marked as nodoc file-locally or globally. Not inherited from parene nesting.
+ nesting.doc_state # state(stardoc/stopdoc/enddoc) is inherited
+ )
yield container
ensure
- @container = old_container
- @visibility = old_visibility
- @singleton = old_singleton
- @in_proc_block = old_in_proc_block
- @module_nesting.pop
+ @nestings.pop
end
# Records the location of this +container+ in the file for this parser and
@@ -103,10 +159,44 @@ def scan
process_comments_until(@lines.size + 1)
end
- def should_document?(code_object) # :nodoc:
- return true unless @track_visibility
- return false if code_object.parent&.document_children == false
- code_object.document_self
+ # Apply document control directive such as :startdoc:, :stopdoc: and :enddoc: to the current container
+ def apply_document_control_directive(directives)
+ directives.each do |key, (value, _loc)|
+ case key
+ when 'startdoc', 'stopdoc'
+ state = key.to_sym
+ if current_nesting.doc_state == state || current_nesting.doc_state == :enddoc
+ warn "Already in :#{state}: state, ignoring"
+ else
+ current_nesting.doc_state = state
+ end
+ when 'enddoc'
+ if current_nesting.doc_state == :enddoc
+ warn "Already in :enddoc: state, ignoring"
+ else
+ current_nesting.doc_state = :enddoc
+ end
+ when 'nodoc'
+ if value == 'all'
+ current_nesting.doc_state = :enddoc
+ current_nesting.nodoc = true
+ # Globally mark container as nodoc
+ current_container.document_self = nil
+ elsif current_nesting.nodoc
+ warn "Already in :nodoc: state, ignoring"
+ elsif current_nesting.doc_state == :enddoc
+ warn "Already in :enddoc: state, ignoring"
+ else
+ # Mark this shallow scope as nodoc: methods and constants are not documented
+ current_nesting.nodoc = true
+ # And mark this scope as enddoc: nested containers are not documented
+ current_nesting.doc_state = :enddoc
+ # Mark container as nodoc in this file. When this container is reopened later,
+ # `nodoc!` will be applied again but `enddoc!` will not be applied.
+ locally_mark_const_name_as_nodoc(current_container.full_name) unless current_container.is_a?(RDoc::TopLevel)
+ end
+ end
+ end
end
# Assign AST node to a line.
@@ -216,7 +306,15 @@ def has_modifier_nodoc?(line_no) # :nodoc:
def handle_modifier_directive(code_object, line_no) # :nodoc:
if (comment_text = @modifier_comments[line_no])
_text, directives = @preprocess.parse_comment(comment_text, line_no, :ruby)
- handle_code_object_directives(code_object, directives)
+ if (value, = directives['nodoc'])
+ if value == 'all'
+ nodoc_state = :nodoc_all
+ else
+ nodoc_state = :nodoc
+ end
+ end
+ handle_code_object_directives(code_object, directives.except('nodoc'))
+ nodoc_state
end
end
@@ -235,10 +333,11 @@ def call_node_name_arguments(call_node) # :nodoc:
# Handles meta method comments
def handle_meta_method_comment(comment, directives, node)
- handle_code_object_directives(@container, directives)
+ apply_document_control_directive(directives)
+ handle_code_object_directives(current_container, directives)
is_call_node = node.is_a?(Prism::CallNode)
singleton_method = false
- visibility = @visibility
+ visibility = current_visibility
attributes = rw = line_no = method_name = nil
directives.each do |directive, (param, line)|
case directive
@@ -257,13 +356,16 @@ def handle_meta_method_comment(comment, directives, node)
end
end
+ return unless container_accept_document?(current_container)
+
if attributes
attributes.each do |attr|
- a = RDoc::Attr.new(@container, attr, rw, comment, singleton: @singleton)
+ a = RDoc::Attr.new(current_container, attr, rw, comment, singleton: singleton?)
a.store = @store
a.line = line_no
record_location(a)
- @container.add_attribute(a)
+ current_container.add_attribute(a)
+ mark_container_documentable(current_container)
a.visibility = visibility
end
elsif line_no || node
@@ -276,13 +378,13 @@ def handle_meta_method_comment(comment, directives, node)
end
internal_add_method(
method_name,
- @container,
+ current_container,
comment: comment,
directives: directives,
dont_rename_initialize: false,
line_no: line_no,
visibility: visibility,
- singleton: @singleton || singleton_method,
+ singleton: singleton? || singleton_method,
params: nil,
calls_super: false,
block_params: nil,
@@ -309,7 +411,8 @@ def handle_standalone_consecutive_comment_directive(comment, directives, start_w
elsif normal_comment_treat_as_ghost_method_for_now?(directives, line_no) && start_line != @first_non_meta_comment_start_line
handle_meta_method_comment(comment, directives, nil)
else
- handle_code_object_directives(@container, directives)
+ apply_document_control_directive(directives)
+ handle_code_object_directives(current_container, directives)
end
end
@@ -321,8 +424,8 @@ def process_comments_until(line_no_until)
if @markup == 'tomdoc'
comment = RDoc::Comment.new(text, @top_level, :ruby)
comment.format = 'tomdoc'
- parse_comment_tomdoc(@container, comment, line_no, start_line)
- @preprocess.run_post_processes(comment, @container)
+ parse_comment_tomdoc(current_container, comment, line_no, start_line)
+ @preprocess.run_post_processes(comment, current_container)
elsif (comment_text, directives = parse_comment_text_to_directives(text, start_line))
handle_standalone_consecutive_comment_directive(comment_text, directives, text.start_with?(/#\#$/), line_no, start_line)
end
@@ -357,10 +460,10 @@ def parse_comment_text_to_directives(comment_text, start_line) # :nodoc:
comment.format = markup&.downcase || @markup
if (section, = directives['section'])
# If comment has :section:, it is not a documentable comment for a code object
- @container.set_current_section(section, comment.dup)
+ current_container.set_current_section(section, comment.dup)
return
end
- @preprocess.run_post_processes(comment, @container)
+ @preprocess.run_post_processes(comment, current_container)
[comment, directives]
end
@@ -393,10 +496,10 @@ def visible_tokens_from_location(location)
# Handles `public :foo, :bar` `private :foo, :bar` and `protected :foo, :bar`
- def change_method_visibility(names, visibility, singleton: @singleton)
+ def change_method_visibility(names, visibility, singleton: singleton?)
new_methods = []
- @container.methods_matching(names, singleton) do |m|
- if m.parent != @container
+ current_container.methods_matching(names, singleton) do |m|
+ if m.parent != current_container
m = m.dup
record_location(m)
new_methods << m
@@ -407,9 +510,9 @@ def change_method_visibility(names, visibility, singleton: @singleton)
new_methods.each do |method|
case method
when RDoc::AnyMethod then
- @container.add_method(method)
+ current_container.add_method(method)
when RDoc::Attr then
- @container.add_attribute(method)
+ current_container.add_attribute(method)
end
method.visibility = visibility
end
@@ -418,9 +521,9 @@ def change_method_visibility(names, visibility, singleton: @singleton)
# Handles `module_function :foo, :bar`
def change_method_to_module_function(names)
- @container.set_visibility_for(names, :private, false)
+ current_container.set_visibility_for(names, :private, false)
new_methods = []
- @container.methods_matching(names) do |m|
+ current_container.methods_matching(names) do |m|
s_m = m.dup
record_location(s_m)
s_m.singleton = true
@@ -429,9 +532,9 @@ def change_method_to_module_function(names)
new_methods.each do |method|
case method
when RDoc::AnyMethod then
- @container.add_method(method)
+ current_container.add_method(method)
when RDoc::Attr then
- @container.add_attribute(method)
+ current_container.add_attribute(method)
end
method.visibility = :public
end
@@ -439,6 +542,7 @@ def change_method_to_module_function(names)
def handle_code_object_directives(code_object, directives) # :nodoc:
directives.each do |directive, (param)|
+ next if directive in 'nodoc' | 'startdoc' | 'stopdoc' | 'enddoc'
@preprocess.handle_directive('', directive, param, code_object)
end
end
@@ -447,34 +551,40 @@ def handle_code_object_directives(code_object, directives) # :nodoc:
def add_alias_method(old_name, new_name, line_no)
comment, directives = consecutive_comment(line_no)
- handle_code_object_directives(@container, directives) if directives
- visibility = @container.find_method(old_name, @singleton)&.visibility || :public
- a = RDoc::Alias.new(nil, old_name, new_name, comment, singleton: @singleton)
- handle_modifier_directive(a, line_no)
+ apply_document_control_directive(directives) if directives
+ handle_code_object_directives(current_container, directives) if directives
+ visibility = current_container.find_method(old_name, singleton?)&.visibility || :public
+ a = RDoc::Alias.new(nil, old_name, new_name, comment, singleton: singleton?)
+ modifier_nodoc = handle_modifier_directive(a, line_no)
+
+ return unless container_accept_document?(current_container) && !(@track_visibility && modifier_nodoc)
+
a.store = @store
a.line = line_no
+ mark_container_documentable(current_container)
record_location(a)
- if should_document?(a)
- @container.add_alias(a)
- @container.find_method(new_name, @singleton)&.visibility = visibility
- end
+ current_container.add_alias(a)
+ current_container.find_method(new_name, singleton?)&.visibility = visibility
end
# Handles `attr :a, :b`, `attr_reader :a, :b`, `attr_writer :a, :b` and `attr_accessor :a, :b`
def add_attributes(names, rw, line_no)
comment, directives = consecutive_comment(line_no)
- handle_code_object_directives(@container, directives) if directives
- return unless @container.document_children
+ apply_document_control_directive(directives) if directives
+ handle_code_object_directives(current_container, directives) if directives
+ return unless container_accept_document?(current_container)
names.each do |symbol|
- a = RDoc::Attr.new(nil, symbol.to_s, rw, comment, singleton: @singleton)
+ a = RDoc::Attr.new(nil, symbol.to_s, rw, comment, singleton: singleton?)
a.store = @store
a.line = line_no
+ modifier_nodoc = handle_modifier_directive(a, line_no)
+ next if @track_visibility && modifier_nodoc
record_location(a)
- handle_modifier_directive(a, line_no)
- @container.add_attribute(a) if should_document?(a)
- a.visibility = visibility # should set after adding to container
+ current_container.add_attribute(a)
+ mark_container_documentable(current_container)
+ a.visibility = current_visibility # should set after adding to container
end
end
@@ -482,10 +592,16 @@ def add_attributes(names, rw, line_no)
def add_includes_extends(names, rdoc_class, line_no) # :nodoc:
comment, directives = consecutive_comment(line_no)
- handle_code_object_directives(@container, directives) if directives
+ apply_document_control_directive(directives) if directives
+ handle_code_object_directives(current_container, directives) if directives
+
+ return unless container_accept_document?(current_container)
+
+ mark_container_documentable(current_container)
+
names.each do |name|
resolved_name = resolve_constant_path(name)
- ie = @container.add(rdoc_class, resolved_name || name, '')
+ ie = current_container.add(rdoc_class, resolved_name || name, '')
ie.store = @store
ie.line = line_no
ie.comment = comment
@@ -508,9 +624,10 @@ def add_extends(names, line_no) # :nodoc:
# Adds a method defined by `def` syntax
def add_method(method_name, receiver_name:, receiver_fallback_type:, visibility:, singleton:, params:, calls_super:, block_params:, tokens:, start_line:, args_end_line:, end_line:)
- receiver = receiver_name ? find_or_create_module_path(receiver_name, receiver_fallback_type) : @container
+ receiver = receiver_name ? find_or_create_module_path(receiver_name, receiver_fallback_type) : current_container
comment, directives = consecutive_comment(start_line)
- handle_code_object_directives(@container, directives) if directives
+ apply_document_control_directive(directives) if directives
+ handle_code_object_directives(current_container, directives) if directives
internal_add_method(
method_name,
@@ -532,10 +649,18 @@ def add_method(method_name, receiver_name:, receiver_fallback_type:, visibility:
meth = RDoc::AnyMethod.new(nil, method_name, singleton: singleton)
meth.comment = comment
handle_code_object_directives(meth, directives) if directives
+ modifier_nodoc = nil
modifier_comment_lines&.each do |line|
- handle_modifier_directive(meth, line)
+ modifier_nodoc ||= handle_modifier_directive(meth, line)
end
- return unless should_document?(meth)
+
+ return unless container_accept_document?(container)
+ if modifier_nodoc
+ return if @track_visibility
+ meth.document_self = nil
+ end
+
+ mark_container_documentable(container)
if directives && (call_seq, = directives['call-seq'])
meth.call_seq = call_seq.lines.map(&:chomp).reject(&:empty?).join("\n") if call_seq
@@ -573,28 +698,35 @@ def add_method(method_name, receiver_name:, receiver_fallback_type:, visibility:
def find_or_create_module_path(module_name, create_mode)
root_name, *path, name = module_name.split('::')
+
+ # Creates intermediate modules/classes if they do not exist.
+ # The created module may not be documented if it does not have comment nor documentable children.
add_module = ->(mod, name, mode) {
- case mode
- when :class
- mod.add_class(RDoc::NormalClass, name, 'Object').tap { |m| m.store = @store }
- when :module
- mod.add_module(RDoc::NormalModule, name).tap { |m| m.store = @store }
- end
+ created =
+ case mode
+ when :class
+ mod.add_class(RDoc::NormalClass, name, 'Object').tap { |m| m.store = @store }
+ when :module
+ mod.add_module(RDoc::NormalModule, name).tap { |m| m.store = @store }
+ end
+ # Set to true later if this module receives comment or documentable children
+ created.ignore
+ created
}
if root_name.empty?
mod = @top_level
else
- @module_nesting.reverse_each do |nesting, singleton|
- next if singleton
- mod = nesting.get_module_named(root_name)
+ @nestings.reverse_each do |nesting|
+ next if nesting.singleton
+ mod = nesting.container.get_module_named(root_name)
break if mod
# If a constant is found and it is not a module or class, RDoc can't document about it.
# Return an anonymous module to avoid wrong document creation.
- return RDoc::NormalModule.new(nil) if nesting.find_constant_named(root_name)
+ return RDoc::NormalModule.new(nil) if nesting.container.find_constant_named(root_name)
end
- last_nesting, = @module_nesting.reverse_each.find { |_, singleton| !singleton }
- return mod || add_module.call(last_nesting, root_name, create_mode) unless name
- mod ||= add_module.call(last_nesting, root_name, :module)
+ last_nesting = @nestings.reverse_each.find { |nesting| !nesting.singleton }
+ return mod || add_module.call(last_nesting.container, root_name, create_mode) unless name
+ mod ||= add_module.call(last_nesting.container, root_name, :module)
end
path.each do |name|
mod = mod.get_module_named(name) || add_module.call(mod, name, :module)
@@ -608,9 +740,9 @@ def resolve_constant_path(constant_path)
owner_name, path = constant_path.split('::', 2)
return constant_path if owner_name.empty? # ::Foo, ::Foo::Bar
mod = nil
- @module_nesting.reverse_each do |nesting, singleton|
- next if singleton
- mod = nesting.get_module_named(owner_name)
+ @nestings.reverse_each do |nesting|
+ next if nesting.singleton
+ mod = nesting.container.get_module_named(owner_name)
break if mod
end
mod ||= @top_level.get_module_named(owner_name)
@@ -626,7 +758,7 @@ def find_or_create_constant_owner_name(constant_path)
# Within `class C` or `module C`, owner is C(== current container)
# Within `class <