# 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 && !@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