summaryrefslogtreecommitdiff
path: root/lib/rdoc/parser/prism_ruby.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/rdoc/parser/prism_ruby.rb')
-rw-r--r--lib/rdoc/parser/prism_ruby.rb1028
1 files changed, 0 insertions, 1028 deletions
diff --git a/lib/rdoc/parser/prism_ruby.rb b/lib/rdoc/parser/prism_ruby.rb
deleted file mode 100644
index 06ef4abdb3..0000000000
--- a/lib/rdoc/parser/prism_ruby.rb
+++ /dev/null
@@ -1,1028 +0,0 @@
-# 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
- # 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
- end
- # add_class should be done after resolving superclass
- mod = owner.classes_hash[name] || owner.add_class(RDoc::NormalClass, name, superclass_name || '::Object')
- if 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 && [email protected]
- 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