mirror of
https://github.com/ruby/ruby.git
synced 2025-08-25 05:55:46 +02:00

Having the @newline instance variable in every node adds up, and
since it is so rarely used, we only want to add it when necessary.
Moving this into an autoloaded file and moving the instance variable
out of the default initializers reduces allocated memory because the
nodes are now smaller and some fit into the compact list. On my
machine, I'm seeing about an 8% drop.
eea92c07d2
187 lines
5.9 KiB
Ruby
187 lines
5.9 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module Prism
|
|
class ParseResult < Result
|
|
# When we've parsed the source, we have both the syntax tree and the list of
|
|
# comments that we found in the source. This class is responsible for
|
|
# walking the tree and finding the nearest location to attach each comment.
|
|
#
|
|
# It does this by first finding the nearest locations to each comment.
|
|
# Locations can either come from nodes directly or from location fields on
|
|
# nodes. For example, a `ClassNode` has an overall location encompassing the
|
|
# entire class, but it also has a location for the `class` keyword.
|
|
#
|
|
# Once the nearest locations are found, it determines which one to attach
|
|
# to. If it's a trailing comment (a comment on the same line as other source
|
|
# code), it will favor attaching to the nearest location that occurs before
|
|
# the comment. Otherwise it will favor attaching to the nearest location
|
|
# that is after the comment.
|
|
class Comments
|
|
# A target for attaching comments that is based on a specific node's
|
|
# location.
|
|
class NodeTarget # :nodoc:
|
|
attr_reader :node
|
|
|
|
def initialize(node)
|
|
@node = node
|
|
end
|
|
|
|
def start_offset
|
|
node.start_offset
|
|
end
|
|
|
|
def end_offset
|
|
node.end_offset
|
|
end
|
|
|
|
def encloses?(comment)
|
|
start_offset <= comment.location.start_offset &&
|
|
comment.location.end_offset <= end_offset
|
|
end
|
|
|
|
def leading_comment(comment)
|
|
node.location.leading_comment(comment)
|
|
end
|
|
|
|
def trailing_comment(comment)
|
|
node.location.trailing_comment(comment)
|
|
end
|
|
end
|
|
|
|
# A target for attaching comments that is based on a location field on a
|
|
# node. For example, the `end` token of a ClassNode.
|
|
class LocationTarget # :nodoc:
|
|
attr_reader :location
|
|
|
|
def initialize(location)
|
|
@location = location
|
|
end
|
|
|
|
def start_offset
|
|
location.start_offset
|
|
end
|
|
|
|
def end_offset
|
|
location.end_offset
|
|
end
|
|
|
|
def encloses?(comment)
|
|
false
|
|
end
|
|
|
|
def leading_comment(comment)
|
|
location.leading_comment(comment)
|
|
end
|
|
|
|
def trailing_comment(comment)
|
|
location.trailing_comment(comment)
|
|
end
|
|
end
|
|
|
|
# The parse result that we are attaching comments to.
|
|
attr_reader :parse_result
|
|
|
|
# Create a new Comments object that will attach comments to the given
|
|
# parse result.
|
|
def initialize(parse_result)
|
|
@parse_result = parse_result
|
|
end
|
|
|
|
# Attach the comments to their respective locations in the tree by
|
|
# mutating the parse result.
|
|
def attach!
|
|
parse_result.comments.each do |comment|
|
|
preceding, enclosing, following = nearest_targets(parse_result.value, comment)
|
|
|
|
if comment.trailing?
|
|
if preceding
|
|
preceding.trailing_comment(comment)
|
|
else
|
|
(following || enclosing || NodeTarget.new(parse_result.value)).leading_comment(comment)
|
|
end
|
|
else
|
|
# If a comment exists on its own line, prefer a leading comment.
|
|
if following
|
|
following.leading_comment(comment)
|
|
elsif preceding
|
|
preceding.trailing_comment(comment)
|
|
else
|
|
(enclosing || NodeTarget.new(parse_result.value)).leading_comment(comment)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
# Responsible for finding the nearest targets to the given comment within
|
|
# the context of the given encapsulating node.
|
|
def nearest_targets(node, comment)
|
|
comment_start = comment.location.start_offset
|
|
comment_end = comment.location.end_offset
|
|
|
|
targets = [] #: Array[_Target]
|
|
node.comment_targets.map do |value|
|
|
case value
|
|
when StatementsNode
|
|
targets.concat(value.body.map { |node| NodeTarget.new(node) })
|
|
when Node
|
|
targets << NodeTarget.new(value)
|
|
when Location
|
|
targets << LocationTarget.new(value)
|
|
end
|
|
end
|
|
|
|
targets.sort_by!(&:start_offset)
|
|
preceding = nil #: _Target?
|
|
following = nil #: _Target?
|
|
|
|
left = 0
|
|
right = targets.length
|
|
|
|
# This is a custom binary search that finds the nearest nodes to the
|
|
# given comment. When it finds a node that completely encapsulates the
|
|
# comment, it recurses downward into the tree.
|
|
while left < right
|
|
middle = (left + right) / 2
|
|
target = targets[middle]
|
|
|
|
target_start = target.start_offset
|
|
target_end = target.end_offset
|
|
|
|
if target.encloses?(comment)
|
|
# @type var target: NodeTarget
|
|
# The comment is completely contained by this target. Abandon the
|
|
# binary search at this level.
|
|
return nearest_targets(target.node, comment)
|
|
end
|
|
|
|
if target_end <= comment_start
|
|
# This target falls completely before the comment. Because we will
|
|
# never consider this target or any targets before it again, this
|
|
# target must be the closest preceding target we have encountered so
|
|
# far.
|
|
preceding = target
|
|
left = middle + 1
|
|
next
|
|
end
|
|
|
|
if comment_end <= target_start
|
|
# This target falls completely after the comment. Because we will
|
|
# never consider this target or any targets after it again, this
|
|
# target must be the closest following target we have encountered so
|
|
# far.
|
|
following = target
|
|
right = middle
|
|
next
|
|
end
|
|
|
|
# This should only happen if there is a bug in this parser.
|
|
raise "Comment location overlaps with a target location"
|
|
end
|
|
|
|
[preceding, NodeTarget.new(node), following]
|
|
end
|
|
end
|
|
end
|
|
end
|