diff options
Diffstat (limited to 'lib/rdoc/parser/prism_ruby.rb')
-rw-r--r-- | lib/rdoc/parser/prism_ruby.rb | 1028 |
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 |