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 lib/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 lib/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 lib/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 lib/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 lib/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 lib/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 lib/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 lib/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 lib/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 lib/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 lib/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 lib/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 lib/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 lib/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 lib/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 lib/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 lib/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 lib/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 lib/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 lib/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