class Parser::Source::Comment::Associator

A processor which associates AST nodes with comments based on their location in source code. It may be used, for example, to implement rdoc-style processing.

@example

require 'parser/current'

ast, comments = Parser::CurrentRuby.parse_with_comments(<<-CODE)
# Class stuff
class Foo
  # Attr stuff
  # @see bar
  attr_accessor :foo
end
CODE

p Parser::Source::Comment.associate(ast, comments)
# => {
#   (class (const nil :Foo) ...) =>
#     [#<Parser::Source::Comment (string):1:1 "# Class stuff">],
#   (send nil :attr_accessor (sym :foo)) =>
#     [#<Parser::Source::Comment (string):3:3 "# Attr stuff">,
#      #<Parser::Source::Comment (string):4:3 "# @see bar">]
# }

@see {associate}

@!attribute skip_directives

Skip file processing directives disguised as comments.
Namely:

  * Shebang line,
  * Magic encoding comment.

@return [Boolean]

@api public

Constants

MAGIC_COMMENT_RE
POSTFIX_TYPES

Attributes

skip_directives[RW]

Public Class Methods

new(ast, comments) click to toggle source

@param [Parser::AST::Node] ast @param [Array<Parser::Source::Comment>] comments

# File lib/parser/source/comment/associator.rb, line 51
def initialize(ast, comments)
  @ast         = ast
  @comments    = comments

  @skip_directives = true
end

Public Instance Methods

associate() click to toggle source

Compute a mapping between AST nodes and comments. Comment is associated with the node, if it is one of the following types:

  • preceding comment, it ends before the node start

  • sparse comment, it is located inside the node, after all child nodes

  • decorating comment, it starts at the same line, where the node ends

This rule is unambiguous and produces the result one could reasonably expect; for example, this code

# foo
hoge # bar
  + fuga

will result in the following association:

{
  (send (lvar :hoge) :+ (lvar :fuga)) =>
    [#<Parser::Source::Comment (string):2:1 "# foo">],
  (lvar :fuga) =>
    [#<Parser::Source::Comment (string):3:8 "# bar">]
}

Note that comments after the end of the end of a passed tree range are ignored (except root decorating comment).

Note that {associate} produces unexpected result for nodes which are equal but have distinct locations; comments for these nodes are merged. You may prefer using {associate_by_identity} or {associate_locations}.

@return [Hash<Parser::AST::Node, Array<Parser::Source::Comment>>] @deprecated Use {associate_locations}.

# File lib/parser/source/comment/associator.rb, line 92
def associate
  @map_using = :eql
  do_associate
end
associate_by_identity() click to toggle source

Same as {associate}, but uses `node.loc` instead of `node` as the hash key, thus producing an unambiguous result even in presence of equal nodes.

@return [Hash<Parser::Source::Map, Array<Parser::Source::Comment>>]

# File lib/parser/source/comment/associator.rb, line 115
def associate_by_identity
  @map_using = :identity
  do_associate
end
associate_locations() click to toggle source

Same as {associate}, but compares by identity, thus producing an unambiguous result even in presence of equal nodes.

@return [Hash<Parser::Source::Node, Array<Parser::Source::Comment>>]

# File lib/parser/source/comment/associator.rb, line 103
def associate_locations
  @map_using = :location
  do_associate
end

Private Instance Methods

advance_comment() click to toggle source
# File lib/parser/source/comment/associator.rb, line 182
def advance_comment
  @comment_num += 1
  @current_comment = @comments[@comment_num]
end
advance_through_directives() click to toggle source
# File lib/parser/source/comment/associator.rb, line 214
def advance_through_directives
  # Skip shebang.
  if @current_comment && @current_comment.text.start_with?('#!'.freeze)
    advance_comment
  end

  # Skip magic comments.
  if @current_comment && @current_comment.text =~ MAGIC_COMMENT_RE
    advance_comment
  end

  # Skip encoding line.
  if @current_comment && @current_comment.text =~ Buffer::ENCODING_RE
    advance_comment
  end
end
associate_and_advance_comment(node) click to toggle source
# File lib/parser/source/comment/associator.rb, line 206
def associate_and_advance_comment(node)
  key = @map_using == :location ? node.location : node
  @mapping[key] << @current_comment
  advance_comment
end
children_in_source_order(node) click to toggle source
# File lib/parser/source/comment/associator.rb, line 123
def children_in_source_order(node)
  if POSTFIX_TYPES.include?(node.type)
    # All these types have either nodes with expressions, or `nil`
    # so a compact will do, but they need to be sorted.
    node.children.compact.sort_by { |child| child.loc.expression.begin_pos }
  else
    node.children.select do |child|
      child.is_a?(AST::Node) && child.loc && child.loc.expression
    end
  end
end
current_comment_before?(node) click to toggle source
# File lib/parser/source/comment/associator.rb, line 187
def current_comment_before?(node)
  return false if !@current_comment
  comment_loc = @current_comment.location.expression
  node_loc = node.location.expression
  comment_loc.end_pos <= node_loc.begin_pos
end
current_comment_before_end?(node) click to toggle source
# File lib/parser/source/comment/associator.rb, line 194
def current_comment_before_end?(node)
  return false if !@current_comment
  comment_loc = @current_comment.location.expression
  node_loc = node.location.expression
  comment_loc.end_pos <= node_loc.end_pos
end
current_comment_decorates?(node) click to toggle source
# File lib/parser/source/comment/associator.rb, line 201
def current_comment_decorates?(node)
  return false if !@current_comment
  @current_comment.location.line == node.location.last_line
end
do_associate() click to toggle source
# File lib/parser/source/comment/associator.rb, line 135
def do_associate
  @mapping     = Hash.new { |h, k| h[k] = [] }
  @mapping.compare_by_identity if @map_using == :identity
  @comment_num = -1
  advance_comment

  advance_through_directives if @skip_directives

  visit(@ast) if @ast

  @mapping
end
process_leading_comments(node) click to toggle source
# File lib/parser/source/comment/associator.rb, line 166
def process_leading_comments(node)
  return if node.type == :begin
  while current_comment_before?(node) # preceding comment
    associate_and_advance_comment(node)
  end
end
process_trailing_comments(node) click to toggle source
# File lib/parser/source/comment/associator.rb, line 173
def process_trailing_comments(node)
  while current_comment_before_end?(node)
    associate_and_advance_comment(node) # sparse comment
  end
  while current_comment_decorates?(node)
    associate_and_advance_comment(node) # decorating comment
  end
end
visit(node) click to toggle source
# File lib/parser/source/comment/associator.rb, line 148
def visit(node)
  process_leading_comments(node)

  return unless @current_comment

  # If the next comment is beyond the last line of this node, we don't
  # need to iterate over its subnodes
  # (Unless this node is a heredoc... there could be a comment in its body,
  # inside an interpolation)
  node_loc = node.location
  if @current_comment.location.line <= node_loc.last_line ||
     node_loc.is_a?(Map::Heredoc)
    children_in_source_order(node).each { |child| visit(child) }

    process_trailing_comments(node)
  end
end