diff options
Diffstat (limited to 'lib/prism/parse_result/comments.rb')
-rw-r--r-- | lib/prism/parse_result/comments.rb | 187 |
1 files changed, 187 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..22c4148b2c --- /dev/null +++ b/lib/prism/parse_result/comments.rb @@ -0,0 +1,187 @@ +# 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 |