[ruby/yarp] Move parse result mutations into their own files
https://github.com/ruby/yarp/commit/3be8272fa2
This commit is contained in:
parent
7fc4db35ee
commit
5d73c0f3df
190
lib/yarp.rb
190
lib/yarp.rb
@ -248,193 +248,6 @@ module YARP
|
|||||||
def failure?
|
def failure?
|
||||||
!success?
|
!success?
|
||||||
end
|
end
|
||||||
|
|
||||||
# CommentAttacher is a utility class to attach comments to locations in the AST
|
|
||||||
class CommentAttacher
|
|
||||||
attr_reader :parse_result
|
|
||||||
|
|
||||||
def initialize(parse_result)
|
|
||||||
@parse_result = parse_result
|
|
||||||
end
|
|
||||||
|
|
||||||
def attach!
|
|
||||||
parse_result.comments.each do |comment|
|
|
||||||
preceding, enclosing, following = nearest_targets(parse_result.value, comment)
|
|
||||||
target =
|
|
||||||
if comment.trailing?
|
|
||||||
preceding || following || enclosing || NodeTarget.new(parse_result.value)
|
|
||||||
else
|
|
||||||
# If a comment exists on its own line, prefer a leading comment.
|
|
||||||
following || preceding || enclosing || NodeTarget.new(parse_result.value)
|
|
||||||
end
|
|
||||||
|
|
||||||
target << comment
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# A target for attaching comments that is based on a specific node
|
|
||||||
class NodeTarget
|
|
||||||
attr_reader :node
|
|
||||||
|
|
||||||
def initialize(node)
|
|
||||||
@node = node
|
|
||||||
end
|
|
||||||
|
|
||||||
def start_offset
|
|
||||||
node.location.start_offset
|
|
||||||
end
|
|
||||||
|
|
||||||
def end_offset
|
|
||||||
node.location.end_offset
|
|
||||||
end
|
|
||||||
|
|
||||||
def encloses?(comment)
|
|
||||||
start_offset <= comment.location.start_offset && comment.location.end_offset <= end_offset
|
|
||||||
end
|
|
||||||
|
|
||||||
def <<(comment)
|
|
||||||
node.location.comments << comment
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# A target for attaching comments that is based on a location, which could be a part of a node. For example, the
|
|
||||||
# `end` token of a ClassNode
|
|
||||||
class LocationTarget
|
|
||||||
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 <<(comment)
|
|
||||||
location.comments << comment
|
|
||||||
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 = []
|
|
||||||
node.deconstruct_keys(nil).each do |key, value|
|
|
||||||
next if key == :location
|
|
||||||
|
|
||||||
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)
|
|
||||||
when Array
|
|
||||||
targets.concat(value.map { |node| NodeTarget.new(node) }) if value.first.is_a?(Node)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
targets.sort_by!(&:start_offset)
|
|
||||||
preceding = nil
|
|
||||||
following = nil
|
|
||||||
|
|
||||||
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 recursed 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)
|
|
||||||
# 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 target location"
|
|
||||||
end
|
|
||||||
|
|
||||||
[preceding, NodeTarget.new(node), following]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Attach the list of comments to their respective locations in the AST
|
|
||||||
def attach_comments!
|
|
||||||
CommentAttacher.new(self).attach!
|
|
||||||
end
|
|
||||||
|
|
||||||
# Keep in sync with Java MarkNewlinesVisitor
|
|
||||||
class MarkNewlinesVisitor < YARP::Visitor
|
|
||||||
def initialize(newline_marked)
|
|
||||||
@newline_marked = newline_marked
|
|
||||||
end
|
|
||||||
|
|
||||||
def visit_block_node(node)
|
|
||||||
old_newline_marked = @newline_marked
|
|
||||||
@newline_marked = Array.new(old_newline_marked.size, false)
|
|
||||||
begin
|
|
||||||
super(node)
|
|
||||||
ensure
|
|
||||||
@newline_marked = old_newline_marked
|
|
||||||
end
|
|
||||||
end
|
|
||||||
alias_method :visit_lambda_node, :visit_block_node
|
|
||||||
|
|
||||||
def visit_if_node(node)
|
|
||||||
node.set_newline_flag(@newline_marked)
|
|
||||||
super(node)
|
|
||||||
end
|
|
||||||
alias_method :visit_unless_node, :visit_if_node
|
|
||||||
|
|
||||||
def visit_statements_node(node)
|
|
||||||
node.body.each do |child|
|
|
||||||
child.set_newline_flag(@newline_marked)
|
|
||||||
end
|
|
||||||
super(node)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
private_constant :MarkNewlinesVisitor
|
|
||||||
|
|
||||||
def mark_newlines
|
|
||||||
newline_marked = Array.new(1 + @source.offsets.size, false)
|
|
||||||
visitor = MarkNewlinesVisitor.new(newline_marked)
|
|
||||||
value.accept(visitor)
|
|
||||||
value
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# This represents a token from the Ruby source.
|
# This represents a token from the Ruby source.
|
||||||
@ -778,6 +591,9 @@ require_relative "yarp/serialize"
|
|||||||
require_relative "yarp/pack"
|
require_relative "yarp/pack"
|
||||||
require_relative "yarp/pattern"
|
require_relative "yarp/pattern"
|
||||||
|
|
||||||
|
require_relative "yarp/parse_result/comments"
|
||||||
|
require_relative "yarp/parse_result/newlines"
|
||||||
|
|
||||||
if RUBY_ENGINE == "ruby" and !ENV["YARP_FFI_BACKEND"]
|
if RUBY_ENGINE == "ruby" and !ENV["YARP_FFI_BACKEND"]
|
||||||
require "yarp/yarp"
|
require "yarp/yarp"
|
||||||
else
|
else
|
||||||
|
174
lib/yarp/parse_result/comments.rb
Normal file
174
lib/yarp/parse_result/comments.rb
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module YARP
|
||||||
|
class ParseResult
|
||||||
|
# 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
|
||||||
|
attr_reader :node
|
||||||
|
|
||||||
|
def initialize(node)
|
||||||
|
@node = node
|
||||||
|
end
|
||||||
|
|
||||||
|
def start_offset
|
||||||
|
node.location.start_offset
|
||||||
|
end
|
||||||
|
|
||||||
|
def end_offset
|
||||||
|
node.location.end_offset
|
||||||
|
end
|
||||||
|
|
||||||
|
def encloses?(comment)
|
||||||
|
start_offset <= comment.location.start_offset &&
|
||||||
|
comment.location.end_offset <= end_offset
|
||||||
|
end
|
||||||
|
|
||||||
|
def <<(comment)
|
||||||
|
node.location.comments << 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
|
||||||
|
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 <<(comment)
|
||||||
|
location.comments << comment
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
attr_reader :parse_result
|
||||||
|
|
||||||
|
def initialize(parse_result)
|
||||||
|
@parse_result = parse_result
|
||||||
|
end
|
||||||
|
|
||||||
|
def attach!
|
||||||
|
parse_result.comments.each do |comment|
|
||||||
|
preceding, enclosing, following = nearest_targets(parse_result.value, comment)
|
||||||
|
target =
|
||||||
|
if comment.trailing?
|
||||||
|
preceding || following || enclosing || NodeTarget.new(parse_result.value)
|
||||||
|
else
|
||||||
|
# If a comment exists on its own line, prefer a leading comment.
|
||||||
|
following || preceding || enclosing || NodeTarget.new(parse_result.value)
|
||||||
|
end
|
||||||
|
|
||||||
|
target << comment
|
||||||
|
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 = []
|
||||||
|
node.deconstruct_keys(nil).each do |key, value|
|
||||||
|
next if key == :location
|
||||||
|
|
||||||
|
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)
|
||||||
|
when Array
|
||||||
|
targets.concat(value.map { |node| NodeTarget.new(node) }) if value.first.is_a?(Node)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
targets.sort_by!(&:start_offset)
|
||||||
|
preceding = nil
|
||||||
|
following = nil
|
||||||
|
|
||||||
|
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)
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Attach the list of comments to their respective locations in the tree.
|
||||||
|
def attach_comments!
|
||||||
|
Comments.new(self).attach!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
61
lib/yarp/parse_result/newlines.rb
Normal file
61
lib/yarp/parse_result/newlines.rb
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module YARP
|
||||||
|
class ParseResult
|
||||||
|
# The :line tracepoint event gets fired whenever the Ruby VM encounters an
|
||||||
|
# expression on a new line. The types of expressions that can trigger this
|
||||||
|
# event are:
|
||||||
|
#
|
||||||
|
# * if statements
|
||||||
|
# * unless statements
|
||||||
|
# * nodes that are children of statements lists
|
||||||
|
#
|
||||||
|
# In order to keep track of the newlines, we have a list of offsets that
|
||||||
|
# come back from the parser. We assign these offsets to the first nodes that
|
||||||
|
# we find in the tree that are on those lines.
|
||||||
|
#
|
||||||
|
# Note that the logic in this file should be kept in sync with the Java
|
||||||
|
# MarkNewlinesVisitor, since that visitor is responsible for marking the
|
||||||
|
# newlines for JRuby/TruffleRuby.
|
||||||
|
class MarkNewlinesVisitor < Visitor
|
||||||
|
def initialize(newline_marked)
|
||||||
|
@newline_marked = newline_marked
|
||||||
|
end
|
||||||
|
|
||||||
|
def visit_block_node(node)
|
||||||
|
old_newline_marked = @newline_marked
|
||||||
|
@newline_marked = Array.new(old_newline_marked.size, false)
|
||||||
|
|
||||||
|
begin
|
||||||
|
super(node)
|
||||||
|
ensure
|
||||||
|
@newline_marked = old_newline_marked
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
alias_method :visit_lambda_node, :visit_block_node
|
||||||
|
|
||||||
|
def visit_if_node(node)
|
||||||
|
node.set_newline_flag(@newline_marked)
|
||||||
|
super(node)
|
||||||
|
end
|
||||||
|
|
||||||
|
alias_method :visit_unless_node, :visit_if_node
|
||||||
|
|
||||||
|
def visit_statements_node(node)
|
||||||
|
node.body.each do |child|
|
||||||
|
child.set_newline_flag(@newline_marked)
|
||||||
|
end
|
||||||
|
super(node)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private_constant :MarkNewlinesVisitor
|
||||||
|
|
||||||
|
# Walk the tree and mark nodes that are on a new line.
|
||||||
|
def mark_newlines!
|
||||||
|
newline_marked = Array.new(1 + source.offsets.size, false)
|
||||||
|
value.accept(MarkNewlinesVisitor.new(newline_marked))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -68,6 +68,8 @@ Gem::Specification.new do |spec|
|
|||||||
"lib/yarp/pattern.rb",
|
"lib/yarp/pattern.rb",
|
||||||
"lib/yarp/ripper_compat.rb",
|
"lib/yarp/ripper_compat.rb",
|
||||||
"lib/yarp/serialize.rb",
|
"lib/yarp/serialize.rb",
|
||||||
|
"lib/yarp/parse_result/comments.rb",
|
||||||
|
"lib/yarp/parse_result/newlines.rb",
|
||||||
"src/diagnostic.c",
|
"src/diagnostic.c",
|
||||||
"src/enc/yp_big5.c",
|
"src/enc/yp_big5.c",
|
||||||
"src/enc/yp_euc_jp.c",
|
"src/enc/yp_euc_jp.c",
|
||||||
|
@ -37,7 +37,7 @@ module YARP
|
|||||||
result = YARP.parse_file(filepath)
|
result = YARP.parse_file(filepath)
|
||||||
assert_empty result.errors
|
assert_empty result.errors
|
||||||
|
|
||||||
result.mark_newlines
|
result.mark_newlines!
|
||||||
visitor = NewlineVisitor.new(result.source)
|
visitor = NewlineVisitor.new(result.source)
|
||||||
|
|
||||||
result.value.accept(visitor)
|
result.value.accept(visitor)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user