summaryrefslogtreecommitdiff
path: root/lib/prism/parse_result/comments.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/prism/parse_result/comments.rb')
-rw-r--r--lib/prism/parse_result/comments.rb219
1 files changed, 219 insertions, 0 deletions
diff --git a/lib/prism/parse_result/comments.rb b/lib/prism/parse_result/comments.rb
new file mode 100644
index 0000000000..df80792d39
--- /dev/null
+++ b/lib/prism/parse_result/comments.rb
@@ -0,0 +1,219 @@
+# frozen_string_literal: true
+# :markup: markdown
+#--
+# rbs_inline: enabled
+
+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
+ # @rbs!
+ # # An internal interface for a target that comments can be attached
+ # # to. This is either going to be a NodeTarget or a CommentTarget.
+ # interface _CommentTarget
+ # def start_offset: () -> Integer
+ # def end_offset: () -> Integer
+ # def encloses?: (Comment) -> bool
+ # def leading_comment: (Comment) -> void
+ # def trailing_comment: (Comment) -> void
+ # end
+
+ # A target for attaching comments that is based on a specific node's
+ # location.
+ class NodeTarget # :nodoc:
+ attr_reader :node #: node
+
+ #: (node node) -> void
+ def initialize(node)
+ @node = node
+ end
+
+ #: () -> Integer
+ def start_offset
+ node.start_offset
+ end
+
+ #: () -> Integer
+ def end_offset
+ node.end_offset
+ end
+
+ #: (Comment comment) -> bool
+ def encloses?(comment)
+ start_offset <= comment.location.start_offset &&
+ comment.location.end_offset <= end_offset
+ end
+
+ #: (Comment comment) -> void
+ def leading_comment(comment)
+ node.location.leading_comment(comment)
+ end
+
+ #: (Comment comment) -> void
+ 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 #: Location
+
+ #: (Location location) -> void
+ def initialize(location)
+ @location = location
+ end
+
+ #: () -> Integer
+ def start_offset
+ location.start_offset
+ end
+
+ #: () -> Integer
+ def end_offset
+ location.end_offset
+ end
+
+ #: (Comment comment) -> bool
+ def encloses?(comment)
+ false
+ end
+
+ #: (Comment comment) -> void
+ def leading_comment(comment)
+ location.leading_comment(comment)
+ end
+
+ #: (Comment comment) -> void
+ def trailing_comment(comment)
+ location.trailing_comment(comment)
+ end
+ end
+
+ # The parse result that we are attaching comments to.
+ attr_reader :parse_result #: ParseResult
+
+ # Create a new Comments object that will attach comments to the given
+ # parse result.
+ #--
+ #: (ParseResult parse_result) -> void
+ def initialize(parse_result)
+ @parse_result = parse_result
+ end
+
+ # Attach the comments to their respective locations in the tree by
+ # mutating the parse result.
+ #--
+ #: () -> void
+ 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.
+ #--
+ #: (node node, Comment comment) -> [_CommentTarget?, _CommentTarget?, _CommentTarget?]
+ def nearest_targets(node, comment)
+ comment_start = comment.location.start_offset
+ comment_end = comment.location.end_offset
+
+ targets = [] #: Array[_CommentTarget]
+ 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 #: _CommentTarget?
+ following = nil #: _CommentTarget?
+
+ 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