class RDoc::Parser::PrismRuby - rdoc: Ruby Standard Library Documentation

# class RDoc::Parser::PrismRuby

Parse and collect document from Ruby source code. `RDoc::Parser::PrismRuby` is compatible with `RDoc::Parser::Ruby` and aims to replace it.

### Attributes

container[R]
singleton[R]
visibility[RW]

### Public Class Methods

new(top_level, file_name, content, options, stats) click to toggle source
Calls superclass method `RDoc::Parser::new`

    # File rdoc/parser/prism_ruby.rb, line 21
    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

### Public Instance Methods

add_alias_method(old_name, new_name, line_no) click to toggle source

Handles ‘alias foo bar` and `alias_method :foo, :bar`

    # File rdoc/parser/prism_ruby.rb, line 434
    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

add_attributes(names, rw, line_no) click to toggle source

Handles ‘attr :a, :b`, `attr_reader :a, :b`, `attr_writer :a, :b` and `attr_accessor :a, :b`

    # File rdoc/parser/prism_ruby.rb, line 452
    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

add_constant(constant_name, rhs_name, start_line, end_line) click to toggle source

Adds a constant

    # File rdoc/parser/prism_ruby.rb, line 611
    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

add_method(name, receiver_name:, receiver_fallback_type:, visibility:, singleton:, params:, calls_super:, block_params:, tokens:, start_line:, end_line:) click to toggle source

Adds a method defined by ‘def` syntax

    # File rdoc/parser/prism_ruby.rb, line 495
    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

add_module_or_class(module_name, start_line, end_line, is_class: false, superclass_name: nil) click to toggle source

Adds module or class

    # File rdoc/parser/prism_ruby.rb, line 638
    def add_module_or_class(module_name, start_line, end_line, is_class: false, superclass_name: nil)
      comment = consecutive_comment(start_line)
      handle_consecutive_comment_directive(@container, comment)
      return unless @container.document_children

      owner, name = find_or_create_constant_owner_name(module_name)
      if is_class
        mod = owner.classes_hash[name] || owner.add_class(RDoc::NormalClass, name, superclass_name || '::Object')

        # RDoc::NormalClass resolves superclass name despite of the lack of module nesting information.
        # We need to fix it when RDoc::NormalClass resolved to a wrong constant name
        if superclass_name
          superclass_full_path = resolve_constant_path(superclass_name)
          superclass = @store.find_class_or_module(superclass_full_path) if superclass_full_path
          superclass_full_path ||= superclass_name
          if superclass
            mod.superclass = superclass
          elsif mod.superclass.is_a?(String) && mod.superclass != superclass_full_path
            mod.superclass = superclass_full_path
          end
        end
      else
        mod = owner.modules_hash[name] || owner.add_module(RDoc::NormalModule, name)
      end

      mod.store = @store
      mod.line = start_line
      record_location(mod)
      handle_modifier_directive(mod, start_line)
      handle_modifier_directive(mod, end_line)
      mod.add_comment(comment, @top_level) if comment
      mod
    end

change_method_to_module_function(names) click to toggle source

Handles ‘module_function :foo, :bar`

    # File rdoc/parser/prism_ruby.rb, line 412
    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

change_method_visibility(names, visibility, singleton: @singleton) click to toggle source

Handles ‘public :foo, :bar` `private :foo, :bar` and `protected :foo, :bar`

    # File rdoc/parser/prism_ruby.rb, line 388
    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

consecutive_comment(line_no) click to toggle source

Returns consecutive comment linked to the given line number

    # File rdoc/parser/prism_ruby.rb, line 353
    def consecutive_comment(line_no)
      if @unprocessed_comments.first&.first == line_no
        @unprocessed_comments.shift.last
      end
    end

find_or_create_constant_owner_name(constant_path) click to toggle source

Returns a pair of owner module and constant name from a given constant path. Creates owner module if it does not exist.

    # File rdoc/parser/prism_ruby.rb, line 598
    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

find_or_create_module_path(module_name, create_mode) click to toggle source

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.

    # File rdoc/parser/prism_ruby.rb, line 555
    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

handle_meta_method_comment(comment, node) click to toggle source

Handles meta method comments

    # File rdoc/parser/prism_ruby.rb, line 243
    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

parse_comment_tomdoc(container, comment, line_no, start_line) click to toggle source

Creates an RDoc::Method on `container` from `comment` if there is a Signature section in the comment

    # File rdoc/parser/prism_ruby.rb, line 184
    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

prepare_comments(comments) click to toggle source

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

    # File rdoc/parser/prism_ruby.rb, line 132
    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

process_comments_until(line_no_until) click to toggle source

Processes consecutive comments that were not linked to any documentable code until the given line number

    # File rdoc/parser/prism_ruby.rb, line 335
    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

resolve_constant_path(constant_path) click to toggle source

Resolves constant path to a full path by searching module nesting

    # File rdoc/parser/prism_ruby.rb, line 583
    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

scan() click to toggle source

Scans this Ruby file for Ruby constructs

    # File rdoc/parser/prism_ruby.rb, line 78
    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

skip_comments_until(line_no_until) click to toggle source

Skips all undocumentable consecutive comments until the given line number. Undocumentable comments are comments written inside ‘def` or inside undocumentable class/module

    # File rdoc/parser/prism_ruby.rb, line 345
    def skip_comments_until(line_no_until)
      while !@unprocessed_comments.empty? && @unprocessed_comments.first[0] <= line_no_until
        @unprocessed_comments.shift
      end
    end

visible_tokens_from_location(location) click to toggle source

Returns tokens from the given location

    # File rdoc/parser/prism_ruby.rb, line 375
    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

with_container(container, singleton: false) { |container| ... } click to toggle source

Dive into another container

    # File rdoc/parser/prism_ruby.rb, line 42
    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