# frozen_string_literal: true
module SyntaxTree
# Represents the location of a node in the tree from the source code.
class Location
attr_reader :start_line,
:start_char,
:start_column,
:end_line,
:end_char,
:end_column
def initialize(
start_line:,
start_char:,
start_column:,
end_line:,
end_char:,
end_column:
)
@start_line = start_line
@start_char = start_char
@start_column = start_column
@end_line = end_line
@end_char = end_char
@end_column = end_column
end
def lines
start_line..end_line
end
def ==(other)
other.is_a?(Location) && start_line == other.start_line &&
start_char == other.start_char && end_line == other.end_line &&
end_char == other.end_char
end
def to(other)
Location.new(
start_line: start_line,
start_char: start_char,
start_column: start_column,
end_line: [end_line, other.end_line].max,
end_char: other.end_char,
end_column: other.end_column
)
end
def deconstruct
[start_line, start_char, start_column, end_line, end_char, end_column]
end
def deconstruct_keys(_keys)
{
start_line: start_line,
start_char: start_char,
start_column: start_column,
end_line: end_line,
end_char: end_char,
end_column: end_column
}
end
def self.token(line:, char:, column:, size:)
new(
start_line: line,
start_char: char,
start_column: column,
end_line: line,
end_char: char + size,
end_column: column + size
)
end
def self.fixed(line:, char:, column:)
new(
start_line: line,
start_char: char,
start_column: column,
end_line: line,
end_char: char,
end_column: column
)
end
# A convenience method that is typically used when you don't care about the
# location of a node, but need to create a Location instance to pass to a
# constructor.
def self.default
new(
start_line: 1,
start_char: 0,
start_column: 0,
end_line: 1,
end_char: 0,
end_column: 0
)
end
end
# This is the parent node of all of the syntax tree nodes. It's pretty much
# exclusively here to make it easier to operate with the tree in cases where
# you're trying to monkey-patch or strictly type.
class Node
# [Location] the location of this node
attr_reader :location
def accept(visitor)
raise NotImplementedError
end
def child_nodes
raise NotImplementedError
end
def deconstruct
raise NotImplementedError
end
def deconstruct_keys(keys)
raise NotImplementedError
end
def format(q)
raise NotImplementedError
end
def start_char
location.start_char
end
def end_char
location.end_char
end
def pretty_print(q)
accept(PrettyPrintVisitor.new(q))
end
def to_json(*opts)
accept(JSONVisitor.new).to_json(*opts)
end
def to_mermaid
accept(MermaidVisitor.new)
end
def construct_keys
PrettierPrint.format(+"") { |q| accept(MatchVisitor.new(q)) }
end
end
# When we're implementing the === operator for a node, we oftentimes need to
# compare two arrays. We want to skip over the === definition of array and use
# our own here, so we do that using this module.
module ArrayMatch
def self.call(left, right)
left.length === right.length &&
left
.zip(right)
.all? { |left_value, right_value| left_value === right_value }
end
end
# BEGINBlock represents the use of the +BEGIN+ keyword, which hooks into the
# lifecycle of the interpreter. Whatever is inside the block will get executed
# when the program starts.
#
# BEGIN {
# }
#
# Interestingly, the BEGIN keyword doesn't allow the do and end keywords for
# the block. Only braces are permitted.
class BEGINBlock < Node
# [LBrace] the left brace that is seen after the keyword
attr_reader :lbrace
# [Statements] the expressions to be executed
attr_reader :statements
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(lbrace:, statements:, location:)
@lbrace = lbrace
@statements = statements
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_BEGIN(self)
end
def child_nodes
[lbrace, statements]
end
def copy(lbrace: nil, statements: nil, location: nil)
node =
BEGINBlock.new(
lbrace: lbrace || self.lbrace,
statements: statements || self.statements,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
lbrace: lbrace,
statements: statements,
location: location,
comments: comments
}
end
def format(q)
q.group do
q.text("BEGIN ")
q.format(lbrace)
q.indent do
q.breakable_space
q.format(statements)
end
q.breakable_space
q.text("}")
end
end
def ===(other)
other.is_a?(BEGINBlock) && lbrace === other.lbrace &&
statements === other.statements
end
end
# CHAR irepresents a single codepoint in the script encoding.
#
# ?a
#
# In the example above, the CHAR node represents the string literal "a". You
# can use control characters with this as well, as in ?\C-a.
class CHAR < Node
# [String] the value of the character literal
attr_reader :value
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(value:, location:)
@value = value
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_CHAR(self)
end
def child_nodes
[]
end
def copy(value: nil, location: nil)
node =
CHAR.new(
value: value || self.value,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location, comments: comments }
end
def format(q)
if value.length != 2
q.text(value)
else
q.text(q.quote)
q.text(value[1] == q.quote ? "\\#{q.quote}" : value[1])
q.text(q.quote)
end
end
def ===(other)
other.is_a?(CHAR) && value === other.value
end
end
# ENDBlock represents the use of the +END+ keyword, which hooks into the
# lifecycle of the interpreter. Whatever is inside the block will get executed
# when the program ends.
#
# END {
# }
#
# Interestingly, the END keyword doesn't allow the do and end keywords for the
# block. Only braces are permitted.
class ENDBlock < Node
# [LBrace] the left brace that is seen after the keyword
attr_reader :lbrace
# [Statements] the expressions to be executed
attr_reader :statements
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(lbrace:, statements:, location:)
@lbrace = lbrace
@statements = statements
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_END(self)
end
def child_nodes
[lbrace, statements]
end
def copy(lbrace: nil, statements: nil, location: nil)
node =
ENDBlock.new(
lbrace: lbrace || self.lbrace,
statements: statements || self.statements,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
lbrace: lbrace,
statements: statements,
location: location,
comments: comments
}
end
def format(q)
q.group do
q.text("END ")
q.format(lbrace)
q.indent do
q.breakable_space
q.format(statements)
end
q.breakable_space
q.text("}")
end
end
def ===(other)
other.is_a?(ENDBlock) && lbrace === other.lbrace &&
statements === other.statements
end
end
# EndContent represents the use of __END__ syntax, which allows individual
# scripts to keep content after the main ruby code that can be read through
# the DATA constant.
#
# puts DATA.read
#
# __END__
# some other content that is not executed by the program
#
class EndContent < Node
# [String] the content after the script
attr_reader :value
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(value:, location:)
@value = value
@location = location
@comments = []
end
def accept(visitor)
visitor.visit___end__(self)
end
def child_nodes
[]
end
def copy(value: nil, location: nil)
node =
EndContent.new(
value: value || self.value,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location, comments: comments }
end
def format(q)
q.text("__END__")
q.breakable_force
first = true
value.each_line(chomp: true) do |line|
if first
first = false
else
q.breakable_return
end
q.text(line)
end
q.breakable_return if value.end_with?("\n")
end
def ===(other)
other.is_a?(EndContent) && value === other.value
end
end
# Alias represents the use of the +alias+ keyword with regular arguments (not
# global variables). The +alias+ keyword is used to make a method respond to
# another name as well as the current one.
#
# alias aliased_name name
#
# For the example above, in the current context you can now call aliased_name
# and it will execute the name method. When you're aliasing two methods, you
# can either provide bare words (like the example above) or you can provide
# symbols (note that this includes dynamic symbols like
# :"left-#{middle}-right").
class AliasNode < Node
# Formats an argument to the alias keyword. For symbol literals it uses the
# value of the symbol directly to look like bare words.
class AliasArgumentFormatter
# [Backref | DynaSymbol | GVar | SymbolLiteral] the argument being passed
# to alias
attr_reader :argument
def initialize(argument)
@argument = argument
end
def comments
if argument.is_a?(SymbolLiteral)
argument.comments + argument.value.comments
else
argument.comments
end
end
def format(q)
if argument.is_a?(SymbolLiteral)
q.format(argument.value)
else
q.format(argument)
end
end
end
# [DynaSymbol | GVar | SymbolLiteral] the new name of the method
attr_reader :left
# [Backref | DynaSymbol | GVar | SymbolLiteral] the old name of the method
attr_reader :right
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(left:, right:, location:)
@left = left
@right = right
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_alias(self)
end
def child_nodes
[left, right]
end
def copy(left: nil, right: nil, location: nil)
node =
AliasNode.new(
left: left || self.left,
right: right || self.right,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ left: left, right: right, location: location, comments: comments }
end
def format(q)
keyword = "alias "
left_argument = AliasArgumentFormatter.new(left)
q.group do
q.text(keyword)
q.format(left_argument, stackable: false)
q.group do
q.nest(keyword.length) do
left_argument.comments.any? ? q.breakable_force : q.breakable_space
q.format(AliasArgumentFormatter.new(right), stackable: false)
end
end
end
end
def ===(other)
other.is_a?(AliasNode) && left === other.left && right === other.right
end
def var_alias?
left.is_a?(GVar)
end
end
# ARef represents when you're pulling a value out of a collection at a
# specific index. Put another way, it's any time you're calling the method
# #[].
#
# collection[index]
#
# The nodes usually contains two children, the collection and the index. In
# some cases, you don't necessarily have the second child node, because you
# can call procs with a pretty esoteric syntax. In the following example, you
# wouldn't have a second child node:
#
# collection[]
#
class ARef < Node
# [Node] the value being indexed
attr_reader :collection
# [nil | Args] the value being passed within the brackets
attr_reader :index
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(collection:, index:, location:)
@collection = collection
@index = index
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_aref(self)
end
def child_nodes
[collection, index]
end
def copy(collection: nil, index: nil, location: nil)
node =
ARef.new(
collection: collection || self.collection,
index: index || self.index,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
collection: collection,
index: index,
location: location,
comments: comments
}
end
def format(q)
q.group do
q.format(collection)
q.text("[")
if index
q.indent do
q.breakable_empty
q.format(index)
end
q.breakable_empty
end
q.text("]")
end
end
def ===(other)
other.is_a?(ARef) && collection === other.collection &&
index === other.index
end
end
# ARefField represents assigning values into collections at specific indices.
# Put another way, it's any time you're calling the method #[]=. The
# ARefField node itself is just the left side of the assignment, and they're
# always wrapped in assign nodes.
#
# collection[index] = value
#
class ARefField < Node
# [Node] the value being indexed
attr_reader :collection
# [nil | Args] the value being passed within the brackets
attr_reader :index
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(collection:, index:, location:)
@collection = collection
@index = index
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_aref_field(self)
end
def child_nodes
[collection, index]
end
def copy(collection: nil, index: nil, location: nil)
node =
ARefField.new(
collection: collection || self.collection,
index: index || self.index,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
collection: collection,
index: index,
location: location,
comments: comments
}
end
def format(q)
q.group do
q.format(collection)
q.text("[")
if index
q.indent do
q.breakable_empty
q.format(index)
end
q.breakable_empty
end
q.text("]")
end
end
def ===(other)
other.is_a?(ARefField) && collection === other.collection &&
index === other.index
end
end
# ArgParen represents wrapping arguments to a method inside a set of
# parentheses.
#
# method(argument)
#
# In the example above, there would be an ArgParen node around the Args node
# that represents the set of arguments being sent to the method method. The
# argument child node can be +nil+ if no arguments were passed, as in:
#
# method()
#
class ArgParen < Node
# [nil | Args | ArgsForward] the arguments inside the
# parentheses
attr_reader :arguments
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(arguments:, location:)
@arguments = arguments
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_arg_paren(self)
end
def child_nodes
[arguments]
end
def copy(arguments: nil, location: nil)
node =
ArgParen.new(
arguments: arguments || self.arguments,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ arguments: arguments, location: location, comments: comments }
end
def format(q)
unless arguments
q.text("()")
return
end
q.text("(")
q.group do
q.indent do
q.breakable_empty
q.format(arguments)
q.if_break { q.text(",") } if q.trailing_comma? && trailing_comma?
end
q.breakable_empty
end
q.text(")")
end
def ===(other)
other.is_a?(ArgParen) && arguments === other.arguments
end
def arity
arguments&.arity || 0
end
private
def trailing_comma?
arguments = self.arguments
return false unless arguments.is_a?(Args)
parts = arguments.parts
if parts.last.is_a?(ArgBlock)
# If the last argument is a block, then we can't put a trailing comma
# after it without resulting in a syntax error.
false
elsif (parts.length == 1) && (part = parts.first) &&
(part.is_a?(Command) || part.is_a?(CommandCall))
# If the only argument is a command or command call, then a trailing
# comma would be parsed as part of that expression instead of on this
# one, so we don't want to add a trailing comma.
false
else
# Otherwise, we should be okay to add a trailing comma.
true
end
end
end
# Args represents a list of arguments being passed to a method call or array
# literal.
#
# method(first, second, third)
#
class Args < Node
# [Array[ Node ]] the arguments that this node wraps
attr_reader :parts
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(parts:, location:)
@parts = parts
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_args(self)
end
def child_nodes
parts
end
def copy(parts: nil, location: nil)
node =
Args.new(
parts: parts || self.parts,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ parts: parts, location: location, comments: comments }
end
def format(q)
q.seplist(parts) { |part| q.format(part) }
end
def ===(other)
other.is_a?(Args) && ArrayMatch.call(parts, other.parts)
end
def arity
parts.sum do |part|
case part
when ArgStar, ArgsForward
Float::INFINITY
when BareAssocHash
part.assocs.sum do |assoc|
assoc.is_a?(AssocSplat) ? Float::INFINITY : 1
end
else
1
end
end
end
end
# ArgBlock represents using a block operator on an expression.
#
# method(&expression)
#
class ArgBlock < Node
# [nil | Node] the expression being turned into a block
attr_reader :value
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(value:, location:)
@value = value
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_arg_block(self)
end
def child_nodes
[value]
end
def copy(value: nil, location: nil)
node =
ArgBlock.new(
value: value || self.value,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location, comments: comments }
end
def format(q)
q.text("&")
q.format(value) if value
end
def ===(other)
other.is_a?(ArgBlock) && value === other.value
end
end
# Star represents using a splat operator on an expression.
#
# method(*arguments)
#
class ArgStar < Node
# [nil | Node] the expression being splatted
attr_reader :value
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(value:, location:)
@value = value
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_arg_star(self)
end
def child_nodes
[value]
end
def copy(value: nil, location: nil)
node =
ArgStar.new(
value: value || self.value,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location, comments: comments }
end
def format(q)
q.text("*")
q.format(value) if value
end
def ===(other)
other.is_a?(ArgStar) && value === other.value
end
end
# ArgsForward represents forwarding all kinds of arguments onto another method
# call.
#
# def request(method, path, **headers, &block); end
#
# def get(...)
# request(:GET, ...)
# end
#
# def post(...)
# request(:POST, ...)
# end
#
# In the example above, both the get and post methods are forwarding all of
# their arguments (positional, keyword, and block) on to the request method.
# The ArgsForward node appears in both the caller (the request method calls)
# and the callee (the get and post definitions).
class ArgsForward < Node
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(location:)
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_args_forward(self)
end
def child_nodes
[]
end
def copy(location: nil)
node = ArgsForward.new(location: location || self.location)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ location: location, comments: comments }
end
def format(q)
q.text("...")
end
def ===(other)
other.is_a?(ArgsForward)
end
def arity
Float::INFINITY
end
end
# ArrayLiteral represents an array literal, which can optionally contain
# elements.
#
# []
# [one, two, three]
#
class ArrayLiteral < Node
# It's very common to use seplist with ->(q) { q.breakable_space }. We wrap
# that pattern into an object to cut down on having to create a bunch of
# lambdas all over the place.
class BreakableSpaceSeparator
def call(q)
q.breakable_space
end
end
BREAKABLE_SPACE_SEPARATOR = BreakableSpaceSeparator.new.freeze
# Formats an array of multiple simple string literals into the %w syntax.
class QWordsFormatter
# [Args] the contents of the array
attr_reader :contents
def initialize(contents)
@contents = contents
end
def format(q)
q.text("%w[")
q.group do
q.indent do
q.breakable_empty
q.seplist(contents.parts, BREAKABLE_SPACE_SEPARATOR) do |part|
if part.is_a?(StringLiteral)
q.format(part.parts.first)
else
q.text(part.value[1..])
end
end
end
q.breakable_empty
end
q.text("]")
end
end
# Formats an array of multiple simple symbol literals into the %i syntax.
class QSymbolsFormatter
# [Args] the contents of the array
attr_reader :contents
def initialize(contents)
@contents = contents
end
def format(q)
q.text("%i[")
q.group do
q.indent do
q.breakable_empty
q.seplist(contents.parts, BREAKABLE_SPACE_SEPARATOR) do |part|
q.format(part.value)
end
end
q.breakable_empty
end
q.text("]")
end
end
# This is a special formatter used if the array literal contains no values
# but _does_ contain comments. In this case we do some special formatting to
# make sure the comments gets indented properly.
class EmptyWithCommentsFormatter
# [LBracket] the opening bracket
attr_reader :lbracket
def initialize(lbracket)
@lbracket = lbracket
end
def format(q)
q.group do
q.text("[")
q.indent do
lbracket.comments.each do |comment|
q.breakable_force
comment.format(q)
end
end
q.breakable_force
q.text("]")
end
end
end
# [nil | LBracket | QSymbolsBeg | QWordsBeg | SymbolsBeg | WordsBeg] the
# bracket that opens this array
attr_reader :lbracket
# [nil | Args] the contents of the array
attr_reader :contents
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(lbracket:, contents:, location:)
@lbracket = lbracket
@contents = contents
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_array(self)
end
def child_nodes
[lbracket, contents]
end
def copy(lbracket: nil, contents: nil, location: nil)
node =
ArrayLiteral.new(
lbracket: lbracket || self.lbracket,
contents: contents || self.contents,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
lbracket: lbracket,
contents: contents,
location: location,
comments: comments
}
end
def format(q)
lbracket = self.lbracket
contents = self.contents
if lbracket.is_a?(LBracket) && lbracket.comments.empty? && contents &&
contents.comments.empty? && contents.parts.length > 1
if qwords?
QWordsFormatter.new(contents).format(q)
return
end
if qsymbols?
QSymbolsFormatter.new(contents).format(q)
return
end
end
if empty_with_comments?
EmptyWithCommentsFormatter.new(lbracket).format(q)
return
end
q.group do
q.format(lbracket)
if contents
q.indent do
q.breakable_empty
q.format(contents)
q.if_break { q.text(",") } if q.trailing_comma?
end
end
q.breakable_empty
q.text("]")
end
end
def ===(other)
other.is_a?(ArrayLiteral) && lbracket === other.lbracket &&
contents === other.contents
end
private
def qwords?
contents.parts.all? do |part|
case part
when StringLiteral
part.comments.empty? && part.parts.length == 1 &&
part.parts.first.is_a?(TStringContent) &&
!part.parts.first.value.match?(/[\s\[\]\\]/)
when CHAR
!part.value.match?(/[\[\]\\]/)
else
false
end
end
end
def qsymbols?
contents.parts.all? do |part|
part.is_a?(SymbolLiteral) && part.comments.empty?
end
end
# If we have an empty array that contains only comments, then we're going
# to do some special printing to ensure they get indented correctly.
def empty_with_comments?
contents.nil? && lbracket.comments.any? &&
lbracket.comments.none?(&:inline?)
end
end
# AryPtn represents matching against an array pattern using the Ruby 2.7+
# pattern matching syntax. It’s one of the more complicated nodes, because
# the four parameters that it accepts can almost all be nil.
#
# case [1, 2, 3]
# in [Integer, Integer]
# "matched"
# in Container[Integer, Integer]
# "matched"
# in [Integer, *, Integer]
# "matched"
# end
#
# An AryPtn node is created with four parameters: an optional constant
# wrapper, an array of positional matches, an optional splat with identifier,
# and an optional array of positional matches that occur after the splat.
# All of the in clauses above would create an AryPtn node.
class AryPtn < Node
# Formats the optional splat of an array pattern.
class RestFormatter
# [VarField] the identifier that represents the remaining positionals
attr_reader :value
def initialize(value)
@value = value
end
def comments
value.comments
end
def format(q)
q.text("*")
q.format(value)
end
end
# [nil | VarRef | ConstPathRef] the optional constant wrapper
attr_reader :constant
# [Array[ Node ]] the regular positional arguments that this array
# pattern is matching against
attr_reader :requireds
# [nil | VarField] the optional starred identifier that grabs up a list of
# positional arguments
attr_reader :rest
# [Array[ Node ]] the list of positional arguments occurring after the
# optional star if there is one
attr_reader :posts
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(constant:, requireds:, rest:, posts:, location:)
@constant = constant
@requireds = requireds
@rest = rest
@posts = posts
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_aryptn(self)
end
def child_nodes
[constant, *requireds, rest, *posts]
end
def copy(
constant: nil,
requireds: nil,
rest: nil,
posts: nil,
location: nil
)
node =
AryPtn.new(
constant: constant || self.constant,
requireds: requireds || self.requireds,
rest: rest || self.rest,
posts: posts || self.posts,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
constant: constant,
requireds: requireds,
rest: rest,
posts: posts,
location: location,
comments: comments
}
end
def format(q)
q.group do
q.format(constant) if constant
q.text("[")
q.indent do
q.breakable_empty
parts = [*requireds]
parts << RestFormatter.new(rest) if rest
parts += posts
q.seplist(parts) { |part| q.format(part) }
end
q.breakable_empty
q.text("]")
end
end
def ===(other)
other.is_a?(AryPtn) && constant === other.constant &&
ArrayMatch.call(requireds, other.requireds) && rest === other.rest &&
ArrayMatch.call(posts, other.posts)
end
end
# Determins if the following value should be indented or not.
module AssignFormatting
def self.skip_indent?(value)
case value
when ArrayLiteral, HashLiteral, Heredoc, Lambda, QSymbols, QWords,
Symbols, Words
true
when CallNode
skip_indent?(value.receiver)
when DynaSymbol
value.quote.start_with?("%s")
else
false
end
end
end
# Assign represents assigning something to a variable or constant. Generally,
# the left side of the assignment is going to be any node that ends with the
# name "Field".
#
# variable = value
#
class Assign < Node
# [ARefField | ConstPathField | Field | TopConstField | VarField] the target
# to assign the result of the expression to
attr_reader :target
# [Node] the expression to be assigned
attr_reader :value
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(target:, value:, location:)
@target = target
@value = value
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_assign(self)
end
def child_nodes
[target, value]
end
def copy(target: nil, value: nil, location: nil)
node =
Assign.new(
target: target || self.target,
value: value || self.value,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ target: target, value: value, location: location, comments: comments }
end
def format(q)
q.group do
q.format(target)
q.text(" =")
if skip_indent?
q.text(" ")
q.format(value)
else
q.indent do
q.breakable_space
q.format(value)
end
end
end
end
def ===(other)
other.is_a?(Assign) && target === other.target && value === other.value
end
private
def skip_indent?
target.comments.empty? &&
(target.is_a?(ARefField) || AssignFormatting.skip_indent?(value))
end
end
# Assoc represents a key-value pair within a hash. It is a child node of
# either an AssocListFromArgs or a BareAssocHash.
#
# { key1: value1, key2: value2 }
#
# In the above example, the would be two Assoc nodes.
class Assoc < Node
# [Node] the key of this pair
attr_reader :key
# [nil | Node] the value of this pair
attr_reader :value
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(key:, value:, location:)
@key = key
@value = value
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_assoc(self)
end
def child_nodes
[key, value]
end
def copy(key: nil, value: nil, location: nil)
node =
Assoc.new(
key: key || self.key,
value: value || self.value,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ key: key, value: value, location: location, comments: comments }
end
def format(q)
if value.is_a?(HashLiteral)
format_contents(q)
else
q.group { format_contents(q) }
end
end
def ===(other)
other.is_a?(Assoc) && key === other.key && value === other.value
end
private
def format_contents(q)
(q.parent || HashKeyFormatter::Identity.new).format_key(q, key)
return unless value
if key.comments.empty? && AssignFormatting.skip_indent?(value)
q.text(" ")
q.format(value)
else
q.indent do
q.breakable_space
q.format(value)
end
end
end
end
# AssocSplat represents double-splatting a value into a hash (either a hash
# literal or a bare hash in a method call).
#
# { **pairs }
#
class AssocSplat < Node
# [nil | Node] the expression that is being splatted
attr_reader :value
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(value:, location:)
@value = value
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_assoc_splat(self)
end
def child_nodes
[value]
end
def copy(value: nil, location: nil)
node =
AssocSplat.new(
value: value || self.value,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location, comments: comments }
end
def format(q)
q.text("**")
q.format(value) if value
end
def ===(other)
other.is_a?(AssocSplat) && value === other.value
end
end
# Backref represents a global variable referencing a matched value. It comes
# in the form of a $ followed by a positive integer.
#
# $1
#
class Backref < Node
# [String] the name of the global backreference variable
attr_reader :value
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(value:, location:)
@value = value
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_backref(self)
end
def child_nodes
[]
end
def copy(value: nil, location: nil)
node =
Backref.new(
value: value || self.value,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location, comments: comments }
end
def format(q)
q.text(value)
end
def ===(other)
other.is_a?(Backref) && value === other.value
end
end
# Backtick represents the use of the ` operator. It's usually found being used
# for an XStringLiteral, but could also be found as the name of a method being
# defined.
class Backtick < Node
# [String] the backtick in the string
attr_reader :value
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(value:, location:)
@value = value
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_backtick(self)
end
def child_nodes
[]
end
def copy(value: nil, location: nil)
node =
Backtick.new(
value: value || self.value,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location, comments: comments }
end
def format(q)
q.text(value)
end
def ===(other)
other.is_a?(Backtick) && value === other.value
end
end
# This module is responsible for formatting the assocs contained within a
# hash or bare hash. It first determines if every key in the hash can use
# labels. If it can, it uses labels. Otherwise it uses hash rockets.
module HashKeyFormatter
# Formats the keys of a hash literal using labels.
class Labels
LABEL = /\A[A-Za-z_](\w*[\w!?])?\z/.freeze
def format_key(q, key)
case key
when Label
q.format(key)
when SymbolLiteral
q.format(key.value)
q.text(":")
when DynaSymbol
parts = key.parts
if parts.length == 1 && (part = parts.first) &&
part.is_a?(TStringContent) && part.value.match?(LABEL)
q.format(part)
q.text(":")
else
q.format(key)
q.text(":")
end
end
end
end
# Formats the keys of a hash literal using hash rockets.
class Rockets
def format_key(q, key)
case key
when Label
q.text(":#{key.value.chomp(":")}")
when DynaSymbol
q.text(":")
q.format(key)
else
q.format(key)
end
q.text(" =>")
end
end
# When formatting a single assoc node without the context of the parent
# hash, this formatter is used. It uses whatever is present in the node,
# because there is nothing to be consistent with.
class Identity
def format_key(q, key)
if key.is_a?(Label)
q.format(key)
else
q.format(key)
q.text(" =>")
end
end
end
class << self
def for(container)
(assocs = container.assocs).each_with_index do |assoc, index|
if assoc.is_a?(AssocSplat)
# Splat nodes do not impact the formatting choice.
elsif assoc.value.nil?
# If the value is nil, then it has been omitted. In this case we
# have to match the existing formatting because standardizing would
# potentially break the code. For example:
#
# { first:, "second" => "value" }
#
return Identity.new
else
# Otherwise, we need to check the type of the key. If it's a label
# or dynamic symbol, we can use labels. If it's a symbol literal
# then it needs to match a certain pattern to be used as a label. If
# it's anything else, then we need to use hash rockets.
case assoc.key
when Label, DynaSymbol
# Here labels can be used.
when SymbolLiteral
# When attempting to convert a hash rocket into a hash label,
# you need to take care because only certain patterns are
# allowed. Ruby source says that they have to match keyword
# arguments to methods, but don't specify what that is. After
# some experimentation, it looks like it's:
value = assoc.key.value.value
if !value.match?(/^[_A-Za-z]/) || value.end_with?("=")
if omitted_value?(assocs[(index + 1)..])
return Identity.new
else
return Rockets.new
end
end
else
if omitted_value?(assocs[(index + 1)..])
return Identity.new
else
return Rockets.new
end
end
end
end
Labels.new
end
private
def omitted_value?(assocs)
assocs.any? { |assoc| !assoc.is_a?(AssocSplat) && assoc.value.nil? }
end
end
end
# BareAssocHash represents a hash of contents being passed as a method
# argument (and therefore has omitted braces). It's very similar to an
# AssocListFromArgs node.
#
# method(key1: value1, key2: value2)
#
class BareAssocHash < Node
# [Array[ Assoc | AssocSplat ]]
attr_reader :assocs
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(assocs:, location:)
@assocs = assocs
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_bare_assoc_hash(self)
end
def child_nodes
assocs
end
def copy(assocs: nil, location: nil)
node =
BareAssocHash.new(
assocs: assocs || self.assocs,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ assocs: assocs, location: location, comments: comments }
end
def format(q)
q.seplist(assocs) { |assoc| q.format(assoc) }
end
def ===(other)
other.is_a?(BareAssocHash) && ArrayMatch.call(assocs, other.assocs)
end
def format_key(q, key)
@key_formatter ||=
case q.parents.take(3).last
when Break, Next, ReturnNode
HashKeyFormatter::Identity.new
else
HashKeyFormatter.for(self)
end
@key_formatter.format_key(q, key)
end
end
# Begin represents a begin..end chain.
#
# begin
# value
# end
#
class Begin < Node
# [BodyStmt] the bodystmt that contains the contents of this begin block
attr_reader :bodystmt
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(bodystmt:, location:)
@bodystmt = bodystmt
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_begin(self)
end
def child_nodes
[bodystmt]
end
def copy(bodystmt: nil, location: nil)
node =
Begin.new(
bodystmt: bodystmt || self.bodystmt,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ bodystmt: bodystmt, location: location, comments: comments }
end
def format(q)
q.text("begin")
unless bodystmt.empty?
q.indent do
q.breakable_force unless bodystmt.statements.empty?
q.format(bodystmt)
end
end
q.breakable_force
q.text("end")
end
def ===(other)
other.is_a?(Begin) && bodystmt === other.bodystmt
end
end
# PinnedBegin represents a pinning a nested statement within pattern matching.
#
# case value
# in ^(statement)
# end
#
class PinnedBegin < Node
# [Node] the expression being pinned
attr_reader :statement
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(statement:, location:)
@statement = statement
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_pinned_begin(self)
end
def child_nodes
[statement]
end
def copy(statement: nil, location: nil)
node =
PinnedBegin.new(
statement: statement || self.statement,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ statement: statement, location: location, comments: comments }
end
def format(q)
q.group do
q.text("^(")
q.nest(1) do
q.indent do
q.breakable_empty
q.format(statement)
end
q.breakable_empty
q.text(")")
end
end
end
def ===(other)
other.is_a?(PinnedBegin) && statement === other.statement
end
end
# Binary represents any expression that involves two sub-expressions with an
# operator in between. This can be something that looks like a mathematical
# operation:
#
# 1 + 1
#
# but can also be something like pushing a value onto an array:
#
# array << value
#
class Binary < Node
# Since Binary's operator is a symbol, it's better to use the `name` method
# than to allocate a new string every time. This is a tiny performance
# optimization, but enough that it shows up in the profiler. Adding this in
# for older Ruby versions.
unless :+.respond_to?(:name)
using Module.new {
refine Symbol do
def name
to_s.freeze
end
end
}
end
# [Node] the left-hand side of the expression
attr_reader :left
# [Symbol] the operator used between the two expressions
attr_reader :operator
# [Node] the right-hand side of the expression
attr_reader :right
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(left:, operator:, right:, location:)
@left = left
@operator = operator
@right = right
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_binary(self)
end
def child_nodes
[left, right]
end
def copy(left: nil, operator: nil, right: nil, location: nil)
node =
Binary.new(
left: left || self.left,
operator: operator || self.operator,
right: right || self.right,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
left: left,
operator: operator,
right: right,
location: location,
comments: comments
}
end
def format(q)
left = self.left
power = operator == :**
q.group do
q.group { q.format(left) }
q.text(" ") unless power
if operator != :<<
q.group do
q.text(operator.name)
q.indent do
power ? q.breakable_empty : q.breakable_space
q.format(right)
end
end
elsif left.is_a?(Binary) && left.operator == :<<
q.group do
q.text(operator.name)
q.indent do
power ? q.breakable_empty : q.breakable_space
q.format(right)
end
end
else
q.text("<< ")
q.format(right)
end
end
end
def ===(other)
other.is_a?(Binary) && left === other.left &&
operator === other.operator && right === other.right
end
end
# BlockVar represents the parameters being declared for a block. Effectively
# this node is everything contained within the pipes. This includes all of the
# various parameter types, as well as block-local variable declarations.
#
# method do |positional, optional = value, keyword:, █ local|
# end
#
class BlockVar < Node
# [Params] the parameters being declared with the block
attr_reader :params
# [Array[ Ident ]] the list of block-local variable declarations
attr_reader :locals
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(params:, locals:, location:)
@params = params
@locals = locals
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_block_var(self)
end
def child_nodes
[params, *locals]
end
def copy(params: nil, locals: nil, location: nil)
node =
BlockVar.new(
params: params || self.params,
locals: locals || self.locals,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ params: params, locals: locals, location: location, comments: comments }
end
# Within the pipes of the block declaration, we don't want any spaces. So
# we'll separate the parameters with a comma and space but no breakables.
class Separator
def call(q)
q.text(", ")
end
end
# We'll keep a single instance of this separator around for all block vars
# to cut down on allocations.
SEPARATOR = Separator.new.freeze
def format(q)
q.text("|")
q.group do
q.remove_breaks(q.format(params))
if locals.any?
q.text("; ")
q.seplist(locals, SEPARATOR) { |local| q.format(local) }
end
end
q.text("|")
end
def ===(other)
other.is_a?(BlockVar) && params === other.params &&
ArrayMatch.call(locals, other.locals)
end
# When a single required parameter is declared for a block, it gets
# automatically expanded if the values being yielded into it are an array.
def arg0?
params.requireds.length == 1 && params.optionals.empty? &&
params.rest.nil? && params.posts.empty? && params.keywords.empty? &&
params.keyword_rest.nil? && params.block.nil?
end
end
# BlockArg represents declaring a block parameter on a method definition.
#
# def method(&block); end
#
class BlockArg < Node
# [nil | Ident] the name of the block argument
attr_reader :name
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(name:, location:)
@name = name
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_blockarg(self)
end
def child_nodes
[name]
end
def copy(name: nil, location: nil)
node =
BlockArg.new(
name: name || self.name,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ name: name, location: location, comments: comments }
end
def format(q)
q.text("&")
q.format(name) if name
end
def ===(other)
other.is_a?(BlockArg) && name === other.name
end
end
# bodystmt can't actually determine its bounds appropriately because it
# doesn't necessarily know where it started. So the parent node needs to
# report back down into this one where it goes.
class BodyStmt < Node
# [Statements] the list of statements inside the begin clause
attr_reader :statements
# [nil | Rescue] the optional rescue chain attached to the begin clause
attr_reader :rescue_clause
# [nil | Kw] the optional else keyword
attr_reader :else_keyword
# [nil | Statements] the optional set of statements inside the else clause
attr_reader :else_clause
# [nil | Ensure] the optional ensure clause
attr_reader :ensure_clause
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(
statements:,
rescue_clause:,
else_keyword:,
else_clause:,
ensure_clause:,
location:
)
@statements = statements
@rescue_clause = rescue_clause
@else_keyword = else_keyword
@else_clause = else_clause
@ensure_clause = ensure_clause
@location = location
@comments = []
end
def bind(parser, start_char, start_column, end_char, end_column)
rescue_clause = self.rescue_clause
@location =
Location.new(
start_line: location.start_line,
start_char: start_char,
start_column: start_column,
end_line: location.end_line,
end_char: end_char,
end_column: end_column
)
# Here we're going to determine the bounds for the statements
consequent = rescue_clause || else_clause || ensure_clause
statements.bind(
parser,
start_char,
start_column,
consequent ? consequent.location.start_char : end_char,
consequent ? consequent.location.start_column : end_column
)
# Next we're going to determine the rescue clause if there is one
if rescue_clause
consequent = else_clause || ensure_clause
rescue_clause.bind_end(
consequent ? consequent.location.start_char : end_char,
consequent ? consequent.location.start_column : end_column
)
end
end
def empty?
statements.empty? && !rescue_clause && !else_clause && !ensure_clause
end
def accept(visitor)
visitor.visit_bodystmt(self)
end
def child_nodes
[statements, rescue_clause, else_keyword, else_clause, ensure_clause]
end
def copy(
statements: nil,
rescue_clause: nil,
else_keyword: nil,
else_clause: nil,
ensure_clause: nil,
location: nil
)
node =
BodyStmt.new(
statements: statements || self.statements,
rescue_clause: rescue_clause || self.rescue_clause,
else_keyword: else_keyword || self.else_keyword,
else_clause: else_clause || self.else_clause,
ensure_clause: ensure_clause || self.ensure_clause,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
statements: statements,
rescue_clause: rescue_clause,
else_keyword: else_keyword,
else_clause: else_clause,
ensure_clause: ensure_clause,
location: location,
comments: comments
}
end
def format(q)
q.group do
q.format(statements) unless statements.empty?
if rescue_clause
q.nest(-2) do
q.breakable_force
q.format(rescue_clause)
end
end
if else_clause
q.nest(-2) do
q.breakable_force
q.format(else_keyword)
end
unless else_clause.empty?
q.breakable_force
q.format(else_clause)
end
end
if ensure_clause
q.nest(-2) do
q.breakable_force
q.format(ensure_clause)
end
end
end
end
def ===(other)
other.is_a?(BodyStmt) && statements === other.statements &&
rescue_clause === other.rescue_clause &&
else_keyword === other.else_keyword &&
else_clause === other.else_clause &&
ensure_clause === other.ensure_clause
end
end
# Formats either a Break, Next, or Return node.
class FlowControlFormatter
# [String] the keyword to print
attr_reader :keyword
# [Break | Next | Return] the node being formatted
attr_reader :node
def initialize(keyword, node)
@keyword = keyword
@node = node
end
def format(q)
# If there are no arguments associated with this flow control, then we can
# safely just print the keyword and return.
if node.arguments.nil?
q.text(keyword)
return
end
q.group do
q.text(keyword)
parts = node.arguments.parts
length = parts.length
if length == 0
# Here there are no arguments at all, so we're not going to print
# anything. This would be like if we had:
#
# break
#
elsif length >= 2
# If there are multiple arguments, format them all. If the line is
# going to break into multiple, then use brackets to start and end the
# expression.
format_arguments(q, " [", "]")
else
# If we get here, then we're formatting a single argument to the flow
# control keyword.
part = parts.first
case part
when Paren
statements = part.contents.body
if statements.length == 1
statement = statements.first
if statement.is_a?(ArrayLiteral)
contents = statement.contents
if contents && contents.parts.length >= 2
# Here we have a single argument that is a set of parentheses
# wrapping an array literal that has at least 2 elements.
# We're going to print the contents of the array directly.
# This would be like if we had:
#
# break([1, 2, 3])
#
# which we will print as:
#
# break 1, 2, 3
#
q.text(" ")
format_array_contents(q, statement)
else
# Here we have a single argument that is a set of parentheses
# wrapping an array literal that has 0 or 1 elements. We're
# going to skip the parentheses but print the array itself.
# This would be like if we had:
#
# break([1])
#
# which we will print as:
#
# break [1]
#
q.text(" ")
q.format(statement)
end
elsif skip_parens?(statement)
# Here we have a single argument that is a set of parentheses
# that themselves contain a single statement. That statement is
# a simple value that we can skip the parentheses for. This
# would be like if we had:
#
# break(1)
#
# which we will print as:
#
# break 1
#
q.text(" ")
q.format(statement)
else
# Here we have a single argument that is a set of parentheses.
# We're going to print the parentheses themselves as if they
# were the set of arguments. This would be like if we had:
#
# break(foo.bar)
#
q.format(part)
end
else
q.format(part)
end
when ArrayLiteral
contents = part.contents
if contents && contents.parts.length >= 2
# Here there is a single argument that is an array literal with at
# least two elements. We skip directly into the array literal's
# elements in order to print the contents. This would be like if
# we had:
#
# break [1, 2, 3]
#
# which we will print as:
#
# break 1, 2, 3
#
q.text(" ")
format_array_contents(q, part)
else
# Here there is a single argument that is an array literal with 0
# or 1 elements. In this case we're going to print the array as it
# is because skipping the brackets would change the remaining.
# This would be like if we had:
#
# break []
# break [1]
#
q.text(" ")
q.format(part)
end
else
# Here there is a single argument that hasn't matched one of our
# previous cases. We're going to print the argument as it is. This
# would be like if we had:
#
# break foo
#
format_arguments(q, "(", ")")
end
end
end
end
private
def format_array_contents(q, array)
q.if_break { q.text("[") }
q.indent do
q.breakable_empty
q.format(array.contents)
end
q.breakable_empty
q.if_break { q.text("]") }
end
def format_arguments(q, opening, closing)
q.if_break { q.text(opening) }
q.indent do
q.breakable_space
q.format(node.arguments)
end
q.breakable_empty
q.if_break { q.text(closing) }
end
def skip_parens?(node)
case node
when FloatLiteral, Imaginary, Int, RationalLiteral
true
when VarRef
case node.value
when Const, CVar, GVar, IVar, Kw
true
else
false
end
else
false
end
end
end
# Break represents using the +break+ keyword.
#
# break
#
# It can also optionally accept arguments, as in:
#
# break 1
#
class Break < Node
# [Args] the arguments being sent to the keyword
attr_reader :arguments
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(arguments:, location:)
@arguments = arguments
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_break(self)
end
def child_nodes
[arguments]
end
def copy(arguments: nil, location: nil)
node =
Break.new(
arguments: arguments || self.arguments,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ arguments: arguments, location: location, comments: comments }
end
def format(q)
FlowControlFormatter.new("break", self).format(q)
end
def ===(other)
other.is_a?(Break) && arguments === other.arguments
end
end
# Wraps a call operator (which can be a string literal :: or an Op node or a
# Period node) and formats it when called.
class CallOperatorFormatter
# [:"::" | Op | Period] the operator being formatted
attr_reader :operator
def initialize(operator)
@operator = operator
end
def comments
operator == :"::" ? [] : operator.comments
end
def format(q)
case operator
when :"::"
q.text(".")
when Op
operator.value == "::" ? q.text(".") : operator.format(q)
else
operator.format(q)
end
end
end
# This is probably the most complicated formatter in this file. It's
# responsible for formatting chains of method calls, with or without arguments
# or blocks. In general, we want to go from something like
#
# foo.bar.baz
#
# to
#
# foo
# .bar
# .baz
#
# Of course there are a lot of caveats to that, including trailing operators
# when necessary, where comments are places, how blocks are aligned, etc.
class CallChainFormatter
# [CallNode | MethodAddBlock] the top of the call chain
attr_reader :node
def initialize(node)
@node = node
end
def format(q)
children = [node]
threshold = 3
# First, walk down the chain until we get to the point where we're not
# longer at a chainable node.
loop do
case (child = children.last)
when CallNode
case (receiver = child.receiver)
when CallNode
if receiver.receiver.nil?
break
else
children << receiver
end
when MethodAddBlock
if (call = receiver.call).is_a?(CallNode) && !call.receiver.nil?
children << receiver
else
break
end
else
break
end
when MethodAddBlock
if (call = child.call).is_a?(CallNode) && !call.receiver.nil?
children << call
else
break
end
else
break
end
end
# Here, we have very specialized behavior where if we're within a sig
# block, then we're going to assume we're creating a Sorbet type
# signature. In that case, we really want the threshold to be lowered so
# that we create method chains off of any two method calls within the
# block. For more details, see
# https://github.com/prettier/plugin-ruby/issues/863.
parents = q.parents.take(4)
if (parent = parents[2])
# If we're at a block with the `do` keywords, then we want to go one
# more level up. This is because do blocks have BodyStmt nodes instead
# of just Statements nodes.
parent = parents[3] if parent.is_a?(BlockNode) && parent.keywords?
if parent.is_a?(MethodAddBlock) &&
(call = parent.call).is_a?(CallNode) && call.message.value == "sig"
threshold = 2
end
end
if children.length >= threshold
q.group do
q
.if_break { format_chain(q, children) }
.if_flat { node.format_contents(q) }
end
else
node.format_contents(q)
end
end
def format_chain(q, children)
# We're going to have some specialized behavior for if it's an entire
# chain of calls without arguments except for the last one. This is common
# enough in Ruby source code that it's worth the extra complexity here.
empty_except_last =
children
.drop(1)
.all? { |child| child.is_a?(CallNode) && child.arguments.nil? }
# Here, we're going to add all of the children onto the stack of the
# formatter so it's as if we had descending normally into them. This is
# necessary so they can check their parents as normal.
q.stack.concat(children)
q.format(children.last.receiver) if children.last.receiver
q.group do
if attach_directly?(children.last)
format_child(q, children.pop)
q.stack.pop
end
q.indent do
# We track another variable that checks if you need to move the
# operator to the previous line in case there are trailing comments
# and a trailing operator.
skip_operator = false
while (child = children.pop)
if child.is_a?(CallNode)
if (receiver = child.receiver).is_a?(CallNode) &&
(receiver.message != :call) &&
(receiver.message.value == "where") &&
(child.message != :call && child.message.value == "not")
# This is very specialized behavior wherein we group
# .where.not calls together because it looks better. For more
# information, see
# https://github.com/prettier/plugin-ruby/issues/862.
else
# If we're at a Call node and not a MethodAddBlock node in the
# chain then we're going to add a newline so it indents
# properly.
q.breakable_empty
end
end
format_child(
q,
child,
skip_comments: children.empty?,
skip_operator: skip_operator,
skip_attached: empty_except_last && children.empty?
)
# If the parent call node has a comment on the message then we need
# to print the operator trailing in order to keep it working.
last_child = children.last
if last_child.is_a?(CallNode) && last_child.message != :call &&
(
(last_child.message.comments.any? && last_child.operator) ||
(last_child.operator && last_child.operator.comments.any?)
)
q.format(CallOperatorFormatter.new(last_child.operator))
skip_operator = true
else
skip_operator = false
end
# Pop off the formatter's stack so that it aligns with what would
# have happened if we had been formatting normally.
q.stack.pop
end
end
end
if empty_except_last
case node
when CallNode
node.format_arguments(q)
when MethodAddBlock
q.format(node.block)
end
end
end
def self.chained?(node)
return false if ENV["STREE_FAST_FORMAT"]
case node
when CallNode
!node.receiver.nil?
when MethodAddBlock
call = node.call
call.is_a?(CallNode) && !call.receiver.nil?
else
false
end
end
private
# For certain nodes, we want to attach directly to the end and don't
# want to indent the first call. So we'll pop off the first children and
# format it separately here.
def attach_directly?(node)
case node.receiver
when ArrayLiteral, HashLiteral, Heredoc, IfNode, UnlessNode,
XStringLiteral
true
else
false
end
end
def format_child(
q,
child,
skip_comments: false,
skip_operator: false,
skip_attached: false
)
# First, format the actual contents of the child.
case child
when CallNode
q.group do
if !skip_operator && child.operator
q.format(CallOperatorFormatter.new(child.operator))
end
q.format(child.message) if child.message != :call
child.format_arguments(q) unless skip_attached
end
when MethodAddBlock
q.format(child.block) unless skip_attached
end
# If there are any comments on this node then we need to explicitly print
# them out here since we're bypassing the normal comment printing.
if child.comments.any? && !skip_comments
child.comments.each do |comment|
comment.inline? ? q.text(" ") : q.breakable_space
comment.format(q)
end
q.break_parent
end
end
end
# CallNode represents a method call.
#
# receiver.message
#
class CallNode < Node
# [nil | Node] the receiver of the method call
attr_reader :receiver
# [nil | :"::" | Op | Period] the operator being used to send the message
attr_reader :operator
# [:call | Backtick | Const | Ident | Op] the message being sent
attr_reader :message
# [nil | ArgParen | Args] the arguments to the method call
attr_reader :arguments
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(receiver:, operator:, message:, arguments:, location:)
@receiver = receiver
@operator = operator
@message = message
@arguments = arguments
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_call(self)
end
def child_nodes
[
receiver,
(operator if operator != :"::"),
(message if message != :call),
arguments
]
end
def copy(
receiver: nil,
operator: nil,
message: nil,
arguments: nil,
location: nil
)
node =
CallNode.new(
receiver: receiver || self.receiver,
operator: operator || self.operator,
message: message || self.message,
arguments: arguments || self.arguments,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
receiver: receiver,
operator: operator,
message: message,
arguments: arguments,
location: location,
comments: comments
}
end
def format(q)
if receiver
# If we're at the top of a call chain, then we're going to do some
# specialized printing in case we can print it nicely. We _only_ do this
# at the top of the chain to avoid weird recursion issues.
if CallChainFormatter.chained?(receiver) &&
!CallChainFormatter.chained?(q.parent)
q.group do
q
.if_break { CallChainFormatter.new(self).format(q) }
.if_flat { format_contents(q) }
end
else
format_contents(q)
end
else
q.format(message)
# Note that this explicitly leaves parentheses in place even if they are
# empty. There are two reasons we would need to do this. The first is if
# we're calling something that looks like a constant, as in:
#
# Foo()
#
# In this case if we remove the parentheses then this becomes a constant
# reference and not a method call. The second is if we're calling a
# method that is the same name as a local variable that is in scope, as
# in:
#
# foo = foo()
#
# In this case we have to keep the parentheses or else it treats this
# like assigning nil to the local variable. Note that we could attempt
# to be smarter about this by tracking the local variables that are in
# scope, but for now it's simpler and more efficient to just leave the
# parentheses in place.
q.format(arguments) if arguments
end
end
def ===(other)
other.is_a?(CallNode) && receiver === other.receiver &&
operator === other.operator && message === other.message &&
arguments === other.arguments
end
# Print out the arguments to this call. If there are no arguments, then do
# nothing.
def format_arguments(q)
case arguments
when ArgParen
q.format(arguments)
when Args
q.text(" ")
q.format(arguments)
end
end
def format_contents(q)
call_operator = CallOperatorFormatter.new(operator)
q.group do
q.format(receiver)
# If there are trailing comments on the call operator, then we need to
# use the trailing form as opposed to the leading form.
q.format(call_operator) if call_operator.comments.any?
q.group do
q.indent do
if receiver.comments.any? || call_operator.comments.any?
q.breakable_force
end
if call_operator.comments.empty?
q.format(call_operator, stackable: false)
end
q.format(message) if message != :call
end
format_arguments(q)
end
end
end
def arity
arguments&.arity || 0
end
end
# Case represents the beginning of a case chain.
#
# case value
# when 1
# "one"
# when 2
# "two"
# else
# "number"
# end
#
class Case < Node
# [Kw] the keyword that opens this expression
attr_reader :keyword
# [nil | Node] optional value being switched on
attr_reader :value
# [In | When] the next clause in the chain
attr_reader :consequent
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(keyword:, value:, consequent:, location:)
@keyword = keyword
@value = value
@consequent = consequent
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_case(self)
end
def child_nodes
[keyword, value, consequent]
end
def copy(keyword: nil, value: nil, consequent: nil, location: nil)
node =
Case.new(
keyword: keyword || self.keyword,
value: value || self.value,
consequent: consequent || self.consequent,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
keyword: keyword,
value: value,
consequent: consequent,
location: location,
comments: comments
}
end
def format(q)
q.group do
q.format(keyword)
if value
q.text(" ")
q.format(value)
end
q.breakable_force
q.format(consequent)
q.breakable_force
q.text("end")
end
end
def ===(other)
other.is_a?(Case) && keyword === other.keyword && value === other.value &&
consequent === other.consequent
end
end
# RAssign represents a single-line pattern match.
#
# value in pattern
# value => pattern
#
class RAssign < Node
# [Node] the left-hand expression
attr_reader :value
# [Kw | Op] the operator being used to match against the pattern, which is
# either => or in
attr_reader :operator
# [Node] the pattern on the right-hand side of the expression
attr_reader :pattern
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(value:, operator:, pattern:, location:)
@value = value
@operator = operator
@pattern = pattern
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_rassign(self)
end
def child_nodes
[value, operator, pattern]
end
def copy(value: nil, operator: nil, pattern: nil, location: nil)
node =
RAssign.new(
value: value || self.value,
operator: operator || self.operator,
pattern: pattern || self.pattern,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
value: value,
operator: operator,
pattern: pattern,
location: location,
comments: comments
}
end
def format(q)
q.group do
q.format(value)
q.text(" ")
q.format(operator)
case pattern
when AryPtn, FndPtn, HshPtn
q.text(" ")
q.format(pattern)
else
q.group do
q.indent do
q.breakable_space
q.format(pattern)
end
end
end
end
end
def ===(other)
other.is_a?(RAssign) && value === other.value &&
operator === other.operator && pattern === other.pattern
end
end
# Class represents defining a class using the +class+ keyword.
#
# class Container
# end
#
# Classes can have path names as their class name in case it's being nested
# under a namespace, as in:
#
# class Namespace::Container
# end
#
# Classes can also be defined as a top-level path, in the case that it's
# already in a namespace but you want to define it at the top-level instead,
# as in:
#
# module OtherNamespace
# class ::Namespace::Container
# end
# end
#
# All of these declarations can also have an optional superclass reference, as
# in:
#
# class Child < Parent
# end
#
# That superclass can actually be any Ruby expression, it doesn't necessarily
# need to be a constant, as in:
#
# class Child < method
# end
#
class ClassDeclaration < Node
# [ConstPathRef | ConstRef | TopConstRef] the name of the class being
# defined
attr_reader :constant
# [nil | Node] the optional superclass declaration
attr_reader :superclass
# [BodyStmt] the expressions to execute within the context of the class
attr_reader :bodystmt
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(constant:, superclass:, bodystmt:, location:)
@constant = constant
@superclass = superclass
@bodystmt = bodystmt
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_class(self)
end
def child_nodes
[constant, superclass, bodystmt]
end
def copy(constant: nil, superclass: nil, bodystmt: nil, location: nil)
node =
ClassDeclaration.new(
constant: constant || self.constant,
superclass: superclass || self.superclass,
bodystmt: bodystmt || self.bodystmt,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
constant: constant,
superclass: superclass,
bodystmt: bodystmt,
location: location,
comments: comments
}
end
def format(q)
if bodystmt.empty?
q.group do
format_declaration(q)
q.breakable_force
q.text("end")
end
else
q.group do
format_declaration(q)
q.indent do
q.breakable_force
q.format(bodystmt)
end
q.breakable_force
q.text("end")
end
end
end
def ===(other)
other.is_a?(ClassDeclaration) && constant === other.constant &&
superclass === other.superclass && bodystmt === other.bodystmt
end
private
def format_declaration(q)
q.group do
q.text("class ")
q.format(constant)
if superclass
q.text(" < ")
q.format(superclass)
end
end
end
end
# Comma represents the use of the , operator.
class Comma < Node
# [String] the comma in the string
attr_reader :value
def initialize(value:, location:)
@value = value
@location = location
end
def accept(visitor)
visitor.visit_comma(self)
end
def child_nodes
[]
end
def copy(value: nil, location: nil)
Comma.new(value: value || self.value, location: location || self.location)
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location }
end
def ===(other)
other.is_a?(Comma) && value === other.value
end
end
# Command represents a method call with arguments and no parentheses. Note
# that Command nodes only happen when there is no explicit receiver for this
# method.
#
# method argument
#
class Command < Node
# [Const | Ident] the message being sent to the implicit receiver
attr_reader :message
# [Args] the arguments being sent with the message
attr_reader :arguments
# [nil | BlockNode] the optional block being passed to the method
attr_reader :block
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(message:, arguments:, block:, location:)
@message = message
@arguments = arguments
@block = block
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_command(self)
end
def child_nodes
[message, arguments, block]
end
def copy(message: nil, arguments: nil, block: nil, location: nil)
node =
Command.new(
message: message || self.message,
arguments: arguments || self.arguments,
block: block || self.block,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
message: message,
arguments: arguments,
block: block,
location: location,
comments: comments
}
end
def format(q)
q.group do
q.format(message)
align(q, self) { q.format(arguments) }
end
q.format(block) if block
end
def ===(other)
other.is_a?(Command) && message === other.message &&
arguments === other.arguments && block === other.block
end
def arity
arguments.arity
end
private
def align(q, node, &block)
arguments = node.arguments
if arguments.is_a?(Args)
parts = arguments.parts
if parts.size == 1
part = parts.first
case part
when DefNode
q.text(" ")
yield
when IfOp
q.if_flat { q.text(" ") }
yield
when Command
align(q, part, &block)
else
q.text(" ")
q.nest(message.value.length + 1) { yield }
end
else
q.text(" ")
q.nest(message.value.length + 1) { yield }
end
else
q.text(" ")
q.nest(message.value.length + 1) { yield }
end
end
end
# CommandCall represents a method call on an object with arguments and no
# parentheses.
#
# object.method argument
#
class CommandCall < Node
# [nil | Node] the receiver of the message
attr_reader :receiver
# [nil | :"::" | Op | Period] the operator used to send the message
attr_reader :operator
# [:call | Const | Ident | Op] the message being send
attr_reader :message
# [nil | Args | ArgParen] the arguments going along with the message
attr_reader :arguments
# [nil | BlockNode] the block associated with this method call
attr_reader :block
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(
receiver:,
operator:,
message:,
arguments:,
block:,
location:
)
@receiver = receiver
@operator = operator
@message = message
@arguments = arguments
@block = block
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_command_call(self)
end
def child_nodes
[receiver, message, arguments, block]
end
def copy(
receiver: nil,
operator: nil,
message: nil,
arguments: nil,
block: nil,
location: nil
)
node =
CommandCall.new(
receiver: receiver || self.receiver,
operator: operator || self.operator,
message: message || self.message,
arguments: arguments || self.arguments,
block: block || self.block,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
receiver: receiver,
operator: operator,
message: message,
arguments: arguments,
block: block,
location: location,
comments: comments
}
end
def format(q)
message = self.message
arguments = self.arguments
block = self.block
q.group do
doc =
q.nest(0) do
q.format(receiver)
# If there are leading comments on the message then we know we have
# a newline in the source that is forcing these things apart. In
# this case we will have to use a trailing operator.
if message != :call && message.comments.any?(&:leading?)
q.format(CallOperatorFormatter.new(operator), stackable: false)
q.indent do
q.breakable_empty
q.format(message)
end
else
q.format(CallOperatorFormatter.new(operator), stackable: false)
q.format(message)
end
end
# Format the arguments for this command call here. If there are no
# arguments, then print nothing.
if arguments
parts = arguments.parts
if parts.length == 1 && parts.first.is_a?(IfOp)
q.if_flat { q.text(" ") }
q.format(arguments)
else
q.text(" ")
q.nest(argument_alignment(q, doc)) { q.format(arguments) }
end
end
end
q.format(block) if block
end
def ===(other)
other.is_a?(CommandCall) && receiver === other.receiver &&
operator === other.operator && message === other.message &&
arguments === other.arguments && block === other.block
end
def arity
arguments&.arity || 0
end
private
def argument_alignment(q, doc)
# Very special handling case for rspec matchers. In general with rspec
# matchers you expect to see something like:
#
# expect(foo).to receive(:bar).with(
# 'one',
# 'two',
# 'three',
# 'four',
# 'five'
# )
#
# In this case the arguments are aligned to the left side as opposed to
# being aligned with the `receive` call.
if %w[to not_to to_not].include?(message.value)
0
else
width = q.last_position(doc) + 1
width > (q.maxwidth / 2) ? 0 : width
end
end
end
# Comment represents a comment in the source.
#
# # comment
#
class Comment < Node
# [String] the contents of the comment
attr_reader :value
# [boolean] whether or not there is code on the same line as this comment.
# If there is, then inline will be true.
attr_reader :inline
alias inline? inline
def initialize(value:, inline:, location:)
@value = value
@inline = inline
@location = location
@leading = false
@trailing = false
end
def leading!
@leading = true
end
def leading?
@leading
end
def trailing!
@trailing = true
end
def trailing?
@trailing
end
def ignore?
value.match?(/\A#\s*stree-ignore\s*\z/)
end
def comments
[]
end
def accept(visitor)
visitor.visit_comment(self)
end
def child_nodes
[]
end
def copy(value: nil, inline: nil, location: nil)
Comment.new(
value: value || self.value,
inline: inline || self.inline,
location: location || self.location
)
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, inline: inline, location: location }
end
def format(q)
q.text(value)
end
def ===(other)
other.is_a?(Comment) && value === other.value && inline === other.inline
end
end
# Const represents a literal value that _looks_ like a constant. This could
# actually be a reference to a constant:
#
# Constant
#
# It could also be something that looks like a constant in another context, as
# in a method call to a capitalized method:
#
# object.Constant
#
# or a symbol that starts with a capital letter:
#
# :Constant
#
class Const < Node
# [String] the name of the constant
attr_reader :value
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(value:, location:)
@value = value
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_const(self)
end
def child_nodes
[]
end
def copy(value: nil, location: nil)
node =
Const.new(
value: value || self.value,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location, comments: comments }
end
def format(q)
q.text(value)
end
def ===(other)
other.is_a?(Const) && value === other.value
end
end
# ConstPathField represents the child node of some kind of assignment. It
# represents when you're assigning to a constant that is being referenced as
# a child of another variable.
#
# object::Const = value
#
class ConstPathField < Node
# [Node] the source of the constant
attr_reader :parent
# [Const] the constant itself
attr_reader :constant
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(parent:, constant:, location:)
@parent = parent
@constant = constant
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_const_path_field(self)
end
def child_nodes
[parent, constant]
end
def copy(parent: nil, constant: nil, location: nil)
node =
ConstPathField.new(
parent: parent || self.parent,
constant: constant || self.constant,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
parent: parent,
constant: constant,
location: location,
comments: comments
}
end
def format(q)
q.format(parent)
q.text("::")
q.format(constant)
end
def ===(other)
other.is_a?(ConstPathField) && parent === other.parent &&
constant === other.constant
end
end
# ConstPathRef represents referencing a constant by a path.
#
# object::Const
#
class ConstPathRef < Node
# [Node] the source of the constant
attr_reader :parent
# [Const] the constant itself
attr_reader :constant
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(parent:, constant:, location:)
@parent = parent
@constant = constant
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_const_path_ref(self)
end
def child_nodes
[parent, constant]
end
def copy(parent: nil, constant: nil, location: nil)
node =
ConstPathRef.new(
parent: parent || self.parent,
constant: constant || self.constant,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
parent: parent,
constant: constant,
location: location,
comments: comments
}
end
def format(q)
q.format(parent)
q.text("::")
q.format(constant)
end
def ===(other)
other.is_a?(ConstPathRef) && parent === other.parent &&
constant === other.constant
end
end
# ConstRef represents the name of the constant being used in a class or module
# declaration.
#
# class Container
# end
#
class ConstRef < Node
# [Const] the constant itself
attr_reader :constant
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(constant:, location:)
@constant = constant
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_const_ref(self)
end
def child_nodes
[constant]
end
def copy(constant: nil, location: nil)
node =
ConstRef.new(
constant: constant || self.constant,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ constant: constant, location: location, comments: comments }
end
def format(q)
q.format(constant)
end
def ===(other)
other.is_a?(ConstRef) && constant === other.constant
end
end
# CVar represents the use of a class variable.
#
# @@variable
#
class CVar < Node
# [String] the name of the class variable
attr_reader :value
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(value:, location:)
@value = value
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_cvar(self)
end
def child_nodes
[]
end
def copy(value: nil, location: nil)
node =
CVar.new(
value: value || self.value,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location, comments: comments }
end
def format(q)
q.text(value)
end
def ===(other)
other.is_a?(CVar) && value === other.value
end
end
# Def represents defining a regular method on the current self object.
#
# def method(param) result end
# def object.method(param) result end
#
class DefNode < Node
# [nil | Node] the target where the method is being defined
attr_reader :target
# [nil | Op | Period] the operator being used to declare the method
attr_reader :operator
# [Backtick | Const | Ident | Kw | Op] the name of the method
attr_reader :name
# [nil | Params | Paren] the parameter declaration for the method
attr_reader :params
# [BodyStmt | Node] the expressions to be executed by the method
attr_reader :bodystmt
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(target:, operator:, name:, params:, bodystmt:, location:)
@target = target
@operator = operator
@name = name
@params = params
@bodystmt = bodystmt
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_def(self)
end
def child_nodes
[target, operator, name, params, bodystmt]
end
def copy(
target: nil,
operator: nil,
name: nil,
params: nil,
bodystmt: nil,
location: nil
)
node =
DefNode.new(
target: target || self.target,
operator: operator || self.operator,
name: name || self.name,
params: params || self.params,
bodystmt: bodystmt || self.bodystmt,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
target: target,
operator: operator,
name: name,
params: params,
bodystmt: bodystmt,
location: location,
comments: comments
}
end
def format(q)
params = self.params
bodystmt = self.bodystmt
q.group do
q.group do
q.text("def")
q.text(" ") if target || name.comments.empty?
if target
q.format(target)
q.format(CallOperatorFormatter.new(operator), stackable: false)
end
q.format(name)
case params
when Paren
q.format(params)
when Params
q.format(params) if !params.empty? || params.comments.any?
end
end
if endless?
q.text(" =")
q.group do
q.indent do
q.breakable_space
q.format(bodystmt)
end
end
else
unless bodystmt.empty?
q.indent do
q.breakable_force
q.format(bodystmt)
end
end
q.breakable_force
q.text("end")
end
end
end
def ===(other)
other.is_a?(DefNode) && target === other.target &&
operator === other.operator && name === other.name &&
params === other.params && bodystmt === other.bodystmt
end
# Returns true if the method was found in the source in the "endless" form,
# i.e. where the method body is defined using the `=` operator after the
# method name and parameters.
def endless?
!bodystmt.is_a?(BodyStmt)
end
def arity
params = self.params
case params
when Params
params.arity
when Paren
params.contents.arity
else
0..0
end
end
end
# Defined represents the use of the +defined?+ operator. It can be used with
# and without parentheses.
#
# defined?(variable)
#
class Defined < Node
# [Node] the value being sent to the keyword
attr_reader :value
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(value:, location:)
@value = value
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_defined(self)
end
def child_nodes
[value]
end
def copy(value: nil, location: nil)
node =
Defined.new(
value: value || self.value,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location, comments: comments }
end
def format(q)
q.text("defined?(")
q.group do
q.indent do
q.breakable_empty
q.format(value)
end
q.breakable_empty
end
q.text(")")
end
def ===(other)
other.is_a?(Defined) && value === other.value
end
end
# Block represents passing a block to a method call using the +do+ and +end+
# keywords or the +{+ and +}+ operators.
#
# method do |value|
# end
#
# method { |value| }
#
class BlockNode < Node
# Formats the opening brace or keyword of a block.
class BlockOpenFormatter
# [String] the actual output that should be printed
attr_reader :text
# [LBrace | Keyword] the node that is being represented
attr_reader :node
def initialize(text, node)
@text = text
@node = node
end
def comments
node.comments
end
def format(q)
q.text(text)
end
end
# [LBrace | Kw] the left brace or the do keyword that opens this block
attr_reader :opening
# [nil | BlockVar] the optional variable declaration within this block
attr_reader :block_var
# [BodyStmt | Statements] the expressions to be executed within this block
attr_reader :bodystmt
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(opening:, block_var:, bodystmt:, location:)
@opening = opening
@block_var = block_var
@bodystmt = bodystmt
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_block(self)
end
def child_nodes
[opening, block_var, bodystmt]
end
def copy(opening: nil, block_var: nil, bodystmt: nil, location: nil)
node =
BlockNode.new(
opening: opening || self.opening,
block_var: block_var || self.block_var,
bodystmt: bodystmt || self.bodystmt,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
opening: opening,
block_var: block_var,
bodystmt: bodystmt,
location: location,
comments: comments
}
end
def format(q)
# If this is nested anywhere inside of a Command or CommandCall node, then
# we can't change which operators we're using for the bounds of the block.
break_opening, break_closing, flat_opening, flat_closing =
if unchangeable_bounds?(q)
block_close = keywords? ? "end" : "}"
[opening.value, block_close, opening.value, block_close]
elsif forced_do_end_bounds?(q)
%w[do end do end]
elsif forced_brace_bounds?(q)
%w[{ } { }]
else
%w[do end { }]
end
# If the receiver of this block a Command or CommandCall node, then there
# are no parentheses around the arguments to that command, so we need to
# break the block.
case q.parent
when nil, Command, CommandCall
q.break_parent
format_break(q, break_opening, break_closing)
return
end
q.group do
q
.if_break { format_break(q, break_opening, break_closing) }
.if_flat { format_flat(q, flat_opening, flat_closing) }
end
end
def ===(other)
other.is_a?(BlockNode) && opening === other.opening &&
block_var === other.block_var && bodystmt === other.bodystmt
end
def keywords?
opening.is_a?(Kw)
end
def arity
case block_var
when BlockVar
block_var.params.arity
else
0..0
end
end
private
# If this is nested anywhere inside certain nodes, then we can't change
# which operators/keywords we're using for the bounds of the block.
def unchangeable_bounds?(q)
q.parents.any? do |parent|
# If we hit a statements, then we're safe to use whatever since we
# know for certain we're going to get split over multiple lines
# anyway.
case parent
when Statements, ArgParen
break false
when Command, CommandCall
true
else
false
end
end
end
# If we're a sibling of a control-flow keyword, then we're going to have to
# use the do..end bounds.
def forced_do_end_bounds?(q)
case q.parent&.call
when Break, Next, ReturnNode, Super
true
else
false
end
end
# If we're the predicate of a loop or conditional, then we're going to have
# to go with the {..} bounds.
def forced_brace_bounds?(q)
previous = nil
q.parents.any? do |parent|
case parent
when Paren, Statements
# If we hit certain breakpoints then we know we're safe.
return false
when IfNode, IfOp, UnlessNode, WhileNode, UntilNode
return true if parent.predicate == previous
end
previous = parent
false
end
end
def format_break(q, break_opening, break_closing)
q.text(" ")
q.format(BlockOpenFormatter.new(break_opening, opening), stackable: false)
if block_var
q.text(" ")
q.format(block_var)
end
unless bodystmt.empty?
q.indent do
q.breakable_space
q.format(bodystmt)
end
end
q.breakable_space
q.text(break_closing)
end
def format_flat(q, flat_opening, flat_closing)
q.text(" ")
q.format(BlockOpenFormatter.new(flat_opening, opening), stackable: false)
if block_var
q.breakable_space
q.format(block_var)
q.breakable_space
end
if bodystmt.empty?
q.text(" ") if flat_opening == "do"
else
q.breakable_space unless block_var
q.format(bodystmt)
q.breakable_space
end
q.text(flat_closing)
end
end
# RangeNode represents using the .. or the ... operator between two
# expressions. Usually this is to create a range object.
#
# 1..2
#
# Sometimes this operator is used to create a flip-flop.
#
# if value == 5 .. value == 10
# end
#
# One of the sides of the expression may be nil, but not both.
class RangeNode < Node
# [nil | Node] the left side of the expression
attr_reader :left
# [Op] the operator used for this range
attr_reader :operator
# [nil | Node] the right side of the expression
attr_reader :right
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(left:, operator:, right:, location:)
@left = left
@operator = operator
@right = right
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_range(self)
end
def child_nodes
[left, right]
end
def copy(left: nil, operator: nil, right: nil, location: nil)
node =
RangeNode.new(
left: left || self.left,
operator: operator || self.operator,
right: right || self.right,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
left: left,
operator: operator,
right: right,
location: location,
comments: comments
}
end
def format(q)
q.format(left) if left
case q.parent
when IfNode, UnlessNode
q.text(" #{operator.value} ")
else
q.text(operator.value)
end
q.format(right) if right
end
def ===(other)
other.is_a?(RangeNode) && left === other.left &&
operator === other.operator && right === other.right
end
end
# Responsible for providing information about quotes to be used for strings
# and dynamic symbols.
module Quotes
# The matching pairs of quotes that can be used with % literals.
PAIRS = { "(" => ")", "[" => "]", "{" => "}", "<" => ">" }.freeze
# If there is some part of this string that matches an escape sequence or
# that contains the interpolation pattern ("#{"), then we are locked into
# whichever quote the user chose. (If they chose single quotes, then double
# quoting would activate the escape sequence, and if they chose double
# quotes, then single quotes would deactivate it.)
def self.locked?(node, quote)
node.parts.any? do |part|
!part.is_a?(TStringContent) || part.value.match?(/\\|#[@${]|#{quote}/)
end
end
# Find the matching closing quote for the given opening quote.
def self.matching(quote)
PAIRS.fetch(quote) { quote }
end
# Escape and unescape single and double quotes as needed to be able to
# enclose +content+ with +enclosing+.
def self.normalize(content, enclosing)
return content if enclosing != "\"" && enclosing != "'"
content.gsub(/\\([\s\S])|(['"])/) do
_match, escaped, quote = Regexp.last_match.to_a
if quote == enclosing
"\\#{quote}"
elsif quote
quote
else
"\\#{escaped}"
end
end
end
end
# DynaSymbol represents a symbol literal that uses quotes to dynamically
# define its value.
#
# :"#{variable}"
#
# They can also be used as a special kind of dynamic hash key, as in:
#
# { "#{key}": value }
#
class DynaSymbol < Node
# [Array[ StringDVar | StringEmbExpr | TStringContent ]] the parts of the
# dynamic symbol
attr_reader :parts
# [nil | String] the quote used to delimit the dynamic symbol
attr_reader :quote
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(parts:, quote:, location:)
@parts = parts
@quote = quote
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_dyna_symbol(self)
end
def child_nodes
parts
end
def copy(parts: nil, quote: nil, location: nil)
node =
DynaSymbol.new(
parts: parts || self.parts,
quote: quote || self.quote,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ parts: parts, quote: quote, location: location, comments: comments }
end
def format(q)
opening_quote, closing_quote = quotes(q)
q.text(opening_quote)
q.group do
parts.each do |part|
if part.is_a?(TStringContent)
value = Quotes.normalize(part.value, closing_quote)
first = true
value.each_line(chomp: true) do |line|
if first
first = false
else
q.breakable_return
end
q.text(line)
end
q.breakable_return if value.end_with?("\n")
else
q.format(part)
end
end
end
q.text(closing_quote)
end
def ===(other)
other.is_a?(DynaSymbol) && ArrayMatch.call(parts, other.parts) &&
quote === other.quote
end
private
# Here we determine the quotes to use for a dynamic symbol. It's bound by a
# lot of rules because it could be in many different contexts with many
# different kinds of escaping.
def quotes(q)
# If we're inside of an assoc node as the key, then it will handle
# printing the : on its own since it could change sides.
parent = q.parent
hash_key = parent.is_a?(Assoc) && parent.key == self
if quote.start_with?("%s")
# Here we're going to check if there is a closing character, a new line,
# or a quote in the content of the dyna symbol. If there is, then
# quoting could get weird, so just bail out and stick to the original
# quotes in the source.
matching = Quotes.matching(quote[2])
pattern = /[\n#{Regexp.escape(matching)}'"]/
# This check is to ensure we don't find a matching quote inside of the
# symbol that would be confusing.
matched =
parts.any? do |part|
part.is_a?(TStringContent) && part.value.match?(pattern)
end
if matched
[quote, matching]
elsif Quotes.locked?(self, q.quote)
["#{":" unless hash_key}'", "'"]
else
["#{":" unless hash_key}#{q.quote}", q.quote]
end
elsif Quotes.locked?(self, q.quote)
if quote.start_with?(":")
[hash_key ? quote[1..] : quote, quote[1..]]
else
[hash_key ? quote : ":#{quote}", quote]
end
else
[hash_key ? q.quote : ":#{q.quote}", q.quote]
end
end
end
# Else represents the end of an +if+, +unless+, or +case+ chain.
#
# if variable
# else
# end
#
class Else < Node
# [Kw] the else keyword
attr_reader :keyword
# [Statements] the expressions to be executed
attr_reader :statements
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(keyword:, statements:, location:)
@keyword = keyword
@statements = statements
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_else(self)
end
def child_nodes
[keyword, statements]
end
def copy(keyword: nil, statements: nil, location: nil)
node =
Else.new(
keyword: keyword || self.keyword,
statements: statements || self.statements,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
keyword: keyword,
statements: statements,
location: location,
comments: comments
}
end
def format(q)
q.group do
q.format(keyword)
unless statements.empty?
q.indent do
q.breakable_force
q.format(statements)
end
end
end
end
def ===(other)
other.is_a?(Else) && keyword === other.keyword &&
statements === other.statements
end
end
# Elsif represents another clause in an +if+ or +unless+ chain.
#
# if variable
# elsif other_variable
# end
#
class Elsif < Node
# [Node] the expression to be checked
attr_reader :predicate
# [Statements] the expressions to be executed
attr_reader :statements
# [nil | Elsif | Else] the next clause in the chain
attr_reader :consequent
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(predicate:, statements:, consequent:, location:)
@predicate = predicate
@statements = statements
@consequent = consequent
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_elsif(self)
end
def child_nodes
[predicate, statements, consequent]
end
def copy(predicate: nil, statements: nil, consequent: nil, location: nil)
node =
Elsif.new(
predicate: predicate || self.predicate,
statements: statements || self.statements,
consequent: consequent || self.consequent,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
predicate: predicate,
statements: statements,
consequent: consequent,
location: location,
comments: comments
}
end
def format(q)
q.group do
q.group do
q.text("elsif ")
q.nest("elsif".length - 1) { q.format(predicate) }
end
unless statements.empty?
q.indent do
q.breakable_force
q.format(statements)
end
end
if consequent
q.group do
q.breakable_force
q.format(consequent)
end
end
end
end
def ===(other)
other.is_a?(Elsif) && predicate === other.predicate &&
statements === other.statements && consequent === other.consequent
end
end
# EmbDoc represents a multi-line comment.
#
# =begin
# first line
# second line
# =end
#
class EmbDoc < Node
# [String] the contents of the comment
attr_reader :value
def initialize(value:, location:)
@value = value
@location = location
@leading = false
@trailing = false
end
def leading!
@leading = true
end
def leading?
@leading
end
def trailing!
@trailing = true
end
def trailing?
@trailing
end
def inline?
false
end
def ignore?
false
end
def comments
[]
end
def accept(visitor)
visitor.visit_embdoc(self)
end
def child_nodes
[]
end
def copy(value: nil, location: nil)
EmbDoc.new(
value: value || self.value,
location: location || self.location
)
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location }
end
def format(q)
if (q.parent.is_a?(DefNode) && q.parent.endless?) ||
q.parent.is_a?(Statements)
q.trim
else
q.breakable_return
end
q.text(value)
end
def ===(other)
other.is_a?(EmbDoc) && value === other.value
end
end
# EmbExprBeg represents the beginning token for using interpolation inside of
# a parent node that accepts string content (like a string or regular
# expression).
#
# "Hello, #{person}!"
#
class EmbExprBeg < Node
# [String] the #{ used in the string
attr_reader :value
def initialize(value:, location:)
@value = value
@location = location
end
def accept(visitor)
visitor.visit_embexpr_beg(self)
end
def child_nodes
[]
end
def copy(value: nil, location: nil)
EmbExprBeg.new(
value: value || self.value,
location: location || self.location
)
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location }
end
def ===(other)
other.is_a?(EmbExprBeg) && value === other.value
end
end
# EmbExprEnd represents the ending token for using interpolation inside of a
# parent node that accepts string content (like a string or regular
# expression).
#
# "Hello, #{person}!"
#
class EmbExprEnd < Node
# [String] the } used in the string
attr_reader :value
def initialize(value:, location:)
@value = value
@location = location
end
def accept(visitor)
visitor.visit_embexpr_end(self)
end
def child_nodes
[]
end
def copy(value: nil, location: nil)
EmbExprEnd.new(
value: value || self.value,
location: location || self.location
)
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location }
end
def ===(other)
other.is_a?(EmbExprEnd) && value === other.value
end
end
# EmbVar represents the use of shorthand interpolation for an instance, class,
# or global variable into a parent node that accepts string content (like a
# string or regular expression).
#
# "#@variable"
#
# In the example above, an EmbVar node represents the # because it forces
# @variable to be interpolated.
class EmbVar < Node
# [String] the # used in the string
attr_reader :value
def initialize(value:, location:)
@value = value
@location = location
end
def accept(visitor)
visitor.visit_embvar(self)
end
def child_nodes
[]
end
def copy(value: nil, location: nil)
EmbVar.new(
value: value || self.value,
location: location || self.location
)
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location }
end
def ===(other)
other.is_a?(EmbVar) && value === other.value
end
end
# Ensure represents the use of the +ensure+ keyword and its subsequent
# statements.
#
# begin
# ensure
# end
#
class Ensure < Node
# [Kw] the ensure keyword that began this node
attr_reader :keyword
# [Statements] the expressions to be executed
attr_reader :statements
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(keyword:, statements:, location:)
@keyword = keyword
@statements = statements
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_ensure(self)
end
def child_nodes
[keyword, statements]
end
def copy(keyword: nil, statements: nil, location: nil)
node =
Ensure.new(
keyword: keyword || self.keyword,
statements: statements || self.statements,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
keyword: keyword,
statements: statements,
location: location,
comments: comments
}
end
def format(q)
q.format(keyword)
unless statements.empty?
q.indent do
q.breakable_force
q.format(statements)
end
end
end
def ===(other)
other.is_a?(Ensure) && keyword === other.keyword &&
statements === other.statements
end
end
# ExcessedComma represents a trailing comma in a list of block parameters. It
# changes the block parameters such that they will destructure.
#
# [[1, 2, 3], [2, 3, 4]].each do |first, second,|
# end
#
# In the above example, an ExcessedComma node would appear in the third
# position of the Params node that is used to declare that block. The third
# position typically represents a rest-type parameter, but in this case is
# used to indicate that a trailing comma was used.
class ExcessedComma < Node
# [String] the comma
attr_reader :value
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(value:, location:)
@value = value
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_excessed_comma(self)
end
def child_nodes
[]
end
def copy(value: nil, location: nil)
node =
ExcessedComma.new(
value: value || self.value,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location, comments: comments }
end
def format(q)
q.text(value)
end
def ===(other)
other.is_a?(ExcessedComma) && value === other.value
end
end
# Field is always the child of an assignment. It represents assigning to a
# “field” on an object.
#
# object.variable = value
#
class Field < Node
# [Node] the parent object that owns the field being assigned
attr_reader :parent
# [:"::" | Op | Period] the operator being used for the assignment
attr_reader :operator
# [Const | Ident] the name of the field being assigned
attr_reader :name
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(parent:, operator:, name:, location:)
@parent = parent
@operator = operator
@name = name
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_field(self)
end
def child_nodes
operator = self.operator
[parent, (operator if operator != :"::"), name]
end
def copy(parent: nil, operator: nil, name: nil, location: nil)
node =
Field.new(
parent: parent || self.parent,
operator: operator || self.operator,
name: name || self.name,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
parent: parent,
operator: operator,
name: name,
location: location,
comments: comments
}
end
def format(q)
q.group do
q.format(parent)
q.format(CallOperatorFormatter.new(operator), stackable: false)
q.format(name)
end
end
def ===(other)
other.is_a?(Field) && parent === other.parent &&
operator === other.operator && name === other.name
end
end
# FloatLiteral represents a floating point number literal.
#
# 1.0
#
class FloatLiteral < Node
# [String] the value of the floating point number literal
attr_reader :value
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(value:, location:)
@value = value
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_float(self)
end
def child_nodes
[]
end
def copy(value: nil, location: nil)
node =
FloatLiteral.new(
value: value || self.value,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location, comments: comments }
end
def format(q)
q.text(value)
end
def ===(other)
other.is_a?(FloatLiteral) && value === other.value
end
end
# FndPtn represents matching against a pattern where you find a pattern in an
# array using the Ruby 3.0+ pattern matching syntax.
#
# case value
# in [*, 7, *]
# end
#
class FndPtn < Node
# [nil | VarRef | ConstPathRef] the optional constant wrapper
attr_reader :constant
# [VarField] the splat on the left-hand side
attr_reader :left
# [Array[ Node ]] the list of positional expressions in the pattern that
# are being matched
attr_reader :values
# [VarField] the splat on the right-hand side
attr_reader :right
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(constant:, left:, values:, right:, location:)
@constant = constant
@left = left
@values = values
@right = right
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_fndptn(self)
end
def child_nodes
[constant, left, *values, right]
end
def copy(constant: nil, left: nil, values: nil, right: nil, location: nil)
node =
FndPtn.new(
constant: constant || self.constant,
left: left || self.left,
values: values || self.values,
right: right || self.right,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
constant: constant,
left: left,
values: values,
right: right,
location: location,
comments: comments
}
end
def format(q)
q.format(constant) if constant
q.group do
q.text("[")
q.indent do
q.breakable_empty
q.text("*")
q.format(left)
q.comma_breakable
q.seplist(values) { |value| q.format(value) }
q.comma_breakable
q.text("*")
q.format(right)
end
q.breakable_empty
q.text("]")
end
end
def ===(other)
other.is_a?(FndPtn) && constant === other.constant &&
left === other.left && ArrayMatch.call(values, other.values) &&
right === other.right
end
end
# For represents using a +for+ loop.
#
# for value in list do
# end
#
class For < Node
# [MLHS | VarField] the variable declaration being used to
# pull values out of the object being enumerated
attr_reader :index
# [Node] the object being enumerated in the loop
attr_reader :collection
# [Statements] the statements to be executed
attr_reader :statements
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(index:, collection:, statements:, location:)
@index = index
@collection = collection
@statements = statements
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_for(self)
end
def child_nodes
[index, collection, statements]
end
def copy(index: nil, collection: nil, statements: nil, location: nil)
node =
For.new(
index: index || self.index,
collection: collection || self.collection,
statements: statements || self.statements,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
index: index,
collection: collection,
statements: statements,
location: location,
comments: comments
}
end
def format(q)
q.group do
q.text("for ")
q.group { q.format(index) }
q.text(" in ")
q.format(collection)
unless statements.empty?
q.indent do
q.breakable_force
q.format(statements)
end
end
q.breakable_force
q.text("end")
end
end
def ===(other)
other.is_a?(For) && index === other.index &&
collection === other.collection && statements === other.statements
end
end
# GVar represents a global variable literal.
#
# $variable
#
class GVar < Node
# [String] the name of the global variable
attr_reader :value
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(value:, location:)
@value = value
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_gvar(self)
end
def child_nodes
[]
end
def copy(value: nil, location: nil)
node =
GVar.new(
value: value || self.value,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location, comments: comments }
end
def format(q)
q.text(value)
end
def ===(other)
other.is_a?(GVar) && value === other.value
end
end
# HashLiteral represents a hash literal.
#
# { key => value }
#
class HashLiteral < Node
# This is a special formatter used if the hash literal contains no values
# but _does_ contain comments. In this case we do some special formatting to
# make sure the comments gets indented properly.
class EmptyWithCommentsFormatter
# [LBrace] the opening brace
attr_reader :lbrace
def initialize(lbrace)
@lbrace = lbrace
end
def format(q)
q.group do
q.text("{")
q.indent do
lbrace.comments.each do |comment|
q.breakable_force
comment.format(q)
end
end
q.breakable_force
q.text("}")
end
end
end
# [LBrace] the left brace that opens this hash
attr_reader :lbrace
# [Array[ Assoc | AssocSplat ]] the optional contents of the hash
attr_reader :assocs
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(lbrace:, assocs:, location:)
@lbrace = lbrace
@assocs = assocs
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_hash(self)
end
def child_nodes
[lbrace].concat(assocs)
end
def copy(lbrace: nil, assocs: nil, location: nil)
node =
HashLiteral.new(
lbrace: lbrace || self.lbrace,
assocs: assocs || self.assocs,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ lbrace: lbrace, assocs: assocs, location: location, comments: comments }
end
def format(q)
if q.parent.is_a?(Assoc)
format_contents(q)
else
q.group { format_contents(q) }
end
end
def ===(other)
other.is_a?(HashLiteral) && lbrace === other.lbrace &&
ArrayMatch.call(assocs, other.assocs)
end
def format_key(q, key)
(@key_formatter ||= HashKeyFormatter.for(self)).format_key(q, key)
end
private
# If we have an empty hash that contains only comments, then we're going
# to do some special printing to ensure they get indented correctly.
def empty_with_comments?
assocs.empty? && lbrace.comments.any? && lbrace.comments.none?(&:inline?)
end
def format_contents(q)
if empty_with_comments?
EmptyWithCommentsFormatter.new(lbrace).format(q)
return
end
q.format(lbrace)
if assocs.empty?
q.breakable_empty
else
q.indent do
q.breakable_space
q.seplist(assocs) { |assoc| q.format(assoc) }
q.if_break { q.text(",") } if q.trailing_comma?
end
q.breakable_space
end
q.text("}")
end
end
# Heredoc represents a heredoc string literal.
#
# <<~DOC
# contents
# DOC
#
class Heredoc < Node
# [HeredocBeg] the opening of the heredoc
attr_reader :beginning
# [HeredocEnd] the ending of the heredoc
attr_reader :ending
# [Integer] how far to dedent the heredoc
attr_reader :dedent
# [Array[ StringEmbExpr | StringDVar | TStringContent ]] the parts of the
# heredoc string literal
attr_reader :parts
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(beginning:, location:, ending: nil, dedent: 0, parts: [])
@beginning = beginning
@ending = ending
@dedent = dedent
@parts = parts
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_heredoc(self)
end
def child_nodes
[beginning, *parts, ending]
end
def copy(beginning: nil, location: nil, ending: nil, parts: nil)
node =
Heredoc.new(
beginning: beginning || self.beginning,
location: location || self.location,
ending: ending || self.ending,
parts: parts || self.parts
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
beginning: beginning,
location: location,
ending: ending,
parts: parts,
comments: comments
}
end
# This is a very specific behavior where you want to force a newline, but
# don't want to force the break parent.
SEPARATOR =
PrettierPrint::Breakable.new(" ", 1, indent: false, force: true).freeze
def format(q)
q.group do
q.format(beginning)
q.line_suffix(priority: Formatter::HEREDOC_PRIORITY) do
q.group do
q.target << SEPARATOR
parts.each do |part|
if part.is_a?(TStringContent)
value = part.value
first = true
value.each_line(chomp: true) do |line|
if first
first = false
else
q.target << SEPARATOR
end
q.text(line)
end
q.target << SEPARATOR if value.end_with?("\n")
else
q.format(part)
end
end
q.format(ending)
end
end
end
end
def ===(other)
other.is_a?(Heredoc) && beginning === other.beginning &&
ending === other.ending && ArrayMatch.call(parts, other.parts)
end
end
# HeredocBeg represents the beginning declaration of a heredoc.
#
# <<~DOC
# contents
# DOC
#
# In the example above the HeredocBeg node represents <<~DOC.
class HeredocBeg < Node
# [String] the opening declaration of the heredoc
attr_reader :value
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(value:, location:)
@value = value
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_heredoc_beg(self)
end
def child_nodes
[]
end
def copy(value: nil, location: nil)
node =
HeredocBeg.new(
value: value || self.value,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location, comments: comments }
end
def format(q)
q.text(value)
end
def ===(other)
other.is_a?(HeredocBeg) && value === other.value
end
end
# HeredocEnd represents the closing declaration of a heredoc.
#
# <<~DOC
# contents
# DOC
#
# In the example above the HeredocEnd node represents the closing DOC.
class HeredocEnd < Node
# [String] the closing declaration of the heredoc
attr_reader :value
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(value:, location:)
@value = value
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_heredoc_end(self)
end
def child_nodes
[]
end
def copy(value: nil, location: nil)
node =
HeredocEnd.new(
value: value || self.value,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location, comments: comments }
end
def format(q)
q.text(value)
end
def ===(other)
other.is_a?(HeredocEnd) && value === other.value
end
end
# HshPtn represents matching against a hash pattern using the Ruby 2.7+
# pattern matching syntax.
#
# case value
# in { key: }
# end
#
class HshPtn < Node
# Formats a key-value pair in a hash pattern. The value is optional.
class KeywordFormatter
# [Label] the keyword being used
attr_reader :key
# [Node] the optional value for the keyword
attr_reader :value
def initialize(key, value)
@key = key
@value = value
end
def comments
[]
end
def format(q)
HashKeyFormatter::Labels.new.format_key(q, key)
if value
q.text(" ")
q.format(value)
end
end
end
# Formats the optional double-splat from the pattern.
class KeywordRestFormatter
# [VarField] the parameter that matches the remaining keywords
attr_reader :keyword_rest
def initialize(keyword_rest)
@keyword_rest = keyword_rest
end
def comments
[]
end
def format(q)
q.text("**")
q.format(keyword_rest)
end
end
# [nil | VarRef | ConstPathRef] the optional constant wrapper
attr_reader :constant
# [Array[ [DynaSymbol | Label, nil | Node] ]] the set of tuples
# representing the keywords that should be matched against in the pattern
attr_reader :keywords
# [nil | VarField] an optional parameter to gather up all remaining keywords
attr_reader :keyword_rest
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(constant:, keywords:, keyword_rest:, location:)
@constant = constant
@keywords = keywords
@keyword_rest = keyword_rest
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_hshptn(self)
end
def child_nodes
[constant, *keywords.flatten(1), keyword_rest]
end
def copy(constant: nil, keywords: nil, keyword_rest: nil, location: nil)
node =
HshPtn.new(
constant: constant || self.constant,
keywords: keywords || self.keywords,
keyword_rest: keyword_rest || self.keyword_rest,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
constant: constant,
keywords: keywords,
keyword_rest: keyword_rest,
location: location,
comments: comments
}
end
def format(q)
parts = keywords.map { |(key, value)| KeywordFormatter.new(key, value) }
parts << KeywordRestFormatter.new(keyword_rest) if keyword_rest
nested = PATTERNS.include?(q.parent.class)
# If there is a constant, we're going to format to have the constant name
# first and then use brackets.
if constant
q.group do
q.format(constant)
q.text("[")
q.indent do
q.breakable_empty
format_contents(q, parts, nested)
end
q.breakable_empty
q.text("]")
end
return
end
# If there's nothing at all, then we're going to use empty braces.
if parts.empty?
q.text("{}")
return
end
# If there's only one pair, then we'll just print the contents provided
# we're not inside another pattern.
if !nested && parts.size == 1
format_contents(q, parts, nested)
return
end
# Otherwise, we're going to always use braces to make it clear it's a hash
# pattern.
q.group do
q.text("{")
q.indent do
q.breakable_space
format_contents(q, parts, nested)
end
if q.target_ruby_version < Formatter::SemanticVersion.new("2.7.3")
q.text(" }")
else
q.breakable_space
q.text("}")
end
end
end
def ===(other)
other.is_a?(HshPtn) && constant === other.constant &&
keywords.length == other.keywords.length &&
keywords
.zip(other.keywords)
.all? { |left, right| ArrayMatch.call(left, right) } &&
keyword_rest === other.keyword_rest
end
private
def format_contents(q, parts, nested)
keyword_rest = self.keyword_rest
q.group { q.seplist(parts) { |part| q.format(part, stackable: false) } }
# If there isn't a constant, and there's a blank keyword_rest, then we
# have an plain ** that needs to have a `then` after it in order to
# parse correctly on the next parse.
if !constant && keyword_rest && keyword_rest.value.nil? && !nested
q.text(" then")
end
end
end
# The list of nodes that represent patterns inside of pattern matching so that
# when a pattern is being printed it knows if it's nested.
PATTERNS = [AryPtn, Binary, FndPtn, HshPtn, RAssign].freeze
# Ident represents an identifier anywhere in code. It can represent a very
# large number of things, depending on where it is in the syntax tree.
#
# value
#
class Ident < Node
# [String] the value of the identifier
attr_reader :value
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(value:, location:)
@value = value
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_ident(self)
end
def child_nodes
[]
end
def copy(value: nil, location: nil)
node =
Ident.new(
value: value || self.value,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location, comments: comments }
end
def format(q)
q.text(value)
end
def ===(other)
other.is_a?(Ident) && value === other.value
end
end
# If the predicate of a conditional or loop contains an assignment (in which
# case we can't know for certain that that assignment doesn't impact the
# statements inside the conditional) then we can't use the modifier form
# and we must use the block form.
module ContainsAssignment
def self.call(parent)
queue = [parent]
while (node = queue.shift)
case node
when Assign, MAssign, OpAssign
return true
else
node.child_nodes.each { |child| queue << child if child }
end
end
false
end
end
# In order for an `if` or `unless` expression to be shortened to a ternary,
# there has to be one and only one consequent clause which is an Else. Both
# the body of the main node and the body of the Else node must have only one
# statement, and that statement must not be on the denied list of potential
# statements.
module Ternaryable
class << self
def call(q, node)
return false if ENV["STREE_FAST_FORMAT"] || q.disable_auto_ternary?
# If this is a conditional inside of a parentheses as the only content,
# then we don't want to transform it into a ternary. Presumably the user
# wanted it to be an explicit conditional because there are parentheses
# around it. So we'll just leave it in place.
grandparent = q.grandparent
if grandparent.is_a?(Paren) && (body = grandparent.contents.body) &&
body.length == 1 && body.first == node
return false
end
# Otherwise, we'll check the type of predicate. For certain nodes we
# want to force it to not be a ternary, like if the predicate is an
# assignment because it's hard to read.
case node.predicate
when Assign, Binary, Command, CommandCall, MAssign, OpAssign
return false
when Not
return false unless node.predicate.parentheses?
end
# If there's no Else, then this can't be represented as a ternary.
return false unless node.consequent.is_a?(Else)
truthy_body = node.statements.body
falsy_body = node.consequent.statements.body
(truthy_body.length == 1) && ternaryable?(truthy_body.first) &&
(falsy_body.length == 1) && ternaryable?(falsy_body.first)
end
private
# Certain expressions cannot be reduced to a ternary without adding
# parentheses around them. In this case we say they cannot be ternaried
# and default instead to breaking them into multiple lines.
def ternaryable?(statement)
case statement
when AliasNode, Assign, Break, Command, CommandCall, Defined, Heredoc,
IfNode, IfOp, Lambda, MAssign, Next, OpAssign, RescueMod,
ReturnNode, Super, Undef, UnlessNode, UntilNode, VoidStmt,
WhileNode, YieldNode, ZSuper
# This is a list of nodes that should not be allowed to be a part of a
# ternary clause.
false
when Binary
# If the user is using one of the lower precedence "and" or "or"
# operators, then we can't use a ternary expression as it would break
# the flow control.
operator = statement.operator
operator != :and && operator != :or
else
true
end
end
end
end
# Formats an If or Unless node.
class ConditionalFormatter
# [String] the keyword associated with this conditional
attr_reader :keyword
# [If | Unless] the node that is being formatted
attr_reader :node
def initialize(keyword, node)
@keyword = keyword
@node = node
end
def format(q)
if node.modifier?
statement = node.statements.body[0]
if ContainsAssignment.call(statement) || q.parent.is_a?(In)
q.group { format_flat(q) }
else
q.group do
q
.if_break { format_break(q, force: false) }
.if_flat { format_flat(q) }
end
end
else
# If we can transform this node into a ternary, then we're going to
# print a special version that uses the ternary operator if it fits on
# one line.
if Ternaryable.call(q, node)
format_ternary(q)
return
end
# If the predicate of the conditional contains an assignment (in which
# case we can't know for certain that that assignment doesn't impact the
# statements inside the conditional) then we can't use the modifier form
# and we must use the block form.
if ContainsAssignment.call(node.predicate)
format_break(q, force: true)
return
end
if node.consequent || node.statements.empty? || contains_conditional?
q.group { format_break(q, force: true) }
else
q.group do
q
.if_break { format_break(q, force: false) }
.if_flat do
Parentheses.flat(q) do
q.format(node.statements)
q.text(" #{keyword} ")
q.format(node.predicate)
end
end
end
end
end
end
private
def format_flat(q)
Parentheses.flat(q) do
q.format(node.statements.body[0])
q.text(" #{keyword} ")
q.format(node.predicate)
end
end
def format_break(q, force:)
q.text("#{keyword} ")
q.nest(keyword.length + 1) { q.format(node.predicate) }
unless node.statements.empty?
q.indent do
force ? q.breakable_force : q.breakable_space
q.format(node.statements)
end
end
if node.consequent
force ? q.breakable_force : q.breakable_space
q.format(node.consequent)
end
force ? q.breakable_force : q.breakable_space
q.text("end")
end
def format_ternary(q)
q.group do
q
.if_break do
q.text("#{keyword} ")
q.nest(keyword.length + 1) { q.format(node.predicate) }
q.indent do
q.breakable_space
q.format(node.statements)
end
q.breakable_space
q.group do
q.format(node.consequent.keyword)
q.indent do
# This is a very special case of breakable where we want to
# force it into the output but we _don't_ want to explicitly
# break the parent. If a break-parent shows up in the tree, then
# it's going to force it all the way up to the tree, which is
# going to negate the ternary.
q.breakable(force: :skip_break_parent)
q.format(node.consequent.statements)
end
end
q.breakable_space
q.text("end")
end
.if_flat do
Parentheses.flat(q) do
q.format(node.predicate)
q.text(" ? ")
statements = [node.statements, node.consequent.statements]
statements.reverse! if keyword == "unless"
q.format(statements[0])
q.text(" : ")
q.format(statements[1])
end
end
end
end
def contains_conditional?
statements = node.statements.body
return false if statements.length != 1
case statements.first
when IfNode, IfOp, UnlessNode
true
else
false
end
end
end
# If represents the first clause in an +if+ chain.
#
# if predicate
# end
#
class IfNode < Node
# [Node] the expression to be checked
attr_reader :predicate
# [Statements] the expressions to be executed
attr_reader :statements
# [nil | Elsif | Else] the next clause in the chain
attr_reader :consequent
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(predicate:, statements:, consequent:, location:)
@predicate = predicate
@statements = statements
@consequent = consequent
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_if(self)
end
def child_nodes
[predicate, statements, consequent]
end
def copy(predicate: nil, statements: nil, consequent: nil, location: nil)
node =
IfNode.new(
predicate: predicate || self.predicate,
statements: statements || self.statements,
consequent: consequent || self.consequent,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
predicate: predicate,
statements: statements,
consequent: consequent,
location: location,
comments: comments
}
end
def format(q)
ConditionalFormatter.new("if", self).format(q)
end
def ===(other)
other.is_a?(IfNode) && predicate === other.predicate &&
statements === other.statements && consequent === other.consequent
end
# Checks if the node was originally found in the modifier form.
def modifier?
predicate.location.start_char > statements.location.start_char
end
end
# IfOp represents a ternary clause.
#
# predicate ? truthy : falsy
#
class IfOp < Node
# [Node] the expression to be checked
attr_reader :predicate
# [Node] the expression to be executed if the predicate is truthy
attr_reader :truthy
# [Node] the expression to be executed if the predicate is falsy
attr_reader :falsy
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(predicate:, truthy:, falsy:, location:)
@predicate = predicate
@truthy = truthy
@falsy = falsy
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_if_op(self)
end
def child_nodes
[predicate, truthy, falsy]
end
def copy(predicate: nil, truthy: nil, falsy: nil, location: nil)
node =
IfOp.new(
predicate: predicate || self.predicate,
truthy: truthy || self.truthy,
falsy: falsy || self.falsy,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
predicate: predicate,
truthy: truthy,
falsy: falsy,
location: location,
comments: comments
}
end
def format(q)
force_flat = [
AliasNode,
Assign,
Break,
Command,
CommandCall,
Heredoc,
IfNode,
IfOp,
Lambda,
MAssign,
Next,
OpAssign,
RescueMod,
ReturnNode,
Super,
Undef,
UnlessNode,
VoidStmt,
YieldNode,
ZSuper
]
if q.parent.is_a?(Paren) || force_flat.include?(truthy.class) ||
force_flat.include?(falsy.class)
q.group { format_flat(q) }
return
end
q.group { q.if_break { format_break(q) }.if_flat { format_flat(q) } }
end
def ===(other)
other.is_a?(IfOp) && predicate === other.predicate &&
truthy === other.truthy && falsy === other.falsy
end
private
def format_break(q)
Parentheses.break(q) do
q.text("if ")
q.nest("if ".length) { q.format(predicate) }
q.indent do
q.breakable_space
q.format(truthy)
end
q.breakable_space
q.text("else")
q.indent do
q.breakable_space
q.format(falsy)
end
q.breakable_space
q.text("end")
end
end
def format_flat(q)
q.format(predicate)
q.text(" ?")
q.indent do
q.breakable_space
q.format(truthy)
q.text(" :")
q.breakable_space
q.format(falsy)
end
end
end
# Imaginary represents an imaginary number literal.
#
# 1i
#
class Imaginary < Node
# [String] the value of the imaginary number literal
attr_reader :value
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(value:, location:)
@value = value
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_imaginary(self)
end
def child_nodes
[]
end
def copy(value: nil, location: nil)
node =
Imaginary.new(
value: value || self.value,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location, comments: comments }
end
def format(q)
q.text(value)
end
def ===(other)
other.is_a?(Imaginary) && value === other.value
end
end
# In represents using the +in+ keyword within the Ruby 2.7+ pattern matching
# syntax.
#
# case value
# in pattern
# end
#
class In < Node
# [Node] the pattern to check against
attr_reader :pattern
# [Statements] the expressions to execute if the pattern matched
attr_reader :statements
# [nil | In | Else] the next clause in the chain
attr_reader :consequent
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(pattern:, statements:, consequent:, location:)
@pattern = pattern
@statements = statements
@consequent = consequent
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_in(self)
end
def child_nodes
[pattern, statements, consequent]
end
def copy(pattern: nil, statements: nil, consequent: nil, location: nil)
node =
In.new(
pattern: pattern || self.pattern,
statements: statements || self.statements,
consequent: consequent || self.consequent,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
pattern: pattern,
statements: statements,
consequent: consequent,
location: location,
comments: comments
}
end
def format(q)
keyword = "in "
pattern = self.pattern
consequent = self.consequent
q.group do
q.text(keyword)
q.nest(keyword.length) { q.format(pattern) }
q.text(" then") if pattern.is_a?(RangeNode) && pattern.right.nil?
unless statements.empty?
q.indent do
q.breakable_force
q.format(statements)
end
end
if consequent
q.breakable_force
q.format(consequent)
end
end
end
def ===(other)
other.is_a?(In) && pattern === other.pattern &&
statements === other.statements && consequent === other.consequent
end
end
# Int represents an integer number literal.
#
# 1
#
class Int < Node
# [String] the value of the integer
attr_reader :value
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(value:, location:)
@value = value
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_int(self)
end
def child_nodes
[]
end
def copy(value: nil, location: nil)
node =
Int.new(value: value || self.value, location: location || self.location)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location, comments: comments }
end
def format(q)
if !value.start_with?(/\+?0/) && value.length >= 5 && !value.include?("_")
# If it's a plain integer and it doesn't have any underscores separating
# the values, then we're going to insert them every 3 characters
# starting from the right.
index = (value.length + 2) % 3
q.text(" #{value}"[index..].scan(/.../).join("_").strip)
else
q.text(value)
end
end
def ===(other)
other.is_a?(Int) && value === other.value
end
end
# IVar represents an instance variable literal.
#
# @variable
#
class IVar < Node
# [String] the name of the instance variable
attr_reader :value
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(value:, location:)
@value = value
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_ivar(self)
end
def child_nodes
[]
end
def copy(value: nil, location: nil)
node =
IVar.new(
value: value || self.value,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location, comments: comments }
end
def format(q)
q.text(value)
end
def ===(other)
other.is_a?(IVar) && value === other.value
end
end
# Kw represents the use of a keyword. It can be almost anywhere in the syntax
# tree, so you end up seeing it quite a lot.
#
# if value
# end
#
# In the above example, there would be two Kw nodes: one for the if and one
# for the end. Note that anything that matches the list of keywords in Ruby
# will use a Kw, so if you use a keyword in a symbol literal for instance:
#
# :if
#
# then the contents of the symbol node will contain a Kw node.
class Kw < Node
# [String] the value of the keyword
attr_reader :value
# [Symbol] the symbol version of the value
attr_reader :name
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(value:, location:)
@value = value
@name = value.to_sym
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_kw(self)
end
def child_nodes
[]
end
def copy(value: nil, location: nil)
node =
Kw.new(value: value || self.value, location: location || self.location)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location, comments: comments }
end
def format(q)
q.text(value)
end
def ===(other)
other.is_a?(Kw) && value === other.value
end
end
# KwRestParam represents defining a parameter in a method definition that
# accepts all remaining keyword parameters.
#
# def method(**kwargs) end
#
class KwRestParam < Node
# [nil | Ident] the name of the parameter
attr_reader :name
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(name:, location:)
@name = name
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_kwrest_param(self)
end
def child_nodes
[name]
end
def copy(name: nil, location: nil)
node =
KwRestParam.new(
name: name || self.name,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ name: name, location: location, comments: comments }
end
def format(q)
q.text("**")
q.format(name) if name
end
def ===(other)
other.is_a?(KwRestParam) && name === other.name
end
end
# Label represents the use of an identifier to associate with an object. You
# can find it in a hash key, as in:
#
# { key: value }
#
# In this case "key:" would be the body of the label. You can also find it in
# pattern matching, as in:
#
# case value
# in key:
# end
#
# In this case "key:" would be the body of the label.
class Label < Node
# [String] the value of the label
attr_reader :value
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(value:, location:)
@value = value
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_label(self)
end
def child_nodes
[]
end
def copy(value: nil, location: nil)
node =
Label.new(
value: value || self.value,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location, comments: comments }
end
def format(q)
q.text(value)
end
def ===(other)
other.is_a?(Label) && value === other.value
end
end
# LabelEnd represents the end of a dynamic symbol.
#
# { "key": value }
#
# In the example above, LabelEnd represents the "\":" token at the end of the
# hash key. This node is important for determining the type of quote being
# used by the label.
class LabelEnd < Node
# [String] the end of the label
attr_reader :value
def initialize(value:, location:)
@value = value
@location = location
end
def accept(visitor)
visitor.visit_label_end(self)
end
def child_nodes
[]
end
def copy(value: nil, location: nil)
LabelEnd.new(
value: value || self.value,
location: location || self.location
)
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location }
end
def ===(other)
other.is_a?(LabelEnd) && value === other.value
end
end
# Lambda represents using a lambda literal (not the lambda method call).
#
# ->(value) { value * 2 }
#
class Lambda < Node
# [LambdaVar | Paren] the parameter declaration for this lambda
attr_reader :params
# [BodyStmt | Statements] the expressions to be executed in this lambda
attr_reader :statements
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(params:, statements:, location:)
@params = params
@statements = statements
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_lambda(self)
end
def child_nodes
[params, statements]
end
def copy(params: nil, statements: nil, location: nil)
node =
Lambda.new(
params: params || self.params,
statements: statements || self.statements,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
params: params,
statements: statements,
location: location,
comments: comments
}
end
def format(q)
params = self.params
q.text("->")
q.group do
if params.is_a?(Paren)
q.format(params) unless params.contents.empty?
elsif params.empty? && params.comments.any?
q.format(params)
elsif !params.empty?
q.group do
q.text("(")
q.format(params)
q.text(")")
end
end
q.text(" ")
q
.if_break do
q.text("do")
unless statements.empty?
q.indent do
q.breakable_space
q.format(statements)
end
end
q.breakable_space
q.text("end")
end
.if_flat do
q.text("{")
unless statements.empty?
q.text(" ")
q.format(statements)
q.text(" ")
end
q.text("}")
end
end
end
def ===(other)
other.is_a?(Lambda) && params === other.params &&
statements === other.statements
end
end
# LambdaVar represents the parameters being declared for a lambda. Effectively
# this node is everything contained within the parentheses. This includes all
# of the various parameter types, as well as block-local variable
# declarations.
#
# -> (positional, optional = value, keyword:, █ local) do
# end
#
class LambdaVar < Node
# [Params] the parameters being declared with the block
attr_reader :params
# [Array[ Ident ]] the list of block-local variable declarations
attr_reader :locals
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(params:, locals:, location:)
@params = params
@locals = locals
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_lambda_var(self)
end
def child_nodes
[params, *locals]
end
def copy(params: nil, locals: nil, location: nil)
node =
LambdaVar.new(
params: params || self.params,
locals: locals || self.locals,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ params: params, locals: locals, location: location, comments: comments }
end
def empty?
params.empty? && locals.empty?
end
def format(q)
q.format(params)
if locals.any?
q.text("; ")
q.seplist(locals, BlockVar::SEPARATOR) { |local| q.format(local) }
end
end
def ===(other)
other.is_a?(LambdaVar) && params === other.params &&
ArrayMatch.call(locals, other.locals)
end
end
# LBrace represents the use of a left brace, i.e., {.
class LBrace < Node
# [String] the left brace
attr_reader :value
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(value:, location:)
@value = value
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_lbrace(self)
end
def child_nodes
[]
end
def copy(value: nil, location: nil)
node =
LBrace.new(
value: value || self.value,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location, comments: comments }
end
def format(q)
q.text(value)
end
def ===(other)
other.is_a?(LBrace) && value === other.value
end
# Because some nodes keep around a { token so that comments can be attached
# to it if they occur in the source, oftentimes an LBrace is a child of
# another node. This means it's required at initialization time. To make it
# easier to create LBrace nodes without any specific value, this method
# provides a default node.
def self.default
new(value: "{", location: Location.default)
end
end
# LBracket represents the use of a left bracket, i.e., [.
class LBracket < Node
# [String] the left bracket
attr_reader :value
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(value:, location:)
@value = value
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_lbracket(self)
end
def child_nodes
[]
end
def copy(value: nil, location: nil)
node =
LBracket.new(
value: value || self.value,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location, comments: comments }
end
def format(q)
q.text(value)
end
def ===(other)
other.is_a?(LBracket) && value === other.value
end
# Because some nodes keep around a [ token so that comments can be attached
# to it if they occur in the source, oftentimes an LBracket is a child of
# another node. This means it's required at initialization time. To make it
# easier to create LBracket nodes without any specific value, this method
# provides a default node.
def self.default
new(value: "[", location: Location.default)
end
end
# LParen represents the use of a left parenthesis, i.e., (.
class LParen < Node
# [String] the left parenthesis
attr_reader :value
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(value:, location:)
@value = value
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_lparen(self)
end
def child_nodes
[]
end
def copy(value: nil, location: nil)
node =
LParen.new(
value: value || self.value,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location, comments: comments }
end
def format(q)
q.text(value)
end
def ===(other)
other.is_a?(LParen) && value === other.value
end
# Because some nodes keep around a ( token so that comments can be attached
# to it if they occur in the source, oftentimes an LParen is a child of
# another node. This means it's required at initialization time. To make it
# easier to create LParen nodes without any specific value, this method
# provides a default node.
def self.default
new(value: "(", location: Location.default)
end
end
# MAssign is a parent node of any kind of multiple assignment. This includes
# splitting out variables on the left like:
#
# first, second, third = value
#
# as well as splitting out variables on the right, as in:
#
# value = first, second, third
#
# Both sides support splats, as well as variables following them. There's also
# destructuring behavior that you can achieve with the following:
#
# first, = value
#
class MAssign < Node
# [MLHS | MLHSParen] the target of the multiple assignment
attr_reader :target
# [Node] the value being assigned
attr_reader :value
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(target:, value:, location:)
@target = target
@value = value
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_massign(self)
end
def child_nodes
[target, value]
end
def copy(target: nil, value: nil, location: nil)
node =
MAssign.new(
target: target || self.target,
value: value || self.value,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ target: target, value: value, location: location, comments: comments }
end
def format(q)
q.group do
q.group { q.format(target) }
q.text(" =")
q.indent do
q.breakable_space
q.format(value)
end
end
end
def ===(other)
other.is_a?(MAssign) && target === other.target && value === other.value
end
end
# MethodAddBlock represents a method call with a block argument.
#
# method {}
#
class MethodAddBlock < Node
# [ARef | CallNode | Command | CommandCall | Super | ZSuper] the method call
attr_reader :call
# [BlockNode] the block being sent with the method call
attr_reader :block
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(call:, block:, location:)
@call = call
@block = block
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_method_add_block(self)
end
def child_nodes
[call, block]
end
def copy(call: nil, block: nil, location: nil)
node =
MethodAddBlock.new(
call: call || self.call,
block: block || self.block,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ call: call, block: block, location: location, comments: comments }
end
def format(q)
# If we're at the top of a call chain, then we're going to do some
# specialized printing in case we can print it nicely. We _only_ do this
# at the top of the chain to avoid weird recursion issues.
if CallChainFormatter.chained?(call) &&
!CallChainFormatter.chained?(q.parent)
q.group do
q
.if_break { CallChainFormatter.new(self).format(q) }
.if_flat { format_contents(q) }
end
else
format_contents(q)
end
end
def ===(other)
other.is_a?(MethodAddBlock) && call === other.call &&
block === other.block
end
def format_contents(q)
q.format(call)
q.format(block)
end
end
# MLHS represents a list of values being destructured on the left-hand side
# of a multiple assignment.
#
# first, second, third = value
#
class MLHS < Node
# [
# Array[
# ARefField | ArgStar | ConstPathField | Field | Ident | MLHSParen |
# TopConstField | VarField
# ]
# ] the parts of the left-hand side of a multiple assignment
attr_reader :parts
# [boolean] whether or not there is a trailing comma at the end of this
# list, which impacts destructuring. It's an attr_accessor so that while
# the syntax tree is being built it can be set by its parent node
attr_accessor :comma
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(parts:, location:, comma: false)
@parts = parts
@comma = comma
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_mlhs(self)
end
def child_nodes
parts
end
def copy(parts: nil, location: nil, comma: nil)
node =
MLHS.new(
parts: parts || self.parts,
location: location || self.location,
comma: comma || self.comma
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ parts: parts, location: location, comma: comma, comments: comments }
end
def format(q)
q.seplist(parts) { |part| q.format(part) }
q.text(",") if comma
end
def ===(other)
other.is_a?(MLHS) && ArrayMatch.call(parts, other.parts) &&
comma === other.comma
end
end
# MLHSParen represents parentheses being used to destruct values in a multiple
# assignment on the left hand side.
#
# (left, right) = value
#
class MLHSParen < Node
# [MLHS | MLHSParen] the contents inside of the parentheses
attr_reader :contents
# [boolean] whether or not there is a trailing comma at the end of this
# list, which impacts destructuring. It's an attr_accessor so that while
# the syntax tree is being built it can be set by its parent node
attr_accessor :comma
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(contents:, location:, comma: false)
@contents = contents
@comma = comma
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_mlhs_paren(self)
end
def child_nodes
[contents]
end
def copy(contents: nil, location: nil)
node =
MLHSParen.new(
contents: contents || self.contents,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ contents: contents, location: location, comments: comments }
end
def format(q)
parent = q.parent
if parent.is_a?(MAssign) || parent.is_a?(MLHSParen)
q.format(contents)
q.text(",") if comma
else
q.text("(")
q.group do
q.indent do
q.breakable_empty
q.format(contents)
end
q.text(",") if comma
q.breakable_empty
end
q.text(")")
end
end
def ===(other)
other.is_a?(MLHSParen) && contents === other.contents
end
end
# ModuleDeclaration represents defining a module using the +module+ keyword.
#
# module Namespace
# end
#
class ModuleDeclaration < Node
# [ConstPathRef | ConstRef | TopConstRef] the name of the module
attr_reader :constant
# [BodyStmt] the expressions to be executed in the context of the module
attr_reader :bodystmt
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(constant:, bodystmt:, location:)
@constant = constant
@bodystmt = bodystmt
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_module(self)
end
def child_nodes
[constant, bodystmt]
end
def copy(constant: nil, bodystmt: nil, location: nil)
node =
ModuleDeclaration.new(
constant: constant || self.constant,
bodystmt: bodystmt || self.bodystmt,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
constant: constant,
bodystmt: bodystmt,
location: location,
comments: comments
}
end
def format(q)
if bodystmt.empty?
q.group do
format_declaration(q)
q.breakable_force
q.text("end")
end
else
q.group do
format_declaration(q)
q.indent do
q.breakable_force
q.format(bodystmt)
end
q.breakable_force
q.text("end")
end
end
end
def ===(other)
other.is_a?(ModuleDeclaration) && constant === other.constant &&
bodystmt === other.bodystmt
end
private
def format_declaration(q)
q.group do
q.text("module ")
q.format(constant)
end
end
end
# MRHS represents the values that are being assigned on the right-hand side of
# a multiple assignment.
#
# values = first, second, third
#
class MRHS < Node
# [Array[Node]] the parts that are being assigned
attr_reader :parts
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(parts:, location:)
@parts = parts
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_mrhs(self)
end
def child_nodes
parts
end
def copy(parts: nil, location: nil)
node =
MRHS.new(
parts: parts || self.parts,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ parts: parts, location: location, comments: comments }
end
def format(q)
q.seplist(parts) { |part| q.format(part) }
end
def ===(other)
other.is_a?(MRHS) && ArrayMatch.call(parts, other.parts)
end
end
# Next represents using the +next+ keyword.
#
# next
#
# The +next+ keyword can also optionally be called with an argument:
#
# next value
#
# +next+ can even be called with multiple arguments, but only if parentheses
# are omitted, as in:
#
# next first, second, third
#
# If a single value is being given, parentheses can be used, as in:
#
# next(value)
#
class Next < Node
# [Args] the arguments passed to the next keyword
attr_reader :arguments
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(arguments:, location:)
@arguments = arguments
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_next(self)
end
def child_nodes
[arguments]
end
def copy(arguments: nil, location: nil)
node =
Next.new(
arguments: arguments || self.arguments,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ arguments: arguments, location: location, comments: comments }
end
def format(q)
FlowControlFormatter.new("next", self).format(q)
end
def ===(other)
other.is_a?(Next) && arguments === other.arguments
end
end
# Op represents an operator literal in the source.
#
# 1 + 2
#
# In the example above, the Op node represents the + operator.
class Op < Node
# [String] the operator
attr_reader :value
# [Symbol] the symbol version of the value
attr_reader :name
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(value:, location:)
@value = value
@name = value.to_sym
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_op(self)
end
def child_nodes
[]
end
def copy(value: nil, location: nil)
node =
Op.new(value: value || self.value, location: location || self.location)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location, comments: comments }
end
def format(q)
q.text(value)
end
def ===(other)
other.is_a?(Op) && value === other.value
end
end
# OpAssign represents assigning a value to a variable or constant using an
# operator like += or ||=.
#
# variable += value
#
class OpAssign < Node
# [ARefField | ConstPathField | Field | TopConstField | VarField] the target
# to assign the result of the expression to
attr_reader :target
# [Op] the operator being used for the assignment
attr_reader :operator
# [Node] the expression to be assigned
attr_reader :value
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(target:, operator:, value:, location:)
@target = target
@operator = operator
@value = value
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_opassign(self)
end
def child_nodes
[target, operator, value]
end
def copy(target: nil, operator: nil, value: nil, location: nil)
node =
OpAssign.new(
target: target || self.target,
operator: operator || self.operator,
value: value || self.value,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
target: target,
operator: operator,
value: value,
location: location,
comments: comments
}
end
def format(q)
q.group do
q.format(target)
q.text(" ")
q.format(operator)
if skip_indent?
q.text(" ")
q.format(value)
else
q.indent do
q.breakable_space
q.format(value)
end
end
end
end
def ===(other)
other.is_a?(OpAssign) && target === other.target &&
operator === other.operator && value === other.value
end
private
def skip_indent?
target.comments.empty? &&
(target.is_a?(ARefField) || AssignFormatting.skip_indent?(value))
end
end
# If you have a modifier statement (for instance a modifier if statement or a
# modifier while loop) there are times when you need to wrap the entire
# statement in parentheses. This occurs when you have something like:
#
# foo[:foo] =
# if bar?
# baz
# end
#
# Normally we would shorten this to an inline version, which would result in:
#
# foo[:foo] = baz if bar?
#
# but this actually has different semantic meaning. The first example will
# result in a nil being inserted into the hash for the :foo key, whereas the
# second example will result in an empty hash because the if statement applies
# to the entire assignment.
#
# We can fix this in a couple of ways. We can use the then keyword, as in:
#
# foo[:foo] = if bar? then baz end
#
# But this isn't used very often. We can also just leave it as is with the
# multi-line version, but for a short predicate and short value it looks
# verbose. The last option and the one used here is to add parentheses on
# both sides of the expression, as in:
#
# foo[:foo] = (baz if bar?)
#
# This approach maintains the nice conciseness of the inline version, while
# keeping the correct semantic meaning.
module Parentheses
NODES = [
Args,
Assign,
Assoc,
Binary,
CallNode,
Defined,
MAssign,
OpAssign
].freeze
def self.flat(q)
return yield unless NODES.include?(q.parent.class)
q.text("(")
yield
q.text(")")
end
def self.break(q)
return yield unless NODES.include?(q.parent.class)
q.text("(")
q.indent do
q.breakable_empty
yield
end
q.breakable_empty
q.text(")")
end
end
# def on_operator_ambiguous(value)
# value
# end
# Params represents defining parameters on a method or lambda.
#
# def method(param) end
#
class Params < Node
# Formats the optional position of the parameters. This includes the label,
# as well as the default value.
class OptionalFormatter
# [Ident] the name of the parameter
attr_reader :name
# [Node] the value of the parameter
attr_reader :value
def initialize(name, value)
@name = name
@value = value
end
def comments
[]
end
def format(q)
q.format(name)
q.text(" = ")
q.format(value)
end
end
# Formats the keyword position of the parameters. This includes the label,
# as well as an optional default value.
class KeywordFormatter
# [Ident] the name of the parameter
attr_reader :name
# [nil | Node] the value of the parameter
attr_reader :value
def initialize(name, value)
@name = name
@value = value
end
def comments
[]
end
def format(q)
q.format(name)
if value
q.text(" ")
q.format(value)
end
end
end
# Formats the keyword_rest position of the parameters. This can be the **nil
# syntax, the ... syntax, or the ** syntax.
class KeywordRestFormatter
# [:nil | ArgsForward | KwRestParam] the value of the parameter
attr_reader :value
def initialize(value)
@value = value
end
def comments
[]
end
def format(q)
value == :nil ? q.text("**nil") : q.format(value)
end
end
# [Array[ Ident | MLHSParen ]] any required parameters
attr_reader :requireds
# [Array[ [ Ident, Node ] ]] any optional parameters and their default
# values
attr_reader :optionals
# [nil | ArgsForward | ExcessedComma | RestParam] the optional rest
# parameter
attr_reader :rest
# [Array[ Ident | MLHSParen ]] any positional parameters that exist after a
# rest parameter
attr_reader :posts
# [Array[ [ Label, nil | Node ] ]] any keyword parameters and their
# optional default values
attr_reader :keywords
# [nil | :nil | ArgsForward | KwRestParam] the optional keyword rest
# parameter
attr_reader :keyword_rest
# [nil | BlockArg] the optional block parameter
attr_reader :block
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(
location:,
requireds: [],
optionals: [],
rest: nil,
posts: [],
keywords: [],
keyword_rest: nil,
block: nil
)
@requireds = requireds
@optionals = optionals
@rest = rest
@posts = posts
@keywords = keywords
@keyword_rest = keyword_rest
@block = block
@location = location
@comments = []
end
# Params nodes are the most complicated in the tree. Occasionally you want
# to know if they are "empty", which means not having any parameters
# declared. This logic accesses every kind of parameter and determines if
# it's missing.
def empty?
requireds.empty? && optionals.empty? && !rest && posts.empty? &&
keywords.empty? && !keyword_rest && !block
end
def accept(visitor)
visitor.visit_params(self)
end
def child_nodes
keyword_rest = self.keyword_rest
[
*requireds,
*optionals.flatten(1),
rest,
*posts,
*keywords.flatten(1),
(keyword_rest if keyword_rest != :nil),
block
]
end
def copy(
location: nil,
requireds: nil,
optionals: nil,
rest: nil,
posts: nil,
keywords: nil,
keyword_rest: nil,
block: nil
)
node =
Params.new(
location: location || self.location,
requireds: requireds || self.requireds,
optionals: optionals || self.optionals,
rest: rest || self.rest,
posts: posts || self.posts,
keywords: keywords || self.keywords,
keyword_rest: keyword_rest || self.keyword_rest,
block: block || self.block
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
location: location,
requireds: requireds,
optionals: optionals,
rest: rest,
posts: posts,
keywords: keywords,
keyword_rest: keyword_rest,
block: block,
comments: comments
}
end
def format(q)
rest = self.rest
keyword_rest = self.keyword_rest
parts = [
*requireds,
*optionals.map { |(name, value)| OptionalFormatter.new(name, value) }
]
parts << rest if rest && !rest.is_a?(ExcessedComma)
parts.concat(posts)
parts.concat(
keywords.map { |(name, value)| KeywordFormatter.new(name, value) }
)
parts << KeywordRestFormatter.new(keyword_rest) if keyword_rest
parts << block if block
if parts.empty?
q.nest(0) { format_contents(q, parts) }
return
end
if q.parent.is_a?(DefNode)
q.nest(0) do
q.text("(")
q.group do
q.indent do
q.breakable_empty
format_contents(q, parts)
end
q.breakable_empty
end
q.text(")")
end
else
q.nest(0) { format_contents(q, parts) }
end
end
def ===(other)
other.is_a?(Params) && ArrayMatch.call(requireds, other.requireds) &&
optionals.length == other.optionals.length &&
optionals
.zip(other.optionals)
.all? { |left, right| ArrayMatch.call(left, right) } &&
rest === other.rest && ArrayMatch.call(posts, other.posts) &&
keywords.length == other.keywords.length &&
keywords
.zip(other.keywords)
.all? { |left, right| ArrayMatch.call(left, right) } &&
keyword_rest === other.keyword_rest && block === other.block
end
# Returns a range representing the possible number of arguments accepted
# by this params node not including the block. For example:
#
# def foo(a, b = 1, c:, d: 2, &block)
# ...
# end
#
# has arity 2..4.
#
def arity
optional_keywords = keywords.count { |_label, value| value }
lower_bound =
requireds.length + posts.length + keywords.length - optional_keywords
upper_bound =
if keyword_rest.nil? && rest.nil?
lower_bound + optionals.length + optional_keywords
end
lower_bound..upper_bound
end
private
def format_contents(q, parts)
q.seplist(parts) { |part| q.format(part) }
q.format(rest) if rest.is_a?(ExcessedComma)
end
end
# Paren represents using balanced parentheses in a couple places in a Ruby
# program. In general parentheses can be used anywhere a Ruby expression can
# be used.
#
# (1 + 2)
#
class Paren < Node
# [LParen] the left parenthesis that opened this statement
attr_reader :lparen
# [nil | Node] the expression inside the parentheses
attr_reader :contents
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(lparen:, contents:, location:)
@lparen = lparen
@contents = contents
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_paren(self)
end
def child_nodes
[lparen, contents]
end
def copy(lparen: nil, contents: nil, location: nil)
node =
Paren.new(
lparen: lparen || self.lparen,
contents: contents || self.contents,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
lparen: lparen,
contents: contents,
location: location,
comments: comments
}
end
def format(q)
contents = self.contents
q.group do
q.format(lparen)
if contents && (!contents.is_a?(Params) || !contents.empty?)
q.indent do
q.breakable_empty
q.format(contents)
end
end
q.breakable_empty
q.text(")")
end
end
def ===(other)
other.is_a?(Paren) && lparen === other.lparen &&
contents === other.contents
end
end
# Period represents the use of the +.+ operator. It is usually found in method
# calls.
class Period < Node
# [String] the period
attr_reader :value
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(value:, location:)
@value = value
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_period(self)
end
def child_nodes
[]
end
def copy(value: nil, location: nil)
node =
Period.new(
value: value || self.value,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location, comments: comments }
end
def format(q)
q.text(value)
end
def ===(other)
other.is_a?(Period) && value === other.value
end
end
# Program represents the overall syntax tree.
class Program < Node
# [Statements] the top-level expressions of the program
attr_reader :statements
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(statements:, location:)
@statements = statements
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_program(self)
end
def child_nodes
[statements]
end
def copy(statements: nil, location: nil)
node =
Program.new(
statements: statements || self.statements,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ statements: statements, location: location, comments: comments }
end
def format(q)
q.format(statements)
# We're going to put a newline on the end so that it always has one unless
# it ends with the special __END__ syntax. In that case we want to
# replicate the text exactly so we will just let it be.
q.breakable_force unless statements.body.last.is_a?(EndContent)
end
def ===(other)
other.is_a?(Program) && statements === other.statements
end
end
# QSymbols represents a symbol literal array without interpolation.
#
# %i[one two three]
#
class QSymbols < Node
# [QSymbolsBeg] the token that opens this array literal
attr_reader :beginning
# [Array[ TStringContent ]] the elements of the array
attr_reader :elements
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(beginning:, elements:, location:)
@beginning = beginning
@elements = elements
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_qsymbols(self)
end
def child_nodes
[]
end
def copy(beginning: nil, elements: nil, location: nil)
node =
QSymbols.new(
beginning: beginning || self.beginning,
elements: elements || self.elements,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
beginning: beginning,
elements: elements,
location: location,
comments: comments
}
end
def format(q)
opening, closing = "%i[", "]"
if elements.any? { |element| element.match?(/[\[\]]/) }
opening = beginning.value
closing = Quotes.matching(opening[2])
end
q.text(opening)
q.group do
q.indent do
q.breakable_empty
q.seplist(
elements,
ArrayLiteral::BREAKABLE_SPACE_SEPARATOR
) { |element| q.format(element) }
end
q.breakable_empty
end
q.text(closing)
end
def ===(other)
other.is_a?(QSymbols) && beginning === other.beginning &&
ArrayMatch.call(elements, other.elements)
end
end
# QSymbolsBeg represents the beginning of a symbol literal array.
#
# %i[one two three]
#
# In the snippet above, QSymbolsBeg represents the "%i[" token. Note that
# these kinds of arrays can start with a lot of different delimiter types
# (e.g., %i| or %i<).
class QSymbolsBeg < Node
# [String] the beginning of the array literal
attr_reader :value
def initialize(value:, location:)
@value = value
@location = location
end
def accept(visitor)
visitor.visit_qsymbols_beg(self)
end
def child_nodes
[]
end
def copy(value: nil, location: nil)
QSymbolsBeg.new(
value: value || self.value,
location: location || self.location
)
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location }
end
def ===(other)
other.is_a?(QSymbolsBeg) && value === other.value
end
end
# QWords represents a string literal array without interpolation.
#
# %w[one two three]
#
class QWords < Node
# [QWordsBeg] the token that opens this array literal
attr_reader :beginning
# [Array[ TStringContent ]] the elements of the array
attr_reader :elements
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(beginning:, elements:, location:)
@beginning = beginning
@elements = elements
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_qwords(self)
end
def child_nodes
[]
end
def copy(beginning: nil, elements: nil, location: nil)
QWords.new(
beginning: beginning || self.beginning,
elements: elements || self.elements,
location: location || self.location
)
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
beginning: beginning,
elements: elements,
location: location,
comments: comments
}
end
def format(q)
opening, closing = "%w[", "]"
if elements.any? { |element| element.match?(/[\[\]]/) }
opening = beginning.value
closing = Quotes.matching(opening[2])
end
q.text(opening)
q.group do
q.indent do
q.breakable_empty
q.seplist(
elements,
ArrayLiteral::BREAKABLE_SPACE_SEPARATOR
) { |element| q.format(element) }
end
q.breakable_empty
end
q.text(closing)
end
def ===(other)
other.is_a?(QWords) && beginning === other.beginning &&
ArrayMatch.call(elements, other.elements)
end
end
# QWordsBeg represents the beginning of a string literal array.
#
# %w[one two three]
#
# In the snippet above, QWordsBeg represents the "%w[" token. Note that these
# kinds of arrays can start with a lot of different delimiter types (e.g.,
# %w| or %w<).
class QWordsBeg < Node
# [String] the beginning of the array literal
attr_reader :value
def initialize(value:, location:)
@value = value
@location = location
end
def accept(visitor)
visitor.visit_qwords_beg(self)
end
def child_nodes
[]
end
def copy(value: nil, location: nil)
QWordsBeg.new(
value: value || self.value,
location: location || self.location
)
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location }
end
def ===(other)
other.is_a?(QWordsBeg) && value === other.value
end
end
# RationalLiteral represents the use of a rational number literal.
#
# 1r
#
class RationalLiteral < Node
# [String] the rational number literal
attr_reader :value
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(value:, location:)
@value = value
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_rational(self)
end
def child_nodes
[]
end
def copy(value: nil, location: nil)
node =
RationalLiteral.new(
value: value || self.value,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location, comments: comments }
end
def format(q)
q.text(value)
end
def ===(other)
other.is_a?(RationalLiteral) && value === other.value
end
end
# RBrace represents the use of a right brace, i.e., +++.
class RBrace < Node
# [String] the right brace
attr_reader :value
def initialize(value:, location:)
@value = value
@location = location
end
def accept(visitor)
visitor.visit_rbrace(self)
end
def child_nodes
[]
end
def copy(value: nil, location: nil)
RBrace.new(
value: value || self.value,
location: location || self.location
)
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location }
end
def ===(other)
other.is_a?(RBrace) && value === other.value
end
end
# RBracket represents the use of a right bracket, i.e., +]+.
class RBracket < Node
# [String] the right bracket
attr_reader :value
def initialize(value:, location:)
@value = value
@location = location
end
def accept(visitor)
visitor.visit_rbracket(self)
end
def child_nodes
[]
end
def copy(value: nil, location: nil)
RBracket.new(
value: value || self.value,
location: location || self.location
)
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location }
end
def ===(other)
other.is_a?(RBracket) && value === other.value
end
end
# Redo represents the use of the +redo+ keyword.
#
# redo
#
class Redo < Node
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(location:)
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_redo(self)
end
def child_nodes
[]
end
def copy(location: nil)
node = Redo.new(location: location || self.location)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ location: location, comments: comments }
end
def format(q)
q.text("redo")
end
def ===(other)
other.is_a?(Redo)
end
end
# RegexpContent represents the body of a regular expression.
#
# /.+ #{pattern} .+/
#
# In the example above, a RegexpContent node represents everything contained
# within the forward slashes.
class RegexpContent < Node
# [String] the opening of the regular expression
attr_reader :beginning
# [Array[ StringDVar | StringEmbExpr | TStringContent ]] the parts of the
# regular expression
attr_reader :parts
def initialize(beginning:, parts:, location:)
@beginning = beginning
@parts = parts
@location = location
end
def accept(visitor)
visitor.visit_regexp_content(self)
end
def child_nodes
parts
end
def copy(beginning: nil, parts: nil, location: nil)
RegexpContent.new(
beginning: beginning || self.beginning,
parts: parts || self.parts,
location: location || self.location
)
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ beginning: beginning, parts: parts, location: location }
end
def ===(other)
other.is_a?(RegexpContent) && beginning === other.beginning &&
parts === other.parts
end
end
# RegexpBeg represents the start of a regular expression literal.
#
# /.+/
#
# In the example above, RegexpBeg represents the first / token. Regular
# expression literals can also be declared using the %r syntax, as in:
#
# %r{.+}
#
class RegexpBeg < Node
# [String] the beginning of the regular expression
attr_reader :value
def initialize(value:, location:)
@value = value
@location = location
end
def accept(visitor)
visitor.visit_regexp_beg(self)
end
def child_nodes
[]
end
def copy(value: nil, location: nil)
RegexpBeg.new(
value: value || self.value,
location: location || self.location
)
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location }
end
def ===(other)
other.is_a?(RegexpBeg) && value === other.value
end
end
# RegexpEnd represents the end of a regular expression literal.
#
# /.+/m
#
# In the example above, the RegexpEnd event represents the /m at the end of
# the regular expression literal. You can also declare regular expression
# literals using %r, as in:
#
# %r{.+}m
#
class RegexpEnd < Node
# [String] the end of the regular expression
attr_reader :value
def initialize(value:, location:)
@value = value
@location = location
end
def accept(visitor)
visitor.visit_regexp_end(self)
end
def child_nodes
[]
end
def copy(value: nil, location: nil)
RegexpEnd.new(
value: value || self.value,
location: location || self.location
)
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location }
end
def ===(other)
other.is_a?(RegexpEnd) && value === other.value
end
end
# RegexpLiteral represents a regular expression literal.
#
# /.+/
#
class RegexpLiteral < Node
# [String] the beginning of the regular expression literal
attr_reader :beginning
# [String] the ending of the regular expression literal
attr_reader :ending
# [Array[ StringEmbExpr | StringDVar | TStringContent ]] the parts of the
# regular expression literal
attr_reader :parts
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(beginning:, ending:, parts:, location:)
@beginning = beginning
@ending = ending
@parts = parts
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_regexp_literal(self)
end
def child_nodes
parts
end
def copy(beginning: nil, ending: nil, parts: nil, location: nil)
node =
RegexpLiteral.new(
beginning: beginning || self.beginning,
ending: ending || self.ending,
parts: parts || self.parts,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
beginning: beginning,
ending: ending,
options: options,
parts: parts,
location: location,
comments: comments
}
end
def format(q)
braces = ambiguous?(q) || include?(%r{/})
if braces && include?(/[{}]/)
q.group do
q.text(beginning)
q.format_each(parts)
q.text(ending)
end
elsif braces
q.group do
q.text("%r{")
if beginning == "/"
# If we're changing from a forward slash to a %r{, then we can
# replace any escaped forward slashes with regular forward slashes.
parts.each do |part|
if part.is_a?(TStringContent)
q.text(part.value.gsub("\\/", "/"))
else
q.format(part)
end
end
else
q.format_each(parts)
end
q.text("}")
q.text(options)
end
else
q.group do
q.text("/")
q.format_each(parts)
q.text("/")
q.text(options)
end
end
end
def ===(other)
other.is_a?(RegexpLiteral) && beginning === other.beginning &&
ending === other.ending && options === other.options &&
ArrayMatch.call(parts, other.parts)
end
def options
ending[1..]
end
private
def include?(pattern)
parts.any? do |part|
part.is_a?(TStringContent) && part.value.match?(pattern)
end
end
# If the first part of this regex is plain string content, we have a space
# or an =, and we're contained within a command or command_call node, then
# we want to use braces because otherwise we could end up with an ambiguous
# operator, e.g. foo / bar/ or foo /=bar/
def ambiguous?(q)
return false if parts.empty?
part = parts.first
part.is_a?(TStringContent) && part.value.start_with?(" ", "=") &&
q.parents.any? { |node| node.is_a?(Command) || node.is_a?(CommandCall) }
end
end
# RescueEx represents the list of exceptions being rescued in a rescue clause.
#
# begin
# rescue Exception => exception
# end
#
class RescueEx < Node
# [nil | Node] the list of exceptions being rescued
attr_reader :exceptions
# [nil | Field | VarField] the expression being used to capture the raised
# exception
attr_reader :variable
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(exceptions:, variable:, location:)
@exceptions = exceptions
@variable = variable
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_rescue_ex(self)
end
def child_nodes
[*exceptions, variable]
end
def copy(exceptions: nil, variable: nil, location: nil)
node =
RescueEx.new(
exceptions: exceptions || self.exceptions,
variable: variable || self.variable,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
exceptions: exceptions,
variable: variable,
location: location,
comments: comments
}
end
def format(q)
q.group do
if exceptions
q.text(" ")
q.format(exceptions)
end
if variable
q.text(" => ")
q.format(variable)
end
end
end
def ===(other)
other.is_a?(RescueEx) && exceptions === other.exceptions &&
variable === other.variable
end
end
# Rescue represents the use of the rescue keyword inside of a BodyStmt node.
#
# begin
# rescue
# end
#
class Rescue < Node
# [Kw] the rescue keyword
attr_reader :keyword
# [nil | RescueEx] the exceptions being rescued
attr_reader :exception
# [Statements] the expressions to evaluate when an error is rescued
attr_reader :statements
# [nil | Rescue] the optional next clause in the chain
attr_reader :consequent
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(keyword:, exception:, statements:, consequent:, location:)
@keyword = keyword
@exception = exception
@statements = statements
@consequent = consequent
@location = location
@comments = []
end
def bind_end(end_char, end_column)
@location =
Location.new(
start_line: location.start_line,
start_char: location.start_char,
start_column: location.start_column,
end_line: location.end_line,
end_char: end_char,
end_column: end_column
)
if (next_node = consequent)
next_node.bind_end(end_char, end_column)
statements.bind_end(
next_node.location.start_char,
next_node.location.start_column
)
else
statements.bind_end(end_char, end_column)
end
end
def accept(visitor)
visitor.visit_rescue(self)
end
def child_nodes
[keyword, exception, statements, consequent]
end
def copy(
keyword: nil,
exception: nil,
statements: nil,
consequent: nil,
location: nil
)
node =
Rescue.new(
keyword: keyword || self.keyword,
exception: exception || self.exception,
statements: statements || self.statements,
consequent: consequent || self.consequent,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
keyword: keyword,
exception: exception,
statements: statements,
consequent: consequent,
location: location,
comments: comments
}
end
def format(q)
q.group do
q.format(keyword)
if exception
q.nest(keyword.value.length + 1) { q.format(exception) }
else
q.text(" StandardError")
end
unless statements.empty?
q.indent do
q.breakable_force
q.format(statements)
end
end
if consequent
q.breakable_force
q.format(consequent)
end
end
end
def ===(other)
other.is_a?(Rescue) && keyword === other.keyword &&
exception === other.exception && statements === other.statements &&
consequent === other.consequent
end
end
# RescueMod represents the use of the modifier form of a +rescue+ clause.
#
# expression rescue value
#
class RescueMod < Node
# [Node] the expression to execute
attr_reader :statement
# [Node] the value to use if the executed expression raises an error
attr_reader :value
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(statement:, value:, location:)
@statement = statement
@value = value
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_rescue_mod(self)
end
def child_nodes
[statement, value]
end
def copy(statement: nil, value: nil, location: nil)
node =
RescueMod.new(
statement: statement || self.statement,
value: value || self.value,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
statement: statement,
value: value,
location: location,
comments: comments
}
end
def format(q)
q.text("begin")
q.group do
q.indent do
q.breakable_force
q.format(statement)
end
q.breakable_force
q.text("rescue StandardError")
q.indent do
q.breakable_force
q.format(value)
end
q.breakable_force
end
q.text("end")
end
def ===(other)
other.is_a?(RescueMod) && statement === other.statement &&
value === other.value
end
end
# RestParam represents defining a parameter in a method definition that
# accepts all remaining positional parameters.
#
# def method(*rest) end
#
class RestParam < Node
# [nil | Ident] the name of the parameter
attr_reader :name
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(name:, location:)
@name = name
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_rest_param(self)
end
def child_nodes
[name]
end
def copy(name: nil, location: nil)
node =
RestParam.new(
name: name || self.name,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ name: name, location: location, comments: comments }
end
def format(q)
q.text("*")
q.format(name) if name
end
def ===(other)
other.is_a?(RestParam) && name === other.name
end
end
# Retry represents the use of the +retry+ keyword.
#
# retry
#
class Retry < Node
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(location:)
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_retry(self)
end
def child_nodes
[]
end
def copy(location: nil)
node = Retry.new(location: location || self.location)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ location: location, comments: comments }
end
def format(q)
q.text("retry")
end
def ===(other)
other.is_a?(Retry)
end
end
# Return represents using the +return+ keyword with arguments.
#
# return value
#
class ReturnNode < Node
# [nil | Args] the arguments being passed to the keyword
attr_reader :arguments
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(arguments:, location:)
@arguments = arguments
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_return(self)
end
def child_nodes
[arguments]
end
def copy(arguments: nil, location: nil)
node =
ReturnNode.new(
arguments: arguments || self.arguments,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ arguments: arguments, location: location, comments: comments }
end
def format(q)
FlowControlFormatter.new("return", self).format(q)
end
def ===(other)
other.is_a?(ReturnNode) && arguments === other.arguments
end
end
# RParen represents the use of a right parenthesis, i.e., +)+.
class RParen < Node
# [String] the parenthesis
attr_reader :value
def initialize(value:, location:)
@value = value
@location = location
end
def accept(visitor)
visitor.visit_rparen(self)
end
def child_nodes
[]
end
def copy(value: nil, location: nil)
RParen.new(
value: value || self.value,
location: location || self.location
)
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location }
end
def ===(other)
other.is_a?(RParen) && value === other.value
end
end
# SClass represents a block of statements that should be evaluated within the
# context of the singleton class of an object. It's frequently used to define
# singleton methods.
#
# class << self
# end
#
class SClass < Node
# [Node] the target of the singleton class to enter
attr_reader :target
# [BodyStmt] the expressions to be executed
attr_reader :bodystmt
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(target:, bodystmt:, location:)
@target = target
@bodystmt = bodystmt
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_sclass(self)
end
def child_nodes
[target, bodystmt]
end
def copy(target: nil, bodystmt: nil, location: nil)
node =
SClass.new(
target: target || self.target,
bodystmt: bodystmt || self.bodystmt,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
target: target,
bodystmt: bodystmt,
location: location,
comments: comments
}
end
def format(q)
q.text("class << ")
q.group do
q.format(target)
q.indent do
q.breakable_force
q.format(bodystmt)
end
q.breakable_force
end
q.text("end")
end
def ===(other)
other.is_a?(SClass) && target === other.target &&
bodystmt === other.bodystmt
end
end
# Everything that has a block of code inside of it has a list of statements.
# Normally we would just track those as a node that has an array body, but we
# have some special handling in order to handle empty statement lists. They
# need to have the right location information, so all of the parent node of
# stmts nodes will report back down the location information. We then
# propagate that onto void_stmt nodes inside the stmts in order to make sure
# all comments get printed appropriately.
class Statements < Node
# [Array[ Node ]] the list of expressions contained within this node
attr_reader :body
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(body:, location:)
@body = body
@location = location
@comments = []
end
def bind(parser, start_char, start_column, end_char, end_column)
@location =
Location.new(
start_line: location.start_line,
start_char: start_char,
start_column: start_column,
end_line: location.end_line,
end_char: end_char,
end_column: end_column
)
if (void_stmt = body[0]).is_a?(VoidStmt)
location = void_stmt.location
location =
Location.new(
start_line: location.start_line,
start_char: start_char,
start_column: start_column,
end_line: location.end_line,
end_char: start_char,
end_column: end_column
)
body[0] = VoidStmt.new(location: location)
end
attach_comments(parser, start_char, end_char)
end
def bind_end(end_char, end_column)
@location =
Location.new(
start_line: location.start_line,
start_char: location.start_char,
start_column: location.start_column,
end_line: location.end_line,
end_char: end_char,
end_column: end_column
)
end
def empty?
body.all? do |statement|
statement.is_a?(VoidStmt) && statement.comments.empty?
end
end
def accept(visitor)
visitor.visit_statements(self)
end
def child_nodes
body
end
def copy(body: nil, location: nil)
node =
Statements.new(
body: body || self.body,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ body: body, location: location, comments: comments }
end
def format(q)
line = nil
# This handles a special case where you've got a block of statements where
# the only value is a comment. In that case a lot of nodes like
# brace_block will attempt to format as a single line, but since that
# wouldn't work with a comment, we intentionally break the parent group.
if body.length == 2
void_stmt, comment = body
if void_stmt.is_a?(VoidStmt) && comment.is_a?(Comment)
q.format(comment)
q.break_parent
return
end
end
previous = nil
body.each do |statement|
next if statement.is_a?(VoidStmt)
if line.nil?
q.format(statement)
elsif (statement.location.start_line - line) > 1
q.breakable_force
q.breakable_force
q.format(statement)
elsif (statement.is_a?(VCall) && statement.access_control?) ||
(previous.is_a?(VCall) && previous.access_control?)
q.breakable_force
q.breakable_force
q.format(statement)
elsif statement.location.start_line != line
q.breakable_force
q.format(statement)
elsif !q.parent.is_a?(StringEmbExpr)
q.breakable_force
q.format(statement)
else
q.text("; ")
q.format(statement)
end
line = statement.location.end_line
previous = statement
end
end
def ===(other)
other.is_a?(Statements) && ArrayMatch.call(body, other.body)
end
private
# As efficiently as possible, gather up all of the comments that have been
# found while this statements list was being parsed and add them into the
# body.
def attach_comments(parser, start_char, end_char)
parser_comments = parser.comments
comment_index = 0
body_index = 0
while comment_index < parser_comments.size
comment = parser_comments[comment_index]
location = comment.location
if !comment.inline? && (start_char <= location.start_char) &&
(end_char >= location.end_char) && !comment.ignore?
while (node = body[body_index]) &&
(
node.is_a?(VoidStmt) ||
node.location.start_char < location.start_char
)
body_index += 1
end
if body_index != 0 &&
body[body_index - 1].location.start_char < location.start_char &&
body[body_index - 1].location.end_char > location.start_char
# The previous node entirely encapsules the comment, so we don't
# want to attach it here since it will get attached normally. This
# is mostly in the case of hash and array literals.
comment_index += 1
else
parser_comments.delete_at(comment_index)
body.insert(body_index, comment)
end
else
comment_index += 1
end
end
end
end
# StringContent represents the contents of a string-like value.
#
# "string"
#
class StringContent < Node
# [Array[ StringEmbExpr | StringDVar | TStringContent ]] the parts of the
# string
attr_reader :parts
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(parts:, location:)
@parts = parts
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_string_content(self)
end
def child_nodes
parts
end
def copy(parts: nil, location: nil)
StringContent.new(
parts: parts || self.parts,
location: location || self.location
)
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ parts: parts, location: location }
end
def ===(other)
other.is_a?(StringContent) && ArrayMatch.call(parts, other.parts)
end
def format(q)
q.text(q.quote)
q.group do
parts.each do |part|
if part.is_a?(TStringContent)
value = Quotes.normalize(part.value, q.quote)
first = true
value.each_line(chomp: true) do |line|
if first
first = false
else
q.breakable_return
end
q.text(line)
end
q.breakable_return if value.end_with?("\n")
else
q.format(part)
end
end
end
q.text(q.quote)
end
end
# StringConcat represents concatenating two strings together using a backward
# slash.
#
# "first" \
# "second"
#
class StringConcat < Node
# [Heredoc | StringConcat | StringLiteral] the left side of the
# concatenation
attr_reader :left
# [StringLiteral] the right side of the concatenation
attr_reader :right
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(left:, right:, location:)
@left = left
@right = right
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_string_concat(self)
end
def child_nodes
[left, right]
end
def copy(left: nil, right: nil, location: nil)
node =
StringConcat.new(
left: left || self.left,
right: right || self.right,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ left: left, right: right, location: location, comments: comments }
end
def format(q)
q.group do
q.format(left)
q.text(" \\")
q.indent do
q.breakable_force
q.format(right)
end
end
end
def ===(other)
other.is_a?(StringConcat) && left === other.left && right === other.right
end
end
# StringDVar represents shorthand interpolation of a variable into a string.
# It allows you to take an instance variable, class variable, or global
# variable and omit the braces when interpolating.
#
# "#@variable"
#
class StringDVar < Node
# [Backref | VarRef] the variable being interpolated
attr_reader :variable
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(variable:, location:)
@variable = variable
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_string_dvar(self)
end
def child_nodes
[variable]
end
def copy(variable: nil, location: nil)
node =
StringDVar.new(
variable: variable || self.variable,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ variable: variable, location: location, comments: comments }
end
def format(q)
q.text('#{')
q.format(variable)
q.text("}")
end
def ===(other)
other.is_a?(StringDVar) && variable === other.variable
end
end
# StringEmbExpr represents interpolated content. It can be contained within a
# couple of different parent nodes, including regular expressions, strings,
# and dynamic symbols.
#
# "string #{expression}"
#
class StringEmbExpr < Node
# [Statements] the expressions to be interpolated
attr_reader :statements
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(statements:, location:)
@statements = statements
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_string_embexpr(self)
end
def child_nodes
[statements]
end
def copy(statements: nil, location: nil)
node =
StringEmbExpr.new(
statements: statements || self.statements,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ statements: statements, location: location, comments: comments }
end
def format(q)
if location.start_line == location.end_line
# If the contents of this embedded expression were originally on the
# same line in the source, then we're going to leave them in place and
# assume that's the way the developer wanted this expression
# represented.
q.remove_breaks(
q.group do
q.text('#{')
q.format(statements)
q.text("}")
end
)
else
q.group do
q.text('#{')
q.indent do
q.breakable_empty
q.format(statements)
end
q.breakable_empty
q.text("}")
end
end
end
def ===(other)
other.is_a?(StringEmbExpr) && statements === other.statements
end
end
# StringLiteral represents a string literal.
#
# "string"
#
class StringLiteral < Node
# [Array[ StringEmbExpr | StringDVar | TStringContent ]] the parts of the
# string literal
attr_reader :parts
# [nil | String] which quote was used by the string literal
attr_reader :quote
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(parts:, quote:, location:)
@parts = parts
@quote = quote
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_string_literal(self)
end
def child_nodes
parts
end
def copy(parts: nil, quote: nil, location: nil)
node =
StringLiteral.new(
parts: parts || self.parts,
quote: quote || self.quote,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ parts: parts, quote: quote, location: location, comments: comments }
end
def format(q)
if parts.empty?
q.text("#{q.quote}#{q.quote}")
return
end
opening_quote, closing_quote =
if !Quotes.locked?(self, q.quote)
[q.quote, q.quote]
elsif quote&.start_with?("%")
[quote, Quotes.matching(quote[/%[qQ]?(.)/, 1])]
else
[quote, quote]
end
q.text(opening_quote)
q.group do
parts.each do |part|
if part.is_a?(TStringContent)
value = Quotes.normalize(part.value, closing_quote)
first = true
value.each_line(chomp: true) do |line|
if first
first = false
else
q.breakable_return
end
q.text(line)
end
q.breakable_return if value.end_with?("\n")
else
q.format(part)
end
end
end
q.text(closing_quote)
end
def ===(other)
other.is_a?(StringLiteral) && ArrayMatch.call(parts, other.parts) &&
quote === other.quote
end
end
# Super represents using the +super+ keyword with arguments. It can optionally
# use parentheses.
#
# super(value)
#
class Super < Node
# [ArgParen | Args] the arguments to the keyword
attr_reader :arguments
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(arguments:, location:)
@arguments = arguments
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_super(self)
end
def child_nodes
[arguments]
end
def copy(arguments: nil, location: nil)
node =
Super.new(
arguments: arguments || self.arguments,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ arguments: arguments, location: location, comments: comments }
end
def format(q)
q.group do
q.text("super")
if arguments.is_a?(ArgParen)
q.format(arguments)
else
q.text(" ")
q.nest("super ".length) { q.format(arguments) }
end
end
end
def ===(other)
other.is_a?(Super) && arguments === other.arguments
end
end
# SymBeg represents the beginning of a symbol literal.
#
# :symbol
#
# SymBeg is also used for dynamic symbols, as in:
#
# :"symbol"
#
# Finally, SymBeg is also used for symbols using the %s syntax, as in:
#
# %s[symbol]
#
# The value of this node is a string. In most cases (as in the first example
# above) it will contain just ":". In the case of dynamic symbols it will
# contain ":'" or ":\"". In the case of %s symbols, it will contain the start
# of the symbol including the %s and the delimiter.
class SymBeg < Node
# [String] the beginning of the symbol
attr_reader :value
def initialize(value:, location:)
@value = value
@location = location
end
def accept(visitor)
visitor.visit_symbeg(self)
end
def child_nodes
[]
end
def copy(value: nil, location: nil)
SymBeg.new(
value: value || self.value,
location: location || self.location
)
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location }
end
def ===(other)
other.is_a?(SymBeg) && value === other.value
end
end
# SymbolContent represents symbol contents and is always the child of a
# SymbolLiteral node.
#
# :symbol
#
class SymbolContent < Node
# [Backtick | Const | CVar | GVar | Ident | IVar | Kw | Op] the value of the
# symbol
attr_reader :value
def initialize(value:, location:)
@value = value
@location = location
end
def accept(visitor)
visitor.visit_symbol_content(self)
end
def child_nodes
[]
end
def copy(value: nil, location: nil)
SymbolContent.new(
value: value || self.value,
location: location || self.location
)
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location }
end
def ===(other)
other.is_a?(SymbolContent) && value === other.value
end
end
# SymbolLiteral represents a symbol in the system with no interpolation
# (as opposed to a DynaSymbol which has interpolation).
#
# :symbol
#
class SymbolLiteral < Node
# [Backtick | Const | CVar | GVar | Ident | IVar | Kw | Op | TStringContent]
# the value of the symbol
attr_reader :value
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(value:, location:)
@value = value
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_symbol_literal(self)
end
def child_nodes
[value]
end
def copy(value: nil, location: nil)
node =
SymbolLiteral.new(
value: value || self.value,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location, comments: comments }
end
def format(q)
q.text(":")
q.text("\\") if value.comments.any?
q.format(value)
end
def ===(other)
other.is_a?(SymbolLiteral) && value === other.value
end
end
# Symbols represents a symbol array literal with interpolation.
#
# %I[one two three]
#
class Symbols < Node
# [SymbolsBeg] the token that opens this array literal
attr_reader :beginning
# [Array[ Word ]] the words in the symbol array literal
attr_reader :elements
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(beginning:, elements:, location:)
@beginning = beginning
@elements = elements
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_symbols(self)
end
def child_nodes
[]
end
def copy(beginning: nil, elements: nil, location: nil)
Symbols.new(
beginning: beginning || self.beginning,
elements: elements || self.elements,
location: location || self.location
)
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
beginning: beginning,
elements: elements,
location: location,
comments: comments
}
end
def format(q)
opening, closing = "%I[", "]"
if elements.any? { |element| element.match?(/[\[\]]/) }
opening = beginning.value
closing = Quotes.matching(opening[2])
end
q.text(opening)
q.group do
q.indent do
q.breakable_empty
q.seplist(
elements,
ArrayLiteral::BREAKABLE_SPACE_SEPARATOR
) { |element| q.format(element) }
end
q.breakable_empty
end
q.text(closing)
end
def ===(other)
other.is_a?(Symbols) && beginning === other.beginning &&
ArrayMatch.call(elements, other.elements)
end
end
# SymbolsBeg represents the start of a symbol array literal with
# interpolation.
#
# %I[one two three]
#
# In the snippet above, SymbolsBeg represents the "%I[" token. Note that these
# kinds of arrays can start with a lot of different delimiter types
# (e.g., %I| or %I<).
class SymbolsBeg < Node
# [String] the beginning of the symbol literal array
attr_reader :value
def initialize(value:, location:)
@value = value
@location = location
end
def accept(visitor)
visitor.visit_symbols_beg(self)
end
def child_nodes
[]
end
def copy(value: nil, location: nil)
SymbolsBeg.new(
value: value || self.value,
location: location || self.location
)
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location }
end
def ===(other)
other.is_a?(SymbolsBeg) && value === other.value
end
end
# TLambda represents the beginning of a lambda literal.
#
# -> { value }
#
# In the example above the TLambda represents the +->+ operator.
class TLambda < Node
# [String] the beginning of the lambda literal
attr_reader :value
def initialize(value:, location:)
@value = value
@location = location
end
def accept(visitor)
visitor.visit_tlambda(self)
end
def child_nodes
[]
end
def copy(value: nil, location: nil)
TLambda.new(
value: value || self.value,
location: location || self.location
)
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location }
end
def ===(other)
other.is_a?(TLambda) && value === other.value
end
end
# TLamBeg represents the beginning of the body of a lambda literal using
# braces.
#
# -> { value }
#
# In the example above the TLamBeg represents the +{+ operator.
class TLamBeg < Node
# [String] the beginning of the body of the lambda literal
attr_reader :value
def initialize(value:, location:)
@value = value
@location = location
end
def accept(visitor)
visitor.visit_tlambeg(self)
end
def child_nodes
[]
end
def copy(value: nil, location: nil)
TLamBeg.new(
value: value || self.value,
location: location || self.location
)
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location }
end
def ===(other)
other.is_a?(TLamBeg) && value === other.value
end
end
# TopConstField is always the child node of some kind of assignment. It
# represents when you're assigning to a constant that is being referenced at
# the top level.
#
# ::Constant = value
#
class TopConstField < Node
# [Const] the constant being assigned
attr_reader :constant
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(constant:, location:)
@constant = constant
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_top_const_field(self)
end
def child_nodes
[constant]
end
def copy(constant: nil, location: nil)
node =
TopConstField.new(
constant: constant || self.constant,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ constant: constant, location: location, comments: comments }
end
def format(q)
q.text("::")
q.format(constant)
end
def ===(other)
other.is_a?(TopConstField) && constant === other.constant
end
end
# TopConstRef is very similar to TopConstField except that it is not involved
# in an assignment.
#
# ::Constant
#
class TopConstRef < Node
# [Const] the constant being referenced
attr_reader :constant
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(constant:, location:)
@constant = constant
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_top_const_ref(self)
end
def child_nodes
[constant]
end
def copy(constant: nil, location: nil)
node =
TopConstRef.new(
constant: constant || self.constant,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ constant: constant, location: location, comments: comments }
end
def format(q)
q.text("::")
q.format(constant)
end
def ===(other)
other.is_a?(TopConstRef) && constant === other.constant
end
end
# TStringBeg represents the beginning of a string literal.
#
# "string"
#
# In the example above, TStringBeg represents the first set of quotes. Strings
# can also use single quotes. They can also be declared using the +%q+ and
# +%Q+ syntax, as in:
#
# %q{string}
#
class TStringBeg < Node
# [String] the beginning of the string
attr_reader :value
def initialize(value:, location:)
@value = value
@location = location
end
def accept(visitor)
visitor.visit_tstring_beg(self)
end
def child_nodes
[]
end
def copy(value: nil, location: nil)
TStringBeg.new(
value: value || self.value,
location: location || self.location
)
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location }
end
def ===(other)
other.is_a?(TStringBeg) && value === other.value
end
end
# TStringContent represents plain characters inside of an entity that accepts
# string content like a string, heredoc, command string, or regular
# expression.
#
# "string"
#
# In the example above, TStringContent represents the +string+ token contained
# within the string.
class TStringContent < Node
# [String] the content of the string
attr_reader :value
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(value:, location:)
@value = value
@location = location
@comments = []
end
def match?(pattern)
value.match?(pattern)
end
def accept(visitor)
visitor.visit_tstring_content(self)
end
def child_nodes
[]
end
def copy(value: nil, location: nil)
node =
TStringContent.new(
value: value || self.value,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location, comments: comments }
end
def format(q)
q.text(value)
end
def ===(other)
other.is_a?(TStringContent) && value === other.value
end
end
# TStringEnd represents the end of a string literal.
#
# "string"
#
# In the example above, TStringEnd represents the second set of quotes.
# Strings can also use single quotes. They can also be declared using the +%q+
# and +%Q+ syntax, as in:
#
# %q{string}
#
class TStringEnd < Node
# [String] the end of the string
attr_reader :value
def initialize(value:, location:)
@value = value
@location = location
end
def accept(visitor)
visitor.visit_tstring_end(self)
end
def child_nodes
[]
end
def copy(value: nil, location: nil)
TStringEnd.new(
value: value || self.value,
location: location || self.location
)
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location }
end
def ===(other)
other.is_a?(TStringEnd) && value === other.value
end
end
# Not represents the unary +not+ method being called on an expression.
#
# not value
#
class Not < Node
# [nil | Node] the statement on which to operate
attr_reader :statement
# [boolean] whether or not parentheses were used
attr_reader :parentheses
alias parentheses? parentheses
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(statement:, parentheses:, location:)
@statement = statement
@parentheses = parentheses
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_not(self)
end
def child_nodes
[statement]
end
def copy(statement: nil, parentheses: nil, location: nil)
node =
Not.new(
statement: statement || self.statement,
parentheses: parentheses || self.parentheses,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
statement: statement,
parentheses: parentheses,
location: location,
comments: comments
}
end
def format(q)
q.text("not")
if parentheses
q.text("(")
q.format(statement) if statement
q.text(")")
else
grandparent = q.grandparent
ternary =
(grandparent.is_a?(IfNode) || grandparent.is_a?(UnlessNode)) &&
Ternaryable.call(q, grandparent)
if ternary
q.if_break { q.text(" ") }.if_flat { q.text("(") }
q.format(statement) if statement
q.if_flat { q.text(")") } if ternary
else
q.text(" ")
q.format(statement) if statement
end
end
end
def ===(other)
other.is_a?(Not) && statement === other.statement &&
parentheses === other.parentheses
end
end
# Unary represents a unary method being called on an expression, as in +!+ or
# +~+.
#
# !value
#
class Unary < Node
# [String] the operator being used
attr_reader :operator
# [Node] the statement on which to operate
attr_reader :statement
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(operator:, statement:, location:)
@operator = operator
@statement = statement
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_unary(self)
end
def child_nodes
[statement]
end
def copy(operator: nil, statement: nil, location: nil)
node =
Unary.new(
operator: operator || self.operator,
statement: statement || self.statement,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
operator: operator,
statement: statement,
location: location,
comments: comments
}
end
def format(q)
q.text(operator)
q.format(statement)
end
def ===(other)
other.is_a?(Unary) && operator === other.operator &&
statement === other.statement
end
end
# Undef represents the use of the +undef+ keyword.
#
# undef method
#
class Undef < Node
# Undef accepts a variable number of arguments that can be either DynaSymbol
# or SymbolLiteral objects. For SymbolLiteral objects we descend directly
# into the value in order to have it come out as bare words.
class UndefArgumentFormatter
# [DynaSymbol | SymbolLiteral] the symbol to undefine
attr_reader :node
def initialize(node)
@node = node
end
def comments
if node.is_a?(SymbolLiteral)
node.comments + node.value.comments
else
node.comments
end
end
def format(q)
node.is_a?(SymbolLiteral) ? q.format(node.value) : q.format(node)
end
end
# [Array[ DynaSymbol | SymbolLiteral ]] the symbols to undefine
attr_reader :symbols
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(symbols:, location:)
@symbols = symbols
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_undef(self)
end
def child_nodes
symbols
end
def copy(symbols: nil, location: nil)
node =
Undef.new(
symbols: symbols || self.symbols,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ symbols: symbols, location: location, comments: comments }
end
def format(q)
keyword = "undef "
formatters = symbols.map { |symbol| UndefArgumentFormatter.new(symbol) }
q.group do
q.text(keyword)
q.nest(keyword.length) do
q.seplist(formatters) { |formatter| q.format(formatter) }
end
end
end
def ===(other)
other.is_a?(Undef) && ArrayMatch.call(symbols, other.symbols)
end
end
# Unless represents the first clause in an +unless+ chain.
#
# unless predicate
# end
#
class UnlessNode < Node
# [Node] the expression to be checked
attr_reader :predicate
# [Statements] the expressions to be executed
attr_reader :statements
# [nil | Elsif | Else] the next clause in the chain
attr_reader :consequent
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(predicate:, statements:, consequent:, location:)
@predicate = predicate
@statements = statements
@consequent = consequent
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_unless(self)
end
def child_nodes
[predicate, statements, consequent]
end
def copy(predicate: nil, statements: nil, consequent: nil, location: nil)
node =
UnlessNode.new(
predicate: predicate || self.predicate,
statements: statements || self.statements,
consequent: consequent || self.consequent,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
predicate: predicate,
statements: statements,
consequent: consequent,
location: location,
comments: comments
}
end
def format(q)
ConditionalFormatter.new("unless", self).format(q)
end
def ===(other)
other.is_a?(UnlessNode) && predicate === other.predicate &&
statements === other.statements && consequent === other.consequent
end
# Checks if the node was originally found in the modifier form.
def modifier?
predicate.location.start_char > statements.location.start_char
end
end
# Formats an Until or While node.
class LoopFormatter
# [String] the name of the keyword used for this loop
attr_reader :keyword
# [Until | While] the node that is being formatted
attr_reader :node
def initialize(keyword, node)
@keyword = keyword
@node = node
end
def format(q)
# If we're in the modifier form and we're modifying a `begin`, then this
# is a special case where we need to explicitly use the modifier form
# because otherwise the semantic meaning changes. This looks like:
#
# begin
# foo
# end while bar
#
# Also, if the statement of the modifier includes an assignment, then we
# can't know for certain that it won't impact the predicate, so we need to
# force it to stay as it is. This looks like:
#
# foo = bar while foo
#
if node.modifier? && (statement = node.statements.body.first) &&
(statement.is_a?(Begin) || ContainsAssignment.call(statement))
q.format(statement)
q.text(" #{keyword} ")
q.format(node.predicate)
elsif node.statements.empty?
q.group do
q.text("#{keyword} ")
q.nest(keyword.length + 1) { q.format(node.predicate) }
q.breakable_force
q.text("end")
end
elsif ContainsAssignment.call(node.predicate)
format_break(q)
q.break_parent
else
q.group do
q
.if_break { format_break(q) }
.if_flat do
Parentheses.flat(q) do
q.format(node.statements)
q.text(" #{keyword} ")
q.format(node.predicate)
end
end
end
end
end
private
def format_break(q)
q.text("#{keyword} ")
q.nest(keyword.length + 1) { q.format(node.predicate) }
q.indent do
q.breakable_empty
q.format(node.statements)
end
q.breakable_empty
q.text("end")
end
end
# Until represents an +until+ loop.
#
# until predicate
# end
#
class UntilNode < Node
# [Node] the expression to be checked
attr_reader :predicate
# [Statements] the expressions to be executed
attr_reader :statements
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(predicate:, statements:, location:)
@predicate = predicate
@statements = statements
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_until(self)
end
def child_nodes
[predicate, statements]
end
def copy(predicate: nil, statements: nil, location: nil)
node =
UntilNode.new(
predicate: predicate || self.predicate,
statements: statements || self.statements,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
predicate: predicate,
statements: statements,
location: location,
comments: comments
}
end
def format(q)
LoopFormatter.new("until", self).format(q)
end
def ===(other)
other.is_a?(UntilNode) && predicate === other.predicate &&
statements === other.statements
end
def modifier?
predicate.location.start_char > statements.location.start_char
end
end
# VarField represents a variable that is being assigned a value. As such, it
# is always a child of an assignment type node.
#
# variable = value
#
# In the example above, the VarField node represents the +variable+ token.
class VarField < Node
# [nil | :nil | Const | CVar | GVar | Ident | IVar] the target of this node
attr_reader :value
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(value:, location:)
@value = value
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_var_field(self)
end
def child_nodes
value == :nil ? [] : [value]
end
def copy(value: nil, location: nil)
node =
VarField.new(
value: value || self.value,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location, comments: comments }
end
def format(q)
if value == :nil
q.text("nil")
elsif value
q.format(value)
end
end
def ===(other)
other.is_a?(VarField) && value === other.value
end
end
# VarRef represents a variable reference.
#
# true
#
# This can be a plain local variable like the example above. It can also be a
# constant, a class variable, a global variable, an instance variable, a
# keyword (like +self+, +nil+, +true+, or +false+), or a numbered block
# variable.
class VarRef < Node
# [Const | CVar | GVar | Ident | IVar | Kw] the value of this node
attr_reader :value
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(value:, location:)
@value = value
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_var_ref(self)
end
def child_nodes
[value]
end
def copy(value: nil, location: nil)
node =
VarRef.new(
value: value || self.value,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location, comments: comments }
end
def format(q)
q.format(value)
end
def ===(other)
other.is_a?(VarRef) && value === other.value
end
# Oh man I hate this so much. Basically, ripper doesn't provide enough
# functionality to actually know where pins are within an expression. So we
# have to walk the tree ourselves and insert more information. In doing so,
# we have to replace this node by a pinned node when necessary.
#
# To be clear, this method should just not exist. It's not good. It's a
# place of shame. But it's necessary for now, so I'm keeping it.
def pin(parent, pin)
replace =
PinnedVarRef.new(value: value, location: pin.location.to(location))
parent
.deconstruct_keys([])
.each do |key, value|
if value == self
parent.instance_variable_set(:"@#{key}", replace)
break
elsif value.is_a?(Array) && (index = value.index(self))
parent.public_send(key)[index] = replace
break
elsif value.is_a?(Array) &&
(index = value.index { |(_k, v)| v == self })
parent.public_send(key)[index][1] = replace
break
end
end
end
end
# PinnedVarRef represents a pinned variable reference within a pattern
# matching pattern.
#
# case value
# in ^variable
# end
#
# This can be a plain local variable like the example above. It can also be a
# a class variable, a global variable, or an instance variable.
class PinnedVarRef < Node
# [Const | CVar | GVar | Ident | IVar] the value of this node
attr_reader :value
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(value:, location:)
@value = value
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_pinned_var_ref(self)
end
def child_nodes
[value]
end
def copy(value: nil, location: nil)
node =
PinnedVarRef.new(
value: value || self.value,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location, comments: comments }
end
def format(q)
q.group do
q.text("^")
q.format(value)
end
end
def ===(other)
other.is_a?(PinnedVarRef) && value === other.value
end
end
# VCall represent any plain named object with Ruby that could be either a
# local variable or a method call.
#
# variable
#
class VCall < Node
# [Ident] the value of this expression
attr_reader :value
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(value:, location:)
@value = value
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_vcall(self)
end
def child_nodes
[value]
end
def copy(value: nil, location: nil)
node =
VCall.new(
value: value || self.value,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location, comments: comments }
end
def format(q)
q.format(value)
end
def ===(other)
other.is_a?(VCall) && value === other.value
end
def access_control?
@access_control ||= %w[private protected public].include?(value.value)
end
def arity
0
end
end
# VoidStmt represents an empty lexical block of code.
#
# ;;
#
class VoidStmt < Node
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(location:)
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_void_stmt(self)
end
def child_nodes
[]
end
def copy(location: nil)
node = VoidStmt.new(location: location || self.location)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ location: location, comments: comments }
end
def format(q)
end
def ===(other)
other.is_a?(VoidStmt)
end
end
# When represents a +when+ clause in a +case+ chain.
#
# case value
# when predicate
# end
#
class When < Node
# [Args] the arguments to the when clause
attr_reader :arguments
# [Statements] the expressions to be executed
attr_reader :statements
# [nil | Else | When] the next clause in the chain
attr_reader :consequent
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(arguments:, statements:, consequent:, location:)
@arguments = arguments
@statements = statements
@consequent = consequent
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_when(self)
end
def child_nodes
[arguments, statements, consequent]
end
def copy(arguments: nil, statements: nil, consequent: nil, location: nil)
node =
When.new(
arguments: arguments || self.arguments,
statements: statements || self.statements,
consequent: consequent || self.consequent,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
arguments: arguments,
statements: statements,
consequent: consequent,
location: location,
comments: comments
}
end
# We have a special separator here for when clauses which causes them to
# fill as much of the line as possible as opposed to everything breaking
# into its own line as soon as you hit the print limit.
class Separator
def call(q)
q.group do
q.text(",")
q.breakable_space
end
end
end
# We're going to keep a single instance of this separator around so we don't
# have to allocate a new one every time we format a when clause.
SEPARATOR = Separator.new.freeze
def format(q)
keyword = "when "
q.group do
q.group do
q.text(keyword)
q.nest(keyword.length) do
if arguments.comments.any?
q.format(arguments)
else
q.seplist(arguments.parts, SEPARATOR) { |part| q.format(part) }
end
# Very special case here. If you're inside of a when clause and the
# last argument to the predicate is and endless range, then you are
# forced to use the "then" keyword to make it parse properly.
last = arguments.parts.last
q.text(" then") if last.is_a?(RangeNode) && !last.right
end
end
unless statements.empty?
q.indent do
q.breakable_force
q.format(statements)
end
end
if consequent
q.breakable_force
q.format(consequent)
end
end
end
def ===(other)
other.is_a?(When) && arguments === other.arguments &&
statements === other.statements && consequent === other.consequent
end
end
# While represents a +while+ loop.
#
# while predicate
# end
#
class WhileNode < Node
# [Node] the expression to be checked
attr_reader :predicate
# [Statements] the expressions to be executed
attr_reader :statements
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(predicate:, statements:, location:)
@predicate = predicate
@statements = statements
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_while(self)
end
def child_nodes
[predicate, statements]
end
def copy(predicate: nil, statements: nil, location: nil)
node =
WhileNode.new(
predicate: predicate || self.predicate,
statements: statements || self.statements,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
predicate: predicate,
statements: statements,
location: location,
comments: comments
}
end
def format(q)
LoopFormatter.new("while", self).format(q)
end
def ===(other)
other.is_a?(WhileNode) && predicate === other.predicate &&
statements === other.statements
end
def modifier?
predicate.location.start_char > statements.location.start_char
end
end
# Word represents an element within a special array literal that accepts
# interpolation.
#
# %W[a#{b}c xyz]
#
# In the example above, there would be two Word nodes within a parent Words
# node.
class Word < Node
# [Array[ StringEmbExpr | StringDVar | TStringContent ]] the parts of the
# word
attr_reader :parts
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(parts:, location:)
@parts = parts
@location = location
@comments = []
end
def match?(pattern)
parts.any? { |part| part.is_a?(TStringContent) && part.match?(pattern) }
end
def accept(visitor)
visitor.visit_word(self)
end
def child_nodes
parts
end
def copy(parts: nil, location: nil)
node =
Word.new(
parts: parts || self.parts,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ parts: parts, location: location, comments: comments }
end
def format(q)
q.format_each(parts)
end
def ===(other)
other.is_a?(Word) && ArrayMatch.call(parts, other.parts)
end
end
# Words represents a string literal array with interpolation.
#
# %W[one two three]
#
class Words < Node
# [WordsBeg] the token that opens this array literal
attr_reader :beginning
# [Array[ Word ]] the elements of this array
attr_reader :elements
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(beginning:, elements:, location:)
@beginning = beginning
@elements = elements
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_words(self)
end
def child_nodes
[]
end
def copy(beginning: nil, elements: nil, location: nil)
Words.new(
beginning: beginning || self.beginning,
elements: elements || self.elements,
location: location || self.location
)
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{
beginning: beginning,
elements: elements,
location: location,
comments: comments
}
end
def format(q)
opening, closing = "%W[", "]"
if elements.any? { |element| element.match?(/[\[\]]/) }
opening = beginning.value
closing = Quotes.matching(opening[2])
end
q.text(opening)
q.group do
q.indent do
q.breakable_empty
q.seplist(
elements,
ArrayLiteral::BREAKABLE_SPACE_SEPARATOR
) { |element| q.format(element) }
end
q.breakable_empty
end
q.text(closing)
end
def ===(other)
other.is_a?(Words) && beginning === other.beginning &&
ArrayMatch.call(elements, other.elements)
end
end
# WordsBeg represents the beginning of a string literal array with
# interpolation.
#
# %W[one two three]
#
# In the snippet above, a WordsBeg would be created with the value of "%W[".
# Note that these kinds of arrays can start with a lot of different delimiter
# types (e.g., %W| or %W<).
class WordsBeg < Node
# [String] the start of the word literal array
attr_reader :value
def initialize(value:, location:)
@value = value
@location = location
end
def accept(visitor)
visitor.visit_words_beg(self)
end
def child_nodes
[]
end
def copy(value: nil, location: nil)
WordsBeg.new(
value: value || self.value,
location: location || self.location
)
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ value: value, location: location }
end
def ===(other)
other.is_a?(WordsBeg) && value === other.value
end
end
# XString represents the contents of an XStringLiteral.
#
# `ls`
#
class XString < Node
# [Array[ StringEmbExpr | StringDVar | TStringContent ]] the parts of the
# xstring
attr_reader :parts
def initialize(parts:, location:)
@parts = parts
@location = location
end
def accept(visitor)
visitor.visit_xstring(self)
end
def child_nodes
parts
end
def copy(parts: nil, location: nil)
XString.new(
parts: parts || self.parts,
location: location || self.location
)
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ parts: parts, location: location }
end
def ===(other)
other.is_a?(XString) && ArrayMatch.call(parts, other.parts)
end
end
# XStringLiteral represents a string that gets executed.
#
# `ls`
#
class XStringLiteral < Node
# [Array[ StringEmbExpr | StringDVar | TStringContent ]] the parts of the
# xstring
attr_reader :parts
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(parts:, location:)
@parts = parts
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_xstring_literal(self)
end
def child_nodes
parts
end
def copy(parts: nil, location: nil)
node =
XStringLiteral.new(
parts: parts || self.parts,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ parts: parts, location: location, comments: comments }
end
def format(q)
q.text("`")
q.format_each(parts)
q.text("`")
end
def ===(other)
other.is_a?(XStringLiteral) && ArrayMatch.call(parts, other.parts)
end
end
# Yield represents using the +yield+ keyword with arguments.
#
# yield value
#
class YieldNode < Node
# [nil | Args | Paren] the arguments passed to the yield
attr_reader :arguments
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(arguments:, location:)
@arguments = arguments
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_yield(self)
end
def child_nodes
[arguments]
end
def copy(arguments: nil, location: nil)
node =
YieldNode.new(
arguments: arguments || self.arguments,
location: location || self.location
)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ arguments: arguments, location: location, comments: comments }
end
def format(q)
if arguments.nil?
q.text("yield")
return
end
q.group do
q.text("yield")
if arguments.is_a?(Paren)
q.format(arguments)
else
q.if_break { q.text("(") }.if_flat { q.text(" ") }
q.indent do
q.breakable_empty
q.format(arguments)
end
q.breakable_empty
q.if_break { q.text(")") }
end
end
end
def ===(other)
other.is_a?(YieldNode) && arguments === other.arguments
end
end
# ZSuper represents the bare +super+ keyword with no arguments.
#
# super
#
class ZSuper < Node
# [Array[ Comment | EmbDoc ]] the comments attached to this node
attr_reader :comments
def initialize(location:)
@location = location
@comments = []
end
def accept(visitor)
visitor.visit_zsuper(self)
end
def child_nodes
[]
end
def copy(location: nil)
node = ZSuper.new(location: location || self.location)
node.comments.concat(comments.map(&:copy))
node
end
alias deconstruct child_nodes
def deconstruct_keys(_keys)
{ location: location, comments: comments }
end
def format(q)
q.text("super")
end
def ===(other)
other.is_a?(ZSuper)
end
end
end