mirror of
https://github.com/ruby/ruby.git
synced 2025-08-15 21:49:06 +02:00

(https://github.com/ruby/rdoc/pull/1144)
* Add a new ruby parser RDoc::Parser::PrismRuby
* Add a new ruby parser testcase independent from parser's internal implementation
* unknown meta method
* Use MethodSignatureVisitor only to scan params, block_params and calls_super
* Add calls_super test
* Drop ruby 2.6. Prism requires ruby >= 2.7
* Remove duplicated documentation comment from prism_ruby.rb
* Add test for wrong argument passed to metaprogramming method
* Rename visit_call_[DSL_METHOD_NAME] to make it distinguishable from visit_[NODE_TYPE]_node
* Method receiver switch of true/false/nil to a case statement
* Extract common part of add_method(by def keyword) and add meta_comment method
* Reuse consecutive comments array when collecting comments
* Simplify DSL call_node handling
* Refactor extracting method visibility arguments
fde99f1be6
1026 lines
34 KiB
Ruby
1026 lines
34 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'prism'
|
|
require_relative 'ripper_state_lex'
|
|
|
|
# Unlike lib/rdoc/parser/ruby.rb, this file is not based on rtags and does not contain code from
|
|
# rtags.rb -
|
|
# ruby-lex.rb - ruby lexcal analyzer
|
|
# ruby-token.rb - ruby tokens
|
|
|
|
# Parse and collect document from Ruby source code.
|
|
# 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
|
|
|
|
def initialize(top_level, file_name, content, options, stats)
|
|
super
|
|
|
|
content = handle_tab_width(content)
|
|
|
|
@size = 0
|
|
@token_listeners = nil
|
|
content = RDoc::Encoding.remove_magic_comment content
|
|
@content = content
|
|
@markup = @options.markup
|
|
@track_visibility = :nodoc != @options.visibility
|
|
@encoding = @options.encoding
|
|
|
|
@module_nesting = [top_level]
|
|
@container = top_level
|
|
@visibility = :public
|
|
@singleton = false
|
|
end
|
|
|
|
# Dive into another container
|
|
|
|
def with_container(container, singleton: false)
|
|
old_container = @container
|
|
old_visibility = @visibility
|
|
old_singleton = @singleton
|
|
@visibility = :public
|
|
@container = container
|
|
@singleton = singleton
|
|
unless singleton
|
|
@module_nesting.push container
|
|
|
|
# Need to update module parent chain to emulate Module.nesting.
|
|
# This mechanism is inaccurate and needs to be fixed.
|
|
container.parent = old_container
|
|
end
|
|
yield container
|
|
ensure
|
|
@container = old_container
|
|
@visibility = old_visibility
|
|
@singleton = old_singleton
|
|
@module_nesting.pop unless singleton
|
|
end
|
|
|
|
# Records the location of this +container+ in the file for this parser and
|
|
# adds it to the list of classes and modules in the file.
|
|
|
|
def record_location container # :nodoc:
|
|
case container
|
|
when RDoc::ClassModule then
|
|
@top_level.add_to_classes_or_modules container
|
|
end
|
|
|
|
container.record_location @top_level
|
|
end
|
|
|
|
# Scans this Ruby file for Ruby constructs
|
|
|
|
def scan
|
|
@tokens = RDoc::Parser::RipperStateLex.parse(@content)
|
|
@lines = @content.lines
|
|
result = Prism.parse(@content)
|
|
@program_node = result.value
|
|
@line_nodes = {}
|
|
prepare_line_nodes(@program_node)
|
|
prepare_comments(result.comments)
|
|
return if @top_level.done_documenting
|
|
|
|
@first_non_meta_comment = nil
|
|
if (_line_no, start_line, rdoc_comment = @unprocessed_comments.first)
|
|
@first_non_meta_comment = rdoc_comment if start_line < @program_node.location.start_line
|
|
end
|
|
|
|
@program_node.accept(RDocVisitor.new(self, @top_level, @store))
|
|
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
|
|
end
|
|
|
|
# Assign AST node to a line.
|
|
# This is used to show meta-method source code in the documentation.
|
|
|
|
def prepare_line_nodes(node) # :nodoc:
|
|
case node
|
|
when Prism::CallNode, Prism::DefNode
|
|
@line_nodes[node.location.start_line] ||= node
|
|
end
|
|
node.compact_child_nodes.each do |child|
|
|
prepare_line_nodes(child)
|
|
end
|
|
end
|
|
|
|
# Prepares comments for processing. Comments are grouped into consecutive.
|
|
# Consecutive comment is linked to the next non-blank line.
|
|
#
|
|
# Example:
|
|
# 01| class A # modifier comment 1
|
|
# 02| def foo; end # modifier comment 2
|
|
# 03|
|
|
# 04| # consecutive comment 1 start_line: 4
|
|
# 05| # consecutive comment 1 linked to line: 7
|
|
# 06|
|
|
# 07| # consecutive comment 2 start_line: 7
|
|
# 08| # consecutive comment 2 linked to line: 10
|
|
# 09|
|
|
# 10| def bar; end # consecutive comment 2 linked to this line
|
|
# 11| end
|
|
|
|
def prepare_comments(comments)
|
|
current = []
|
|
consecutive_comments = [current]
|
|
@modifier_comments = {}
|
|
comments.each do |comment|
|
|
if comment.is_a? Prism::EmbDocComment
|
|
consecutive_comments << [comment] << (current = [])
|
|
elsif comment.location.start_line_slice.match?(/\S/)
|
|
@modifier_comments[comment.location.start_line] = RDoc::Comment.new(comment.slice, @top_level, :ruby)
|
|
elsif current.empty? || current.last.location.end_line + 1 == comment.location.start_line
|
|
current << comment
|
|
else
|
|
consecutive_comments << (current = [comment])
|
|
end
|
|
end
|
|
consecutive_comments.reject!(&:empty?)
|
|
|
|
# Example: line_no = 5, start_line = 2, comment_text = "# comment_start_line\n# comment\n"
|
|
# 1| class A
|
|
# 2| # comment_start_line
|
|
# 3| # comment
|
|
# 4|
|
|
# 5| def f; end # comment linked to this line
|
|
# 6| end
|
|
@unprocessed_comments = consecutive_comments.map! do |comments|
|
|
start_line = comments.first.location.start_line
|
|
line_no = comments.last.location.end_line + (comments.last.location.end_column == 0 ? 0 : 1)
|
|
texts = comments.map do |c|
|
|
c.is_a?(Prism::EmbDocComment) ? c.slice.lines[1...-1].join : c.slice
|
|
end
|
|
text = RDoc::Encoding.change_encoding(texts.join("\n"), @encoding) if @encoding
|
|
line_no += 1 while @lines[line_no - 1]&.match?(/\A\s*$/)
|
|
comment = RDoc::Comment.new(text, @top_level, :ruby)
|
|
comment.line = start_line
|
|
[line_no, start_line, comment]
|
|
end
|
|
|
|
# The first comment is special. It defines markup for the rest of the comments.
|
|
_, first_comment_start_line, first_comment_text = @unprocessed_comments.first
|
|
if first_comment_text && @lines[0...first_comment_start_line - 1].all? { |l| l.match?(/\A\s*$/) }
|
|
comment = RDoc::Comment.new(first_comment_text.text, @top_level, :ruby)
|
|
handle_consecutive_comment_directive(@container, comment)
|
|
@markup = comment.format
|
|
end
|
|
@unprocessed_comments.each do |_, _, comment|
|
|
comment.format = @markup
|
|
end
|
|
end
|
|
|
|
# Creates an RDoc::Method on +container+ from +comment+ if there is a
|
|
# Signature section in the comment
|
|
|
|
def parse_comment_tomdoc(container, comment, line_no, start_line)
|
|
return unless signature = RDoc::TomDoc.signature(comment)
|
|
|
|
name, = signature.split %r%[ \(]%, 2
|
|
|
|
meth = RDoc::GhostMethod.new comment.text, name
|
|
record_location(meth)
|
|
meth.line = start_line
|
|
meth.call_seq = signature
|
|
return unless meth.name
|
|
|
|
meth.start_collecting_tokens
|
|
node = @line_nodes[line_no]
|
|
tokens = node ? visible_tokens_from_location(node.location) : [file_line_comment_token(start_line)]
|
|
tokens.each { |token| meth.token_stream << token }
|
|
|
|
container.add_method meth
|
|
comment.remove_private
|
|
comment.normalize
|
|
meth.comment = comment
|
|
@stats.add_method meth
|
|
end
|
|
|
|
def handle_modifier_directive(code_object, line_no) # :nodoc:
|
|
comment = @modifier_comments[line_no]
|
|
@preprocess.handle(comment.text, code_object) if comment
|
|
end
|
|
|
|
def handle_consecutive_comment_directive(code_object, comment) # :nodoc:
|
|
return unless comment
|
|
@preprocess.handle(comment, code_object) do |directive, param|
|
|
case directive
|
|
when 'method', 'singleton-method',
|
|
'attr', 'attr_accessor', 'attr_reader', 'attr_writer' then
|
|
# handled elsewhere
|
|
''
|
|
when 'section' then
|
|
@container.set_current_section(param, comment.dup)
|
|
comment.text = ''
|
|
break
|
|
end
|
|
end
|
|
comment.remove_private
|
|
end
|
|
|
|
def call_node_name_arguments(call_node) # :nodoc:
|
|
return [] unless call_node.arguments
|
|
call_node.arguments.arguments.map do |arg|
|
|
case arg
|
|
when Prism::SymbolNode
|
|
arg.value
|
|
when Prism::StringNode
|
|
arg.unescaped
|
|
end
|
|
end || []
|
|
end
|
|
|
|
# Handles meta method comments
|
|
|
|
def handle_meta_method_comment(comment, node)
|
|
is_call_node = node.is_a?(Prism::CallNode)
|
|
singleton_method = false
|
|
visibility = @visibility
|
|
attributes = rw = line_no = method_name = nil
|
|
|
|
processed_comment = comment.dup
|
|
@preprocess.handle(processed_comment, @container) do |directive, param, line|
|
|
case directive
|
|
when 'attr', 'attr_reader', 'attr_writer', 'attr_accessor'
|
|
attributes = [param] if param
|
|
attributes ||= call_node_name_arguments(node) if is_call_node
|
|
rw = directive == 'attr_writer' ? 'W' : directive == 'attr_accessor' ? 'RW' : 'R'
|
|
''
|
|
when 'method'
|
|
method_name = param
|
|
line_no = line
|
|
''
|
|
when 'singleton-method'
|
|
method_name = param
|
|
line_no = line
|
|
singleton_method = true
|
|
visibility = :public
|
|
''
|
|
when 'section' then
|
|
@container.set_current_section(param, comment.dup)
|
|
return # If the comment contains :section:, it is not a meta method comment
|
|
end
|
|
end
|
|
|
|
if attributes
|
|
attributes.each do |attr|
|
|
a = RDoc::Attr.new(@container, attr, rw, processed_comment)
|
|
a.store = @store
|
|
a.line = line_no
|
|
a.singleton = @singleton
|
|
record_location(a)
|
|
@container.add_attribute(a)
|
|
a.visibility = visibility
|
|
end
|
|
elsif line_no || node
|
|
method_name ||= call_node_name_arguments(node).first if is_call_node
|
|
meth = RDoc::AnyMethod.new(@container, method_name)
|
|
meth.singleton = @singleton || singleton_method
|
|
handle_consecutive_comment_directive(meth, comment)
|
|
comment.normalize
|
|
comment.extract_call_seq(meth)
|
|
meth.comment = comment
|
|
if node
|
|
tokens = visible_tokens_from_location(node.location)
|
|
line_no = node.location.start_line
|
|
else
|
|
tokens = [file_line_comment_token(line_no)]
|
|
end
|
|
internal_add_method(
|
|
@container,
|
|
meth,
|
|
line_no: line_no,
|
|
visibility: visibility,
|
|
singleton: @singleton || singleton_method,
|
|
params: '()',
|
|
calls_super: false,
|
|
block_params: nil,
|
|
tokens: tokens
|
|
)
|
|
end
|
|
end
|
|
|
|
def normal_comment_treat_as_ghost_method_for_now?(comment_text, line_no) # :nodoc:
|
|
# Meta method comment should start with `##` but some comments does not follow this rule.
|
|
# For now, RDoc accepts them as a meta method comment if there is no node linked to it.
|
|
!@line_nodes[line_no] && comment_text.match?(/^#\s+:(method|singleton-method|attr|attr_reader|attr_writer|attr_accessor):/)
|
|
end
|
|
|
|
def handle_standalone_consecutive_comment_directive(comment, line_no, start_line) # :nodoc:
|
|
if @markup == 'tomdoc'
|
|
parse_comment_tomdoc(@container, comment, line_no, start_line)
|
|
return
|
|
end
|
|
|
|
if comment.text =~ /\A#\#$/ && comment != @first_non_meta_comment
|
|
node = @line_nodes[line_no]
|
|
handle_meta_method_comment(comment, node)
|
|
elsif normal_comment_treat_as_ghost_method_for_now?(comment.text, line_no) && comment != @first_non_meta_comment
|
|
handle_meta_method_comment(comment, nil)
|
|
else
|
|
handle_consecutive_comment_directive(@container, comment)
|
|
end
|
|
end
|
|
|
|
# Processes consecutive comments that were not linked to any documentable code until the given line number
|
|
|
|
def process_comments_until(line_no_until)
|
|
while !@unprocessed_comments.empty? && @unprocessed_comments.first[0] <= line_no_until
|
|
line_no, start_line, rdoc_comment = @unprocessed_comments.shift
|
|
handle_standalone_consecutive_comment_directive(rdoc_comment, line_no, start_line)
|
|
end
|
|
end
|
|
|
|
# Skips all undocumentable consecutive comments until the given line number.
|
|
# Undocumentable comments are comments written inside `def` or inside undocumentable class/module
|
|
|
|
def skip_comments_until(line_no_until)
|
|
while !@unprocessed_comments.empty? && @unprocessed_comments.first[0] <= line_no_until
|
|
@unprocessed_comments.shift
|
|
end
|
|
end
|
|
|
|
# Returns consecutive comment linked to the given line number
|
|
|
|
def consecutive_comment(line_no)
|
|
if @unprocessed_comments.first&.first == line_no
|
|
@unprocessed_comments.shift.last
|
|
end
|
|
end
|
|
|
|
def slice_tokens(start_pos, end_pos) # :nodoc:
|
|
start_index = @tokens.bsearch_index { |t| ([t.line_no, t.char_no] <=> start_pos) >= 0 }
|
|
end_index = @tokens.bsearch_index { |t| ([t.line_no, t.char_no] <=> end_pos) >= 0 }
|
|
tokens = @tokens[start_index...end_index]
|
|
tokens.pop if tokens.last&.kind == :on_nl
|
|
tokens
|
|
end
|
|
|
|
def file_line_comment_token(line_no) # :nodoc:
|
|
position_comment = RDoc::Parser::RipperStateLex::Token.new(line_no - 1, 0, :on_comment)
|
|
position_comment[:text] = "# File #{@top_level.relative_name}, line #{line_no}"
|
|
position_comment
|
|
end
|
|
|
|
# Returns tokens from the given location
|
|
|
|
def visible_tokens_from_location(location)
|
|
position_comment = file_line_comment_token(location.start_line)
|
|
newline_token = RDoc::Parser::RipperStateLex::Token.new(0, 0, :on_nl, "\n")
|
|
indent_token = RDoc::Parser::RipperStateLex::Token.new(location.start_line, 0, :on_sp, ' ' * location.start_character_column)
|
|
tokens = slice_tokens(
|
|
[location.start_line, location.start_character_column],
|
|
[location.end_line, location.end_character_column]
|
|
)
|
|
[position_comment, newline_token, indent_token, *tokens]
|
|
end
|
|
|
|
# Handles `public :foo, :bar` `private :foo, :bar` and `protected :foo, :bar`
|
|
|
|
def change_method_visibility(names, visibility, singleton: @singleton)
|
|
new_methods = []
|
|
@container.methods_matching(names, singleton) do |m|
|
|
if m.parent != @container
|
|
m = m.dup
|
|
record_location(m)
|
|
new_methods << m
|
|
else
|
|
m.visibility = visibility
|
|
end
|
|
end
|
|
new_methods.each do |method|
|
|
case method
|
|
when RDoc::AnyMethod then
|
|
@container.add_method(method)
|
|
when RDoc::Attr then
|
|
@container.add_attribute(method)
|
|
end
|
|
method.visibility = visibility
|
|
end
|
|
end
|
|
|
|
# Handles `module_function :foo, :bar`
|
|
|
|
def change_method_to_module_function(names)
|
|
@container.set_visibility_for(names, :private, false)
|
|
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|
|
|
case method
|
|
when RDoc::AnyMethod then
|
|
@container.add_method(method)
|
|
when RDoc::Attr then
|
|
@container.add_attribute(method)
|
|
end
|
|
method.visibility = :public
|
|
end
|
|
end
|
|
|
|
# Handles `alias foo bar` and `alias_method :foo, :bar`
|
|
|
|
def add_alias_method(old_name, new_name, line_no)
|
|
comment = consecutive_comment(line_no)
|
|
handle_consecutive_comment_directive(@container, comment)
|
|
visibility = @container.find_method(old_name, @singleton)&.visibility || :public
|
|
a = RDoc::Alias.new(nil, old_name, new_name, comment, @singleton)
|
|
a.comment = comment
|
|
handle_modifier_directive(a, line_no)
|
|
a.store = @store
|
|
a.line = line_no
|
|
record_location(a)
|
|
if should_document?(a)
|
|
@container.add_alias(a)
|
|
@container.find_method(new_name, @singleton)&.visibility = visibility
|
|
end
|
|
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 = consecutive_comment(line_no)
|
|
handle_consecutive_comment_directive(@container, comment)
|
|
return unless @container.document_children
|
|
|
|
names.each do |symbol|
|
|
a = RDoc::Attr.new(nil, symbol.to_s, rw, comment)
|
|
a.store = @store
|
|
a.line = line_no
|
|
a.singleton = @singleton
|
|
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
|
|
end
|
|
end
|
|
|
|
def add_includes_extends(names, rdoc_class, line_no) # :nodoc:
|
|
comment = consecutive_comment(line_no)
|
|
handle_consecutive_comment_directive(@container, comment)
|
|
names.each do |name|
|
|
ie = @container.add(rdoc_class, name, '')
|
|
ie.store = @store
|
|
ie.line = line_no
|
|
ie.comment = comment
|
|
record_location(ie)
|
|
end
|
|
end
|
|
|
|
# Handle `include Foo, Bar`
|
|
|
|
def add_includes(names, line_no) # :nodoc:
|
|
add_includes_extends(names, RDoc::Include, line_no)
|
|
end
|
|
|
|
# Handle `extend Foo, Bar`
|
|
|
|
def add_extends(names, line_no) # :nodoc:
|
|
add_includes_extends(names, RDoc::Extend, line_no)
|
|
end
|
|
|
|
# Adds a method defined by `def` syntax
|
|
|
|
def add_method(name, receiver_name:, receiver_fallback_type:, visibility:, singleton:, params:, calls_super:, block_params:, tokens:, start_line:, end_line:)
|
|
receiver = receiver_name ? find_or_create_module_path(receiver_name, receiver_fallback_type) : @container
|
|
meth = RDoc::AnyMethod.new(nil, name)
|
|
if (comment = consecutive_comment(start_line))
|
|
handle_consecutive_comment_directive(@container, comment)
|
|
handle_consecutive_comment_directive(meth, comment)
|
|
|
|
comment.normalize
|
|
comment.extract_call_seq(meth)
|
|
meth.comment = comment
|
|
end
|
|
handle_modifier_directive(meth, start_line)
|
|
handle_modifier_directive(meth, end_line)
|
|
return unless should_document?(meth)
|
|
|
|
|
|
if meth.name == 'initialize' && !singleton
|
|
if meth.dont_rename_initialize
|
|
visibility = :protected
|
|
else
|
|
meth.name = 'new'
|
|
singleton = true
|
|
visibility = :public
|
|
end
|
|
end
|
|
|
|
internal_add_method(
|
|
receiver,
|
|
meth,
|
|
line_no: start_line,
|
|
visibility: visibility,
|
|
singleton: singleton,
|
|
params: params,
|
|
calls_super: calls_super,
|
|
block_params: block_params,
|
|
tokens: tokens
|
|
)
|
|
end
|
|
|
|
private def internal_add_method(container, meth, line_no:, visibility:, singleton:, params:, calls_super:, block_params:, tokens:) # :nodoc:
|
|
meth.name ||= meth.call_seq[/\A[^()\s]+/] if meth.call_seq
|
|
meth.name ||= 'unknown'
|
|
meth.store = @store
|
|
meth.line = line_no
|
|
meth.singleton = singleton
|
|
container.add_method(meth) # should add after setting singleton and before setting visibility
|
|
meth.visibility = visibility
|
|
meth.params ||= params
|
|
meth.calls_super = calls_super
|
|
meth.block_params ||= block_params if block_params
|
|
record_location(meth)
|
|
meth.start_collecting_tokens
|
|
tokens.each do |token|
|
|
meth.token_stream << token
|
|
end
|
|
end
|
|
|
|
# Find or create module or class from a given module name.
|
|
# If module or class does not exist, creates a module or a class according to `create_mode` argument.
|
|
|
|
def find_or_create_module_path(module_name, create_mode)
|
|
root_name, *path, name = module_name.split('::')
|
|
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
|
|
}
|
|
if root_name.empty?
|
|
mod = @top_level
|
|
else
|
|
@module_nesting.reverse_each do |nesting|
|
|
mod = nesting.find_module_named(root_name)
|
|
break if mod
|
|
end
|
|
return mod || add_module.call(@top_level, root_name, create_mode) unless name
|
|
mod ||= add_module.call(@top_level, root_name, :module)
|
|
end
|
|
path.each do |name|
|
|
mod = mod.find_module_named(name) || add_module.call(mod, name, :module)
|
|
end
|
|
mod.find_module_named(name) || add_module.call(mod, name, create_mode)
|
|
end
|
|
|
|
# Resolves constant path to a full path by searching module nesting
|
|
|
|
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|
|
|
mod = nesting.find_module_named(owner_name)
|
|
break if mod
|
|
end
|
|
mod ||= @top_level.find_module_named(owner_name)
|
|
[mod.full_name, path].compact.join('::') if mod
|
|
end
|
|
|
|
# Returns a pair of owner module and constant name from a given constant path.
|
|
# Creates owner module if it does not exist.
|
|
|
|
def find_or_create_constant_owner_name(constant_path)
|
|
const_path, colon, name = constant_path.rpartition('::')
|
|
if colon.empty? # class Foo
|
|
[@container, name]
|
|
elsif const_path.empty? # class ::Foo
|
|
[@top_level, name]
|
|
else # `class Foo::Bar` or `class ::Foo::Bar`
|
|
[find_or_create_module_path(const_path, :module), name]
|
|
end
|
|
end
|
|
|
|
# Adds a constant
|
|
|
|
def add_constant(constant_name, rhs_name, start_line, end_line)
|
|
comment = consecutive_comment(start_line)
|
|
handle_consecutive_comment_directive(@container, comment)
|
|
owner, name = find_or_create_constant_owner_name(constant_name)
|
|
constant = RDoc::Constant.new(name, rhs_name, comment)
|
|
constant.store = @store
|
|
constant.line = start_line
|
|
record_location(constant)
|
|
handle_modifier_directive(constant, start_line)
|
|
handle_modifier_directive(constant, end_line)
|
|
owner.add_constant(constant)
|
|
mod =
|
|
if rhs_name =~ /^::/
|
|
@store.find_class_or_module(rhs_name)
|
|
else
|
|
@container.find_module_named(rhs_name)
|
|
end
|
|
if mod && constant.document_self
|
|
a = @container.add_module_alias(mod, rhs_name, constant, @top_level)
|
|
a.store = @store
|
|
a.line = start_line
|
|
record_location(a)
|
|
end
|
|
end
|
|
|
|
# Adds module or class
|
|
|
|
def add_module_or_class(module_name, start_line, end_line, is_class: false, superclass_name: nil)
|
|
comment = consecutive_comment(start_line)
|
|
handle_consecutive_comment_directive(@container, comment)
|
|
return unless @container.document_children
|
|
|
|
owner, name = find_or_create_constant_owner_name(module_name)
|
|
if is_class
|
|
mod = owner.classes_hash[name] || owner.add_class(RDoc::NormalClass, name, superclass_name || '::Object')
|
|
|
|
# RDoc::NormalClass resolves superclass name despite of the lack of module nesting information.
|
|
# We need to fix it when RDoc::NormalClass resolved to a wrong constant name
|
|
if superclass_name
|
|
superclass_full_path = resolve_constant_path(superclass_name)
|
|
superclass = @store.find_class_or_module(superclass_full_path) if superclass_full_path
|
|
superclass_full_path ||= superclass_name
|
|
if superclass
|
|
mod.superclass = superclass
|
|
elsif mod.superclass.is_a?(String) && mod.superclass != superclass_full_path
|
|
mod.superclass = superclass_full_path
|
|
end
|
|
end
|
|
else
|
|
mod = owner.modules_hash[name] || owner.add_module(RDoc::NormalModule, name)
|
|
end
|
|
|
|
mod.store = @store
|
|
mod.line = start_line
|
|
record_location(mod)
|
|
handle_modifier_directive(mod, start_line)
|
|
handle_modifier_directive(mod, end_line)
|
|
mod.add_comment(comment, @top_level) if comment
|
|
mod
|
|
end
|
|
|
|
class RDocVisitor < Prism::Visitor # :nodoc:
|
|
def initialize(scanner, top_level, store)
|
|
@scanner = scanner
|
|
@top_level = top_level
|
|
@store = store
|
|
end
|
|
|
|
def visit_call_node(node)
|
|
@scanner.process_comments_until(node.location.start_line - 1)
|
|
if node.receiver.nil?
|
|
case node.name
|
|
when :attr
|
|
_visit_call_attr_reader_writer_accessor(node, 'R')
|
|
when :attr_reader
|
|
_visit_call_attr_reader_writer_accessor(node, 'R')
|
|
when :attr_writer
|
|
_visit_call_attr_reader_writer_accessor(node, 'W')
|
|
when :attr_accessor
|
|
_visit_call_attr_reader_writer_accessor(node, 'RW')
|
|
when :include
|
|
_visit_call_include(node)
|
|
when :extend
|
|
_visit_call_extend(node)
|
|
when :public
|
|
_visit_call_public_private_protected(node, :public) { super }
|
|
when :private
|
|
_visit_call_public_private_protected(node, :private) { super }
|
|
when :protected
|
|
_visit_call_public_private_protected(node, :protected) { super }
|
|
when :private_constant
|
|
_visit_call_private_constant(node)
|
|
when :public_constant
|
|
_visit_call_public_constant(node)
|
|
when :require
|
|
_visit_call_require(node)
|
|
when :alias_method
|
|
_visit_call_alias_method(node)
|
|
when :module_function
|
|
_visit_call_module_function(node) { super }
|
|
when :public_class_method
|
|
_visit_call_public_private_class_method(node, :public) { super }
|
|
when :private_class_method
|
|
_visit_call_public_private_class_method(node, :private) { super }
|
|
else
|
|
super
|
|
end
|
|
else
|
|
super
|
|
end
|
|
end
|
|
|
|
def visit_alias_method_node(node)
|
|
@scanner.process_comments_until(node.location.start_line - 1)
|
|
return unless node.old_name.is_a?(Prism::SymbolNode) && node.new_name.is_a?(Prism::SymbolNode)
|
|
@scanner.add_alias_method(node.old_name.value.to_s, node.new_name.value.to_s, node.location.start_line)
|
|
end
|
|
|
|
def visit_module_node(node)
|
|
@scanner.process_comments_until(node.location.start_line - 1)
|
|
module_name = constant_path_string(node.constant_path)
|
|
mod = @scanner.add_module_or_class(module_name, node.location.start_line, node.location.end_line) if module_name
|
|
if mod
|
|
@scanner.with_container(mod) do
|
|
super
|
|
@scanner.process_comments_until(node.location.end_line)
|
|
end
|
|
else
|
|
@scanner.skip_comments_until(node.location.end_line)
|
|
end
|
|
end
|
|
|
|
def visit_class_node(node)
|
|
@scanner.process_comments_until(node.location.start_line - 1)
|
|
superclass_name = constant_path_string(node.superclass) if node.superclass
|
|
class_name = constant_path_string(node.constant_path)
|
|
klass = @scanner.add_module_or_class(class_name, node.location.start_line, node.location.end_line, is_class: true, superclass_name: superclass_name) if class_name
|
|
if klass
|
|
@scanner.with_container(klass) do
|
|
super
|
|
@scanner.process_comments_until(node.location.end_line)
|
|
end
|
|
else
|
|
@scanner.skip_comments_until(node.location.end_line)
|
|
end
|
|
end
|
|
|
|
def visit_singleton_class_node(node)
|
|
@scanner.process_comments_until(node.location.start_line - 1)
|
|
|
|
expression = node.expression
|
|
expression = expression.body.body.first if expression.is_a?(Prism::ParenthesesNode) && expression.body&.body&.size == 1
|
|
|
|
case expression
|
|
when Prism::ConstantWriteNode
|
|
# Accept `class << (NameErrorCheckers = Object.new)` as a module which is not actually a module
|
|
mod = @scanner.container.add_module(RDoc::NormalModule, expression.name.to_s)
|
|
when Prism::ConstantPathNode, Prism::ConstantReadNode
|
|
expression_name = constant_path_string(expression)
|
|
# If a constant_path does not exist, RDoc creates a module
|
|
mod = @scanner.find_or_create_module_path(expression_name, :module) if expression_name
|
|
when Prism::SelfNode
|
|
mod = @scanner.container if @scanner.container != @top_level
|
|
end
|
|
if mod
|
|
@scanner.with_container(mod, singleton: true) do
|
|
super
|
|
@scanner.process_comments_until(node.location.end_line)
|
|
end
|
|
else
|
|
@scanner.skip_comments_until(node.location.end_line)
|
|
end
|
|
end
|
|
|
|
def visit_def_node(node)
|
|
start_line = node.location.start_line
|
|
end_line = node.location.end_line
|
|
@scanner.process_comments_until(start_line - 1)
|
|
|
|
case node.receiver
|
|
when Prism::NilNode, Prism::TrueNode, Prism::FalseNode
|
|
visibility = :public
|
|
singleton = false
|
|
receiver_name =
|
|
case node.receiver
|
|
when Prism::NilNode
|
|
'NilClass'
|
|
when Prism::TrueNode
|
|
'TrueClass'
|
|
when Prism::FalseNode
|
|
'FalseClass'
|
|
end
|
|
receiver_fallback_type = :class
|
|
when Prism::SelfNode
|
|
# singleton method of a singleton class is not documentable
|
|
return if @scanner.singleton
|
|
visibility = :public
|
|
singleton = true
|
|
when Prism::ConstantReadNode, Prism::ConstantPathNode
|
|
visibility = :public
|
|
singleton = true
|
|
receiver_name = constant_path_string(node.receiver)
|
|
receiver_fallback_type = :module
|
|
return unless receiver_name
|
|
when nil
|
|
visibility = @scanner.visibility
|
|
singleton = @scanner.singleton
|
|
else
|
|
# `def (unknown expression).method_name` is not documentable
|
|
return
|
|
end
|
|
name = node.name.to_s
|
|
params, block_params, calls_super = MethodSignatureVisitor.scan_signature(node)
|
|
tokens = @scanner.visible_tokens_from_location(node.location)
|
|
|
|
@scanner.add_method(
|
|
name,
|
|
receiver_name: receiver_name,
|
|
receiver_fallback_type: receiver_fallback_type,
|
|
visibility: visibility,
|
|
singleton: singleton,
|
|
params: params,
|
|
block_params: block_params,
|
|
calls_super: calls_super,
|
|
tokens: tokens,
|
|
start_line: start_line,
|
|
end_line: end_line
|
|
)
|
|
ensure
|
|
@scanner.skip_comments_until(end_line)
|
|
end
|
|
|
|
def visit_constant_path_write_node(node)
|
|
@scanner.process_comments_until(node.location.start_line - 1)
|
|
path = constant_path_string(node.target)
|
|
return unless path
|
|
|
|
@scanner.add_constant(
|
|
path,
|
|
constant_path_string(node.value) || node.value.slice,
|
|
node.location.start_line,
|
|
node.location.end_line
|
|
)
|
|
@scanner.skip_comments_until(node.location.end_line)
|
|
# Do not traverse rhs not to document `A::B = Struct.new{def undocumentable_method; end}`
|
|
end
|
|
|
|
def visit_constant_write_node(node)
|
|
@scanner.process_comments_until(node.location.start_line - 1)
|
|
@scanner.add_constant(
|
|
node.name.to_s,
|
|
constant_path_string(node.value) || node.value.slice,
|
|
node.location.start_line,
|
|
node.location.end_line
|
|
)
|
|
@scanner.skip_comments_until(node.location.end_line)
|
|
# Do not traverse rhs not to document `A = Struct.new{def undocumentable_method; end}`
|
|
end
|
|
|
|
private
|
|
|
|
def constant_arguments_names(call_node)
|
|
return unless call_node.arguments
|
|
names = call_node.arguments.arguments.map { |arg| constant_path_string(arg) }
|
|
names.all? ? names : nil
|
|
end
|
|
|
|
def symbol_arguments(call_node)
|
|
arguments_node = call_node.arguments
|
|
return unless arguments_node && arguments_node.arguments.all? { |arg| arg.is_a?(Prism::SymbolNode)}
|
|
arguments_node.arguments.map { |arg| arg.value.to_sym }
|
|
end
|
|
|
|
def visibility_method_arguments(call_node, singleton:)
|
|
arguments_node = call_node.arguments
|
|
return unless arguments_node
|
|
symbols = symbol_arguments(call_node)
|
|
if symbols
|
|
# module_function :foo, :bar
|
|
return symbols.map(&:to_s)
|
|
else
|
|
return unless arguments_node.arguments.size == 1
|
|
arg = arguments_node.arguments.first
|
|
return unless arg.is_a?(Prism::DefNode)
|
|
|
|
if singleton
|
|
# `private_class_method def foo; end` `private_class_method def not_self.foo; end` should be ignored
|
|
return unless arg.receiver.is_a?(Prism::SelfNode)
|
|
else
|
|
# `module_function def something.foo` should be ignored
|
|
return if arg.receiver
|
|
end
|
|
# `module_function def foo; end` or `private_class_method def self.foo; end`
|
|
[arg.name.to_s]
|
|
end
|
|
end
|
|
|
|
def constant_path_string(node)
|
|
case node
|
|
when Prism::ConstantReadNode
|
|
node.name.to_s
|
|
when Prism::ConstantPathNode
|
|
parent_name = node.parent ? constant_path_string(node.parent) : ''
|
|
"#{parent_name}::#{node.name}" if parent_name
|
|
end
|
|
end
|
|
|
|
def _visit_call_require(call_node)
|
|
return unless call_node.arguments&.arguments&.size == 1
|
|
arg = call_node.arguments.arguments.first
|
|
return unless arg.is_a?(Prism::StringNode)
|
|
@scanner.container.add_require(RDoc::Require.new(arg.unescaped, nil))
|
|
end
|
|
|
|
def _visit_call_module_function(call_node)
|
|
yield
|
|
return if @scanner.singleton
|
|
names = visibility_method_arguments(call_node, singleton: false)&.map(&:to_s)
|
|
@scanner.change_method_to_module_function(names) if names
|
|
end
|
|
|
|
def _visit_call_public_private_class_method(call_node, visibility)
|
|
yield
|
|
return if @scanner.singleton
|
|
names = visibility_method_arguments(call_node, singleton: true)
|
|
@scanner.change_method_visibility(names, visibility, singleton: true) if names
|
|
end
|
|
|
|
def _visit_call_public_private_protected(call_node, visibility)
|
|
arguments_node = call_node.arguments
|
|
if arguments_node.nil? # `public` `private`
|
|
@scanner.visibility = visibility
|
|
else # `public :foo, :bar`, `private def foo; end`
|
|
yield
|
|
names = visibility_method_arguments(call_node, singleton: @scanner.singleton)
|
|
@scanner.change_method_visibility(names, visibility) if names
|
|
end
|
|
end
|
|
|
|
def _visit_call_alias_method(call_node)
|
|
new_name, old_name, *rest = symbol_arguments(call_node)
|
|
return unless old_name && new_name && rest.empty?
|
|
@scanner.add_alias_method(old_name.to_s, new_name.to_s, call_node.location.start_line)
|
|
end
|
|
|
|
def _visit_call_include(call_node)
|
|
names = constant_arguments_names(call_node)
|
|
line_no = call_node.location.start_line
|
|
return unless names
|
|
|
|
if @scanner.singleton
|
|
@scanner.add_extends(names, line_no)
|
|
else
|
|
@scanner.add_includes(names, line_no)
|
|
end
|
|
end
|
|
|
|
def _visit_call_extend(call_node)
|
|
names = constant_arguments_names(call_node)
|
|
@scanner.add_extends(names, call_node.location.start_line) if names && !@scanner.singleton
|
|
end
|
|
|
|
def _visit_call_public_constant(call_node)
|
|
return if @scanner.singleton
|
|
names = symbol_arguments(call_node)
|
|
@scanner.container.set_constant_visibility_for(names.map(&:to_s), :public) if names
|
|
end
|
|
|
|
def _visit_call_private_constant(call_node)
|
|
return if @scanner.singleton
|
|
names = symbol_arguments(call_node)
|
|
@scanner.container.set_constant_visibility_for(names.map(&:to_s), :private) if names
|
|
end
|
|
|
|
def _visit_call_attr_reader_writer_accessor(call_node, rw)
|
|
names = symbol_arguments(call_node)
|
|
@scanner.add_attributes(names.map(&:to_s), rw, call_node.location.start_line) if names
|
|
end
|
|
class MethodSignatureVisitor < Prism::Visitor # :nodoc:
|
|
class << self
|
|
def scan_signature(def_node)
|
|
visitor = new
|
|
def_node.body&.accept(visitor)
|
|
params = "(#{def_node.parameters&.slice})"
|
|
block_params = visitor.yields.first
|
|
[params, block_params, visitor.calls_super]
|
|
end
|
|
end
|
|
|
|
attr_reader :params, :yields, :calls_super
|
|
|
|
def initialize
|
|
@params = nil
|
|
@calls_super = false
|
|
@yields = []
|
|
end
|
|
|
|
def visit_def_node(node)
|
|
# stop traverse inside nested def
|
|
end
|
|
|
|
def visit_yield_node(node)
|
|
@yields << (node.arguments&.slice || '')
|
|
end
|
|
|
|
def visit_super_node(node)
|
|
@calls_super = true
|
|
super
|
|
end
|
|
|
|
def visit_forwarding_super_node(node)
|
|
@calls_super = true
|
|
end
|
|
end
|
|
end
|
|
end
|