зеркало из https://github.com/github/ruby.git
9235 строки
212 KiB
Plaintext
9235 строки
212 KiB
Plaintext
# frozen_string_literal: true
|
||
|
||
require 'ripper'
|
||
require_relative 'syntax_tree/version'
|
||
|
||
class SyntaxTree < Ripper
|
||
# Represents a line in the source. If this class is being used, it means that
|
||
# every character in the string is 1 byte in length, so we can just return the
|
||
# start of the line + the index.
|
||
class SingleByteString
|
||
def initialize(start)
|
||
@start = start
|
||
end
|
||
|
||
def [](byteindex)
|
||
@start + byteindex
|
||
end
|
||
end
|
||
|
||
# Represents a line in the source. If this class is being used, it means that
|
||
# there are characters in the string that are multi-byte, so we will build up
|
||
# an array of indices, such that array[byteindex] will be equal to the index
|
||
# of the character within the string.
|
||
class MultiByteString
|
||
def initialize(start, line)
|
||
@indices = []
|
||
|
||
line
|
||
.each_char
|
||
.with_index(start) do |char, index|
|
||
char.bytesize.times { @indices << index }
|
||
end
|
||
end
|
||
|
||
def [](byteindex)
|
||
@indices[byteindex]
|
||
end
|
||
end
|
||
|
||
# Represents the location of a node in the tree from the source code.
|
||
class Location
|
||
attr_reader :start_line, :start_char, :end_line, :end_char
|
||
|
||
def initialize(start_line:, start_char:, end_line:, end_char:)
|
||
@start_line = start_line
|
||
@start_char = start_char
|
||
@end_line = end_line
|
||
@end_char = end_char
|
||
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,
|
||
end_line: other.end_line,
|
||
end_char: other.end_char
|
||
)
|
||
end
|
||
|
||
def to_json(*opts)
|
||
[start_line, start_char, end_line, end_char].to_json(*opts)
|
||
end
|
||
|
||
def self.token(line:, char:, size:)
|
||
new(
|
||
start_line: line,
|
||
start_char: char,
|
||
end_line: line,
|
||
end_char: char + size
|
||
)
|
||
end
|
||
|
||
def self.fixed(line:, char:)
|
||
new(start_line: line, start_char: char, end_line: line, end_char: char)
|
||
end
|
||
end
|
||
|
||
# A special parser error so that we can get nice syntax displays on the error
|
||
# message when prettier prints out the results.
|
||
class ParseError < StandardError
|
||
attr_reader :lineno, :column
|
||
|
||
def initialize(error, lineno, column)
|
||
super(error)
|
||
@lineno = lineno
|
||
@column = column
|
||
end
|
||
end
|
||
|
||
attr_reader :source, :lines, :tokens
|
||
|
||
# This is an attr_accessor so Stmts objects can grab comments out of this
|
||
# array and attach them to themselves.
|
||
attr_accessor :comments
|
||
|
||
def initialize(source, *)
|
||
super
|
||
|
||
# We keep the source around so that we can refer back to it when we're
|
||
# generating the AST. Sometimes it's easier to just reference the source
|
||
# string when you want to check if it contains a certain character, for
|
||
# example.
|
||
@source = source
|
||
|
||
# Similarly, we keep the lines of the source string around to be able to
|
||
# check if certain lines contain certain characters. For example, we'll use
|
||
# this to generate the content that goes after the __END__ keyword. Or we'll
|
||
# use this to check if a comment has other content on its line.
|
||
@lines = source.split("\n")
|
||
|
||
# This is the full set of comments that have been found by the parser. It's
|
||
# a running list. At the end of every block of statements, they will go in
|
||
# and attempt to grab any comments that are on their own line and turn them
|
||
# into regular statements. So at the end of parsing the only comments left
|
||
# in here will be comments on lines that also contain code.
|
||
@comments = []
|
||
|
||
# This is the current embdoc (comments that start with =begin and end with
|
||
# =end). Since they can't be nested, there's no need for a stack here, as
|
||
# there can only be one active. These end up getting dumped into the
|
||
# comments list before getting picked up by the statements that surround
|
||
# them.
|
||
@embdoc = nil
|
||
|
||
# This is an optional node that can be present if the __END__ keyword is
|
||
# used in the file. In that case, this will represent the content after that
|
||
# keyword.
|
||
@__end__ = nil
|
||
|
||
# Heredocs can actually be nested together if you're using interpolation, so
|
||
# this is a stack of heredoc nodes that are currently being created. When we
|
||
# get to the token that finishes off a heredoc node, we pop the top
|
||
# one off. If there are others surrounding it, then the body events will now
|
||
# be added to the correct nodes.
|
||
@heredocs = []
|
||
|
||
# This is a running list of tokens that have fired. It's useful
|
||
# mostly for maintaining location information. For example, if you're inside
|
||
# the handle of a def event, then in order to determine where the AST node
|
||
# started, you need to look backward in the tokens to find a def
|
||
# keyword. Most of the time, when a parser event consumes one of these
|
||
# events, it will be deleted from the list. So ideally, this list stays
|
||
# pretty short over the course of parsing a source string.
|
||
@tokens = []
|
||
|
||
# Here we're going to build up a list of SingleByteString or MultiByteString
|
||
# objects. They're each going to represent a string in the source. They are
|
||
# used by the `char_pos` method to determine where we are in the source
|
||
# string.
|
||
@line_counts = []
|
||
last_index = 0
|
||
|
||
@source.lines.each do |line|
|
||
if line.size == line.bytesize
|
||
@line_counts << SingleByteString.new(last_index)
|
||
else
|
||
@line_counts << MultiByteString.new(last_index, line)
|
||
end
|
||
|
||
last_index += line.size
|
||
end
|
||
end
|
||
|
||
def self.parse(source)
|
||
parser = new(source)
|
||
response = parser.parse
|
||
response unless parser.error?
|
||
end
|
||
|
||
private
|
||
|
||
# ----------------------------------------------------------------------------
|
||
# :section: Helper methods
|
||
# The following methods are used by the ripper event handlers to either
|
||
# determine their bounds or query other nodes.
|
||
# ----------------------------------------------------------------------------
|
||
|
||
# This represents the current place in the source string that we've gotten to
|
||
# so far. We have a memoized line_counts object that we can use to get the
|
||
# number of characters that we've had to go through to get to the beginning of
|
||
# this line, then we add the number of columns into this line that we've gone
|
||
# through.
|
||
def char_pos
|
||
@line_counts[lineno - 1][column]
|
||
end
|
||
|
||
# As we build up a list of tokens, we'll periodically need to go backwards and
|
||
# find the ones that we've already hit in order to determine the location
|
||
# information for nodes that use them. For example, if you have a module node
|
||
# then you'll look backward for a kw token to determine your start location.
|
||
#
|
||
# This works with nesting since we're deleting tokens from the list once
|
||
# they've been used up. For example if you had nested module declarations then
|
||
# the innermost declaration would grab the last kw node that matches "module"
|
||
# (which would happen to be the innermost keyword). Then the outer one would
|
||
# only be able to grab the first one. In this way all of the tokens act as
|
||
# their own stack.
|
||
def find_token(type, value = :any, consume: true)
|
||
index =
|
||
tokens.rindex do |token|
|
||
token.is_a?(type) && (value == :any || (token.value == value))
|
||
end
|
||
|
||
if consume
|
||
# If we're expecting to be able to find a token and consume it,
|
||
# but can't actually find it, then we need to raise an error. This is
|
||
# _usually_ caused by a syntax error in the source that we're printing. It
|
||
# could also be caused by accidentally attempting to consume a token twice
|
||
# by two different parser event handlers.
|
||
unless index
|
||
message = "Cannot find expected #{value == :any ? type : value}"
|
||
raise ParseError.new(message, lineno, column)
|
||
end
|
||
|
||
tokens.delete_at(index)
|
||
elsif index
|
||
tokens[index]
|
||
end
|
||
end
|
||
|
||
# A helper function to find a :: operator. We do special handling instead of
|
||
# using find_token here because we don't pop off all of the ::
|
||
# operators so you could end up getting the wrong information if you have for
|
||
# instance ::X::Y::Z.
|
||
def find_colon2_before(const)
|
||
index =
|
||
tokens.rindex do |token|
|
||
token.is_a?(Op) && token.value == '::' &&
|
||
token.location.start_char < const.location.start_char
|
||
end
|
||
|
||
tokens[index]
|
||
end
|
||
|
||
# Finds the next position in the source string that begins a statement. This
|
||
# is used to bind statements lists and make sure they don't include a
|
||
# preceding comment. For example, we want the following comment to be attached
|
||
# to the class node and not the statement node:
|
||
#
|
||
# class Foo # :nodoc:
|
||
# ...
|
||
# end
|
||
#
|
||
# By finding the next non-space character, we can make sure that the bounds of
|
||
# the statement list are correct.
|
||
def find_next_statement_start(position)
|
||
remaining = source[position..-1]
|
||
|
||
if remaining.sub(/\A +/, '')[0] == '#'
|
||
return position + remaining.index("\n")
|
||
end
|
||
|
||
position
|
||
end
|
||
|
||
# ----------------------------------------------------------------------------
|
||
# :section: Ripper event handlers
|
||
# The following methods all handle a dispatched ripper event.
|
||
# ----------------------------------------------------------------------------
|
||
|
||
# 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
|
||
# [LBrace] the left brace that is seen after the keyword
|
||
attr_reader :lbrace
|
||
|
||
# [Statements] the expressions to be executed
|
||
attr_reader :statements
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(lbrace:, statements:, location:)
|
||
@lbrace = lbrace
|
||
@statements = statements
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('BEGIN')
|
||
q.breakable
|
||
q.pp(statements)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :BEGIN,
|
||
lbrace: lbrace,
|
||
stmts: statements,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_BEGIN: (Statements statements) -> BEGINBlock
|
||
def on_BEGIN(statements)
|
||
lbrace = find_token(LBrace)
|
||
rbrace = find_token(RBrace)
|
||
|
||
statements.bind(
|
||
find_next_statement_start(lbrace.location.end_char),
|
||
rbrace.location.start_char
|
||
)
|
||
|
||
keyword = find_token(Kw, 'BEGIN')
|
||
|
||
BEGINBlock.new(
|
||
lbrace: lbrace,
|
||
statements: statements,
|
||
location: keyword.location.to(rbrace.location)
|
||
)
|
||
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
|
||
# [String] the value of the character literal
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('CHAR')
|
||
q.breakable
|
||
q.pp(value)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :CHAR, value: value, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_CHAR: (String value) -> CHAR
|
||
def on_CHAR(value)
|
||
node =
|
||
CHAR.new(
|
||
value: value,
|
||
location: Location.token(line: lineno, char: char_pos, size: value.size)
|
||
)
|
||
|
||
tokens << node
|
||
node
|
||
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
|
||
# [LBrace] the left brace that is seen after the keyword
|
||
attr_reader :lbrace
|
||
|
||
# [Statements] the expressions to be executed
|
||
attr_reader :statements
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(lbrace:, statements:, location:)
|
||
@lbrace = lbrace
|
||
@statements = statements
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('END')
|
||
q.breakable
|
||
q.pp(statements)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :END, lbrace: lbrace, stmts: statements, loc: location }.to_json(
|
||
*opts
|
||
)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_END: (Statements statements) -> ENDBlock
|
||
def on_END(statements)
|
||
lbrace = find_token(LBrace)
|
||
rbrace = find_token(RBrace)
|
||
|
||
statements.bind(
|
||
find_next_statement_start(lbrace.location.end_char),
|
||
rbrace.location.start_char
|
||
)
|
||
|
||
keyword = find_token(Kw, 'END')
|
||
|
||
ENDBlock.new(
|
||
lbrace: lbrace,
|
||
statements: statements,
|
||
location: keyword.location.to(rbrace.location)
|
||
)
|
||
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
|
||
# [String] the content after the script
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('__end__')
|
||
q.breakable
|
||
q.pp(value)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :__end__, value: value, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on___end__: (String value) -> EndContent
|
||
def on___end__(value)
|
||
@__end__ =
|
||
EndContent.new(
|
||
value: lines[lineno..-1].join("\n"),
|
||
location: Location.token(line: lineno, char: char_pos, size: value.size)
|
||
)
|
||
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 Alias
|
||
# [DynaSymbol | SymbolLiteral] the new name of the method
|
||
attr_reader :left
|
||
|
||
# [DynaSymbol | SymbolLiteral] the old name of the method
|
||
attr_reader :right
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(left:, right:, location:)
|
||
@left = left
|
||
@right = right
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('alias')
|
||
q.breakable
|
||
q.pp(left)
|
||
q.breakable
|
||
q.pp(right)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :alias, left: left, right: right, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_alias: (
|
||
# (DynaSymbol | SymbolLiteral) left,
|
||
# (DynaSymbol | SymbolLiteral) right
|
||
# ) -> Alias
|
||
def on_alias(left, right)
|
||
keyword = find_token(Kw, 'alias')
|
||
|
||
Alias.new(
|
||
left: left,
|
||
right: right,
|
||
location: keyword.location.to(right.location)
|
||
)
|
||
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
|
||
# [untyped] the value being indexed
|
||
attr_reader :collection
|
||
|
||
# [nil | Args | ArgsAddBlock] the value being passed within the brackets
|
||
attr_reader :index
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(collection:, index:, location:)
|
||
@collection = collection
|
||
@index = index
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('aref')
|
||
q.breakable
|
||
q.pp(collection)
|
||
q.breakable
|
||
q.pp(index)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :aref,
|
||
collection: collection,
|
||
index: index,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_aref: (untyped collection, (nil | Args | ArgsAddBlock) index) -> ARef
|
||
def on_aref(collection, index)
|
||
find_token(LBracket)
|
||
rbracket = find_token(RBracket)
|
||
|
||
ARef.new(
|
||
collection: collection,
|
||
index: index,
|
||
location: collection.location.to(rbracket.location)
|
||
)
|
||
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
|
||
# [untyped] the value being indexed
|
||
attr_reader :collection
|
||
|
||
# [nil | ArgsAddBlock] the value being passed within the brackets
|
||
attr_reader :index
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(collection:, index:, location:)
|
||
@collection = collection
|
||
@index = index
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('aref_field')
|
||
q.breakable
|
||
q.pp(collection)
|
||
q.breakable
|
||
q.pp(index)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :aref_field,
|
||
collection: collection,
|
||
index: index,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_aref_field: (
|
||
# untyped collection,
|
||
# (nil | ArgsAddBlock) index
|
||
# ) -> ARefField
|
||
def on_aref_field(collection, index)
|
||
find_token(LBracket)
|
||
rbracket = find_token(RBracket)
|
||
|
||
ARefField.new(
|
||
collection: collection,
|
||
index: index,
|
||
location: collection.location.to(rbracket.location)
|
||
)
|
||
end
|
||
|
||
# def on_arg_ambiguous(value)
|
||
# value
|
||
# 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
|
||
# ArgsAddBlock 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
|
||
# [nil | Args | ArgsAddBlock | ArgsForward] the arguments inside the
|
||
# parentheses
|
||
attr_reader :arguments
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(arguments:, location:)
|
||
@arguments = arguments
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('arg_paren')
|
||
q.breakable
|
||
q.pp(arguments)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :arg_paren, args: arguments, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_arg_paren: (
|
||
# (nil | Args | ArgsAddBlock | ArgsForward) arguments
|
||
# ) -> ArgParen
|
||
def on_arg_paren(arguments)
|
||
lparen = find_token(LParen)
|
||
rparen = find_token(RParen)
|
||
|
||
# If the arguments exceed the ending of the parentheses, then we know we
|
||
# have a heredoc in the arguments, and we need to use the bounds of the
|
||
# arguments to determine how large the arg_paren is.
|
||
ending =
|
||
if arguments && arguments.location.end_line > rparen.location.end_line
|
||
arguments
|
||
else
|
||
rparen
|
||
end
|
||
|
||
ArgParen.new(
|
||
arguments: arguments,
|
||
location: lparen.location.to(ending.location)
|
||
)
|
||
end
|
||
|
||
# Args represents a list of arguments being passed to a method call or array
|
||
# literal.
|
||
#
|
||
# method(first, second, third)
|
||
#
|
||
class Args
|
||
# [Array[ untyped ]] the arguments that this node wraps
|
||
attr_reader :parts
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(parts:, location:)
|
||
@parts = parts
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('args')
|
||
q.breakable
|
||
q.group(2, '(', ')') { q.seplist(parts) { |part| q.pp(part) } }
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :args, parts: parts, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_args_add: (Args arguments, untyped argument) -> Args
|
||
def on_args_add(arguments, argument)
|
||
if arguments.parts.empty?
|
||
# If this is the first argument being passed into the list of arguments,
|
||
# then we're going to use the bounds of the argument to override the
|
||
# parent node's location since this will be more accurate.
|
||
Args.new(parts: [argument], location: argument.location)
|
||
else
|
||
# Otherwise we're going to update the existing list with the argument
|
||
# being added as well as the new end bounds.
|
||
Args.new(
|
||
parts: arguments.parts << argument,
|
||
location: arguments.location.to(argument.location)
|
||
)
|
||
end
|
||
end
|
||
|
||
# ArgsAddBlock represents a list of arguments and potentially a block
|
||
# argument. ArgsAddBlock is commonly seen being passed to any method where you
|
||
# use parentheses (wrapped in an ArgParen node). It’s also used to pass
|
||
# arguments to the various control-flow keywords like +return+.
|
||
#
|
||
# method(argument, &block)
|
||
#
|
||
class ArgsAddBlock
|
||
# [Args] the arguments before the optional block
|
||
attr_reader :arguments
|
||
|
||
# [nil | untyped] the optional block argument
|
||
attr_reader :block
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(arguments:, block:, location:)
|
||
@arguments = arguments
|
||
@block = block
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('args_add_block')
|
||
q.breakable
|
||
q.pp(arguments)
|
||
q.breakable
|
||
q.pp(block)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :args_add_block,
|
||
args: arguments,
|
||
block: block,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_args_add_block: (
|
||
# Args arguments,
|
||
# (false | untyped) block
|
||
# ) -> ArgsAddBlock
|
||
def on_args_add_block(arguments, block)
|
||
ending = block || arguments
|
||
|
||
ArgsAddBlock.new(
|
||
arguments: arguments,
|
||
block: block || nil,
|
||
location: arguments.location.to(ending.location)
|
||
)
|
||
end
|
||
|
||
# Star represents using a splat operator on an expression.
|
||
#
|
||
# method(*arguments)
|
||
#
|
||
class ArgStar
|
||
# [untyped] the expression being splatted
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('arg_star')
|
||
|
||
q.breakable
|
||
q.pp(value)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :arg_star, value: value, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_args_add_star: (Args arguments, untyped star) -> Args
|
||
def on_args_add_star(arguments, argument)
|
||
beginning = find_token(Op, '*')
|
||
ending = argument || beginning
|
||
|
||
location =
|
||
if arguments.parts.empty?
|
||
ending.location
|
||
else
|
||
arguments.location.to(ending.location)
|
||
end
|
||
|
||
arg_star =
|
||
ArgStar.new(
|
||
value: argument,
|
||
location: beginning.location.to(ending.location)
|
||
)
|
||
|
||
Args.new(parts: arguments.parts << arg_star, location: location)
|
||
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
|
||
# [String] the value of the operator
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('args_forward')
|
||
q.breakable
|
||
q.pp(value)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :args_forward, value: value, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_args_forward: () -> ArgsForward
|
||
def on_args_forward
|
||
op = find_token(Op, '...')
|
||
|
||
ArgsForward.new(value: op.value, location: op.location)
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_args_new: () -> Args
|
||
def on_args_new
|
||
Args.new(parts: [], location: Location.fixed(line: lineno, char: char_pos))
|
||
end
|
||
|
||
# ArrayLiteral represents any form of an array literal, and contains myriad
|
||
# child nodes because of the special array literal syntax like %w and %i.
|
||
#
|
||
# []
|
||
# [one, two, three]
|
||
# [*one_two_three]
|
||
# %i[one two three]
|
||
# %w[one two three]
|
||
# %I[one two three]
|
||
# %W[one two three]
|
||
#
|
||
# Every line in the example above produces an ArrayLiteral node. In order, the
|
||
# child contents node of this ArrayLiteral node would be nil, Args, QSymbols,
|
||
# QWords, Symbols, and Words.
|
||
class ArrayLiteral
|
||
# [nil | Args | QSymbols | QWords | Symbols | Words] the
|
||
# contents of the array
|
||
attr_reader :contents
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(contents:, location:)
|
||
@contents = contents
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('array')
|
||
q.breakable
|
||
q.pp(contents)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :array, cnts: contents, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_array: (
|
||
# (nil | Args | QSymbols | QWords | Symbols | Words) contents
|
||
# ) -> ArrayLiteral
|
||
def on_array(contents)
|
||
if !contents || contents.is_a?(Args)
|
||
lbracket = find_token(LBracket)
|
||
rbracket = find_token(RBracket)
|
||
|
||
ArrayLiteral.new(
|
||
contents: contents,
|
||
location: lbracket.location.to(rbracket.location)
|
||
)
|
||
else
|
||
tstring_end = find_token(TStringEnd)
|
||
contents =
|
||
contents.class.new(
|
||
elements: contents.elements,
|
||
location: contents.location.to(tstring_end.location)
|
||
)
|
||
|
||
ArrayLiteral.new(contents: contents, location: contents.location)
|
||
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
|
||
# [nil | VarRef] the optional constant wrapper
|
||
attr_reader :constant
|
||
|
||
# [Array[ untyped ]] 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[ untyped ]] the list of positional arguments occurring after the
|
||
# optional star if there is one
|
||
attr_reader :posts
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(constant:, requireds:, rest:, posts:, location:)
|
||
@constant = constant
|
||
@requireds = requireds
|
||
@rest = rest
|
||
@posts = posts
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('aryptn')
|
||
|
||
if constant
|
||
q.breakable
|
||
q.pp(constant)
|
||
end
|
||
|
||
if requireds.any?
|
||
q.breakable
|
||
q.group(2, '(', ')') do
|
||
q.seplist(requireds) { |required| q.pp(required) }
|
||
end
|
||
end
|
||
|
||
if rest
|
||
q.breakable
|
||
q.pp(rest)
|
||
end
|
||
|
||
if posts.any?
|
||
q.breakable
|
||
q.group(2, '(', ')') { q.seplist(posts) { |post| q.pp(post) } }
|
||
end
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :aryptn,
|
||
constant: constant,
|
||
reqs: requireds,
|
||
rest: rest,
|
||
posts: posts,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_aryptn: (
|
||
# (nil | VarRef) constant,
|
||
# (nil | Array[untyped]) requireds,
|
||
# (nil | VarField) rest,
|
||
# (nil | Array[untyped]) posts
|
||
# ) -> AryPtn
|
||
def on_aryptn(constant, requireds, rest, posts)
|
||
parts = [constant, *requireds, rest, *posts].compact
|
||
|
||
AryPtn.new(
|
||
constant: constant,
|
||
requireds: requireds || [],
|
||
rest: rest,
|
||
posts: posts || [],
|
||
location: parts[0].location.to(parts[-1].location)
|
||
)
|
||
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
|
||
# [ARefField | ConstPathField | Field | TopConstField | VarField] the target
|
||
# to assign the result of the expression to
|
||
attr_reader :target
|
||
|
||
# [untyped] the expression to be assigned
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(target:, value:, location:)
|
||
@target = target
|
||
@value = value
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('assign')
|
||
q.breakable
|
||
q.pp(target)
|
||
q.breakable
|
||
q.pp(value)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :assign, target: target, value: value, loc: location }.to_json(
|
||
*opts
|
||
)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_assign: (
|
||
# (ARefField | ConstPathField | Field | TopConstField | VarField) target,
|
||
# untyped value
|
||
# ) -> Assign
|
||
def on_assign(target, value)
|
||
Assign.new(
|
||
target: target,
|
||
value: value,
|
||
location: target.location.to(value.location)
|
||
)
|
||
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 AssocNew nodes.
|
||
class Assoc
|
||
# [untyped] the key of this pair
|
||
attr_reader :key
|
||
|
||
# [untyped] the value of this pair
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(key:, value:, location:)
|
||
@key = key
|
||
@value = value
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('assoc')
|
||
q.breakable
|
||
q.pp(key)
|
||
q.breakable
|
||
q.pp(value)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :assoc, key: key, value: value, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_assoc_new: (untyped key, untyped value) -> Assoc
|
||
def on_assoc_new(key, value)
|
||
Assoc.new(
|
||
key: key,
|
||
value: value,
|
||
location: key.location.to(value.location)
|
||
)
|
||
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
|
||
# [untyped] the expression that is being splatted
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('assoc_splat')
|
||
q.breakable
|
||
q.pp(value)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :assoc_splat, value: value, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_assoc_splat: (untyped value) -> AssocSplat
|
||
def on_assoc_splat(value)
|
||
operator = find_token(Op, '**')
|
||
|
||
AssocSplat.new(value: value, location: operator.location.to(value.location))
|
||
end
|
||
|
||
# AssocListFromArgs represents the key-value pairs of a hash literal. Its
|
||
# parent node is always a hash.
|
||
#
|
||
# { key1: value1, key2: value2 }
|
||
#
|
||
class AssocListFromArgs
|
||
# [Array[ AssocNew | AssocSplat ]]
|
||
attr_reader :assocs
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(assocs:, location:)
|
||
@assocs = assocs
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('assoclist_from_args')
|
||
q.breakable
|
||
q.group(2, '(', ')') { q.seplist(assocs) { |assoc| q.pp(assoc) } }
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :assoclist_from_args, assocs: assocs, loc: location }.to_json(
|
||
*opts
|
||
)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_assoclist_from_args: (
|
||
# Array[AssocNew | AssocSplat] assocs
|
||
# ) -> AssocListFromArgs
|
||
def on_assoclist_from_args(assocs)
|
||
AssocListFromArgs.new(
|
||
assocs: assocs,
|
||
location: assocs[0].location.to(assocs[-1].location)
|
||
)
|
||
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
|
||
# [String] the name of the global backreference variable
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('backref')
|
||
q.breakable
|
||
q.pp(value)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :backref, value: value, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_backref: (String value) -> Backref
|
||
def on_backref(value)
|
||
node =
|
||
Backref.new(
|
||
value: value,
|
||
location: Location.token(line: lineno, char: char_pos, size: value.size)
|
||
)
|
||
|
||
tokens << node
|
||
node
|
||
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
|
||
# [String] the backtick in the string
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('backtick')
|
||
q.breakable
|
||
q.pp(value)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :backtick, value: value, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_backtick: (String value) -> Backtick
|
||
def on_backtick(value)
|
||
node =
|
||
Backtick.new(
|
||
value: value,
|
||
location: Location.token(line: lineno, char: char_pos, size: value.size)
|
||
)
|
||
|
||
tokens << node
|
||
node
|
||
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
|
||
# [Array[ AssocNew | AssocSplat ]]
|
||
attr_reader :assocs
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(assocs:, location:)
|
||
@assocs = assocs
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('bare_assoc_hash')
|
||
q.breakable
|
||
q.group(2, '(', ')') { q.seplist(assocs) { |assoc| q.pp(assoc) } }
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :bare_assoc_hash, assocs: assocs, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_bare_assoc_hash: (Array[AssocNew | AssocSplat] assocs) -> BareAssocHash
|
||
def on_bare_assoc_hash(assocs)
|
||
BareAssocHash.new(
|
||
assocs: assocs,
|
||
location: assocs[0].location.to(assocs[-1].location)
|
||
)
|
||
end
|
||
|
||
# Begin represents a begin..end chain.
|
||
#
|
||
# begin
|
||
# value
|
||
# end
|
||
#
|
||
class Begin
|
||
# [BodyStmt] the bodystmt that contains the contents of this begin block
|
||
attr_reader :bodystmt
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(bodystmt:, location:)
|
||
@bodystmt = bodystmt
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('begin')
|
||
q.breakable
|
||
q.pp(bodystmt)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :begin, bodystmt: bodystmt, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_begin: (BodyStmt bodystmt) -> Begin
|
||
def on_begin(bodystmt)
|
||
keyword = find_token(Kw, 'begin')
|
||
end_char =
|
||
if bodystmt.rescue_clause || bodystmt.ensure_clause ||
|
||
bodystmt.else_clause
|
||
bodystmt.location.end_char
|
||
else
|
||
find_token(Kw, 'end').location.end_char
|
||
end
|
||
|
||
bodystmt.bind(keyword.location.end_char, end_char)
|
||
|
||
Begin.new(
|
||
bodystmt: bodystmt,
|
||
location: keyword.location.to(bodystmt.location)
|
||
)
|
||
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
|
||
# [untyped] the left-hand side of the expression
|
||
attr_reader :left
|
||
|
||
# [String] the operator used between the two expressions
|
||
attr_reader :operator
|
||
|
||
# [untyped] the right-hand side of the expression
|
||
attr_reader :right
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(left:, operator:, right:, location:)
|
||
@left = left
|
||
@operator = operator
|
||
@right = right
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('binary')
|
||
q.breakable
|
||
q.pp(left)
|
||
q.breakable
|
||
q.text(operator)
|
||
q.breakable
|
||
q.pp(right)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :binary,
|
||
left: left,
|
||
op: operator,
|
||
right: right,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_binary: (untyped left, (Op | Symbol) operator, untyped right) -> Binary
|
||
def on_binary(left, operator, right)
|
||
# On most Ruby implementations, operator is a Symbol that represents that
|
||
# operation being performed. For instance in the example `1 < 2`, the
|
||
# `operator` object would be `:<`. However, on JRuby, it's an `@op` node,
|
||
# so here we're going to explicitly convert it into the same normalized
|
||
# form.
|
||
operator = tokens.delete(operator).value unless operator.is_a?(Symbol)
|
||
|
||
Binary.new(
|
||
left: left,
|
||
operator: operator,
|
||
right: right,
|
||
location: left.location.to(right.location)
|
||
)
|
||
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
|
||
# [Params] the parameters being declared with the block
|
||
attr_reader :params
|
||
|
||
# [Array[ Ident ]] the list of block-local variable declarations
|
||
attr_reader :locals
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(params:, locals:, location:)
|
||
@params = params
|
||
@locals = locals
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('block_var')
|
||
q.breakable
|
||
q.pp(params)
|
||
|
||
if locals.any?
|
||
q.breakable
|
||
q.group(2, '(', ')') { q.seplist(locals) { |local| q.pp(local) } }
|
||
end
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :block_var,
|
||
params: params,
|
||
locals: locals,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_block_var: (Params params, (nil | Array[Ident]) locals) -> BlockVar
|
||
def on_block_var(params, locals)
|
||
index =
|
||
tokens.rindex do |node|
|
||
node.is_a?(Op) && %w[| ||].include?(node.value) &&
|
||
node.location.start_char < params.location.start_char
|
||
end
|
||
|
||
beginning = tokens[index]
|
||
ending = tokens[-1]
|
||
|
||
BlockVar.new(
|
||
params: params,
|
||
locals: locals || [],
|
||
location: beginning.location.to(ending.location)
|
||
)
|
||
end
|
||
|
||
# BlockArg represents declaring a block parameter on a method definition.
|
||
#
|
||
# def method(&block); end
|
||
#
|
||
class BlockArg
|
||
# [Ident] the name of the block argument
|
||
attr_reader :name
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(name:, location:)
|
||
@name = name
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('blockarg')
|
||
q.breakable
|
||
q.pp(name)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :blockarg, name: name, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_blockarg: (Ident name) -> BlockArg
|
||
def on_blockarg(name)
|
||
operator = find_token(Op, '&')
|
||
|
||
BlockArg.new(name: name, location: operator.location.to(name.location))
|
||
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
|
||
# [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 | Statements] the optional set of statements inside the else clause
|
||
attr_reader :else_clause
|
||
|
||
# [nil | Ensure] the optional ensure clause
|
||
attr_reader :ensure_clause
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(
|
||
statements:,
|
||
rescue_clause:,
|
||
else_clause:,
|
||
ensure_clause:,
|
||
location:
|
||
)
|
||
@statements = statements
|
||
@rescue_clause = rescue_clause
|
||
@else_clause = else_clause
|
||
@ensure_clause = ensure_clause
|
||
@location = location
|
||
end
|
||
|
||
def bind(start_char, end_char)
|
||
@location =
|
||
Location.new(
|
||
start_line: location.start_line,
|
||
start_char: start_char,
|
||
end_line: location.end_line,
|
||
end_char: end_char
|
||
)
|
||
|
||
parts = [rescue_clause, else_clause, ensure_clause]
|
||
|
||
# Here we're going to determine the bounds for the statements
|
||
consequent = parts.compact.first
|
||
statements.bind(
|
||
start_char,
|
||
consequent ? consequent.location.start_char : end_char
|
||
)
|
||
|
||
# Next we're going to determine the rescue clause if there is one
|
||
if rescue_clause
|
||
consequent = parts.drop(1).compact.first
|
||
rescue_clause.bind_end(
|
||
consequent ? consequent.location.start_char : end_char
|
||
)
|
||
end
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('bodystmt')
|
||
q.breakable
|
||
q.pp(statements)
|
||
|
||
if rescue_clause
|
||
q.breakable
|
||
q.pp(rescue_clause)
|
||
end
|
||
|
||
if else_clause
|
||
q.breakable
|
||
q.pp(else_clause)
|
||
end
|
||
|
||
if ensure_clause
|
||
q.breakable
|
||
q.pp(ensure_clause)
|
||
end
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :bodystmt,
|
||
stmts: statements,
|
||
rsc: rescue_clause,
|
||
els: else_clause,
|
||
ens: ensure_clause,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_bodystmt: (
|
||
# Statements statements,
|
||
# (nil | Rescue) rescue_clause,
|
||
# (nil | Statements) else_clause,
|
||
# (nil | Ensure) ensure_clause
|
||
# ) -> BodyStmt
|
||
def on_bodystmt(statements, rescue_clause, else_clause, ensure_clause)
|
||
BodyStmt.new(
|
||
statements: statements,
|
||
rescue_clause: rescue_clause,
|
||
else_clause: else_clause,
|
||
ensure_clause: ensure_clause,
|
||
location: Location.fixed(line: lineno, char: char_pos)
|
||
)
|
||
end
|
||
|
||
# BraceBlock represents passing a block to a method call using the { }
|
||
# operators.
|
||
#
|
||
# method { |variable| variable + 1 }
|
||
#
|
||
class BraceBlock
|
||
# [LBrace] the left brace that opens this block
|
||
attr_reader :lbrace
|
||
|
||
# [nil | BlockVar] the optional set of parameters to the block
|
||
attr_reader :block_var
|
||
|
||
# [Statements] the list of expressions to evaluate within the block
|
||
attr_reader :statements
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(lbrace:, block_var:, statements:, location:)
|
||
@lbrace = lbrace
|
||
@block_var = block_var
|
||
@statements = statements
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('brace_block')
|
||
|
||
if block_var
|
||
q.breakable
|
||
q.pp(block_var)
|
||
end
|
||
|
||
q.breakable
|
||
q.pp(statements)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :brace_block,
|
||
lbrace: lbrace,
|
||
block_var: block_var,
|
||
stmts: statements,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_brace_block: (
|
||
# (nil | BlockVar) block_var,
|
||
# Statements statements
|
||
# ) -> BraceBlock
|
||
def on_brace_block(block_var, statements)
|
||
lbrace = find_token(LBrace)
|
||
rbrace = find_token(RBrace)
|
||
|
||
statements.bind(
|
||
find_next_statement_start((block_var || lbrace).location.end_char),
|
||
rbrace.location.start_char
|
||
)
|
||
|
||
location =
|
||
Location.new(
|
||
start_line: lbrace.location.start_line,
|
||
start_char: lbrace.location.start_char,
|
||
end_line: [rbrace.location.end_line, statements.location.end_line].max,
|
||
end_char: rbrace.location.end_char
|
||
)
|
||
|
||
BraceBlock.new(
|
||
lbrace: lbrace,
|
||
block_var: block_var,
|
||
statements: statements,
|
||
location: location
|
||
)
|
||
end
|
||
|
||
# Break represents using the +break+ keyword.
|
||
#
|
||
# break
|
||
#
|
||
# It can also optionally accept arguments, as in:
|
||
#
|
||
# break 1
|
||
#
|
||
class Break
|
||
# [Args | ArgsAddBlock] the arguments being sent to the keyword
|
||
attr_reader :arguments
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(arguments:, location:)
|
||
@arguments = arguments
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('break')
|
||
q.breakable
|
||
q.pp(arguments)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :break, args: arguments, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_break: ((Args | ArgsAddBlock) arguments) -> Break
|
||
def on_break(arguments)
|
||
keyword = find_token(Kw, 'break')
|
||
|
||
location = keyword.location
|
||
location = location.to(arguments.location) unless arguments.is_a?(Args)
|
||
|
||
Break.new(arguments: arguments, location: location)
|
||
end
|
||
|
||
# Call represents a method call. This node doesn't contain the arguments being
|
||
# passed (if arguments are passed, this node will get nested under a
|
||
# MethodAddArg node).
|
||
#
|
||
# receiver.message
|
||
#
|
||
class Call
|
||
# [untyped] the receiver of the method call
|
||
attr_reader :receiver
|
||
|
||
# [:"::" | 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
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(receiver:, operator:, message:, location:)
|
||
@receiver = receiver
|
||
@operator = operator
|
||
@message = message
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('call')
|
||
q.breakable
|
||
q.pp(receiver)
|
||
q.breakable
|
||
q.pp(operator)
|
||
q.breakable
|
||
q.pp(message)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :call,
|
||
receiver: receiver,
|
||
op: operator,
|
||
message: message,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_call: (
|
||
# untyped receiver,
|
||
# (:"::" | Op | Period) operator,
|
||
# (:call | Backtick | Const | Ident | Op) message
|
||
# ) -> Call
|
||
def on_call(receiver, operator, message)
|
||
ending = message
|
||
ending = operator if message == :call
|
||
|
||
Call.new(
|
||
receiver: receiver,
|
||
operator: operator,
|
||
message: message,
|
||
location:
|
||
Location.new(
|
||
start_line: receiver.location.start_line,
|
||
start_char: receiver.location.start_char,
|
||
end_line: [ending.location.end_line, receiver.location.end_line].max,
|
||
end_char: ending.location.end_char
|
||
)
|
||
)
|
||
end
|
||
|
||
# Case represents the beginning of a case chain.
|
||
#
|
||
# case value
|
||
# when 1
|
||
# "one"
|
||
# when 2
|
||
# "two"
|
||
# else
|
||
# "number"
|
||
# end
|
||
#
|
||
class Case
|
||
# [nil | untyped] optional value being switched on
|
||
attr_reader :value
|
||
|
||
# [In | When] the next clause in the chain
|
||
attr_reader :consequent
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, consequent:, location:)
|
||
@value = value
|
||
@consequent = consequent
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('case')
|
||
|
||
if value
|
||
q.breakable
|
||
q.pp(value)
|
||
end
|
||
|
||
q.breakable
|
||
q.pp(consequent)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :case, value: value, cons: consequent, loc: location }.to_json(
|
||
*opts
|
||
)
|
||
end
|
||
end
|
||
|
||
# RAssign represents a single-line pattern match.
|
||
#
|
||
# value in pattern
|
||
# value => pattern
|
||
#
|
||
class RAssign
|
||
# [untyped] 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
|
||
|
||
# [untyped] the pattern on the right-hand side of the expression
|
||
attr_reader :pattern
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, operator:, pattern:, location:)
|
||
@value = value
|
||
@operator = operator
|
||
@pattern = pattern
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('rassign')
|
||
|
||
q.breakable
|
||
q.pp(value)
|
||
|
||
q.breakable
|
||
q.pp(operator)
|
||
|
||
q.breakable
|
||
q.pp(pattern)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :rassign,
|
||
value: value,
|
||
op: operator,
|
||
pattern: pattern,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_case: (untyped value, untyped consequent) -> Case | RAssign
|
||
def on_case(value, consequent)
|
||
if keyword = find_token(Kw, 'case', consume: false)
|
||
tokens.delete(keyword)
|
||
|
||
Case.new(
|
||
value: value,
|
||
consequent: consequent,
|
||
location: keyword.location.to(consequent.location)
|
||
)
|
||
else
|
||
operator = find_token(Kw, 'in', consume: false) || find_token(Op, '=>')
|
||
|
||
RAssign.new(
|
||
value: value,
|
||
operator: operator,
|
||
pattern: consequent,
|
||
location: value.location.to(consequent.location)
|
||
)
|
||
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
|
||
# [ConstPathRef | ConstRef | TopConstRef] the name of the class being
|
||
# defined
|
||
attr_reader :constant
|
||
|
||
# [nil | untyped] the optional superclass declaration
|
||
attr_reader :superclass
|
||
|
||
# [BodyStmt] the expressions to execute within the context of the class
|
||
attr_reader :bodystmt
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(constant:, superclass:, bodystmt:, location:)
|
||
@constant = constant
|
||
@superclass = superclass
|
||
@bodystmt = bodystmt
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('class')
|
||
|
||
q.breakable
|
||
q.pp(constant)
|
||
|
||
if superclass
|
||
q.breakable
|
||
q.pp(superclass)
|
||
end
|
||
|
||
q.breakable
|
||
q.pp(bodystmt)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :class,
|
||
constant: constant,
|
||
superclass: superclass,
|
||
bodystmt: bodystmt,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_class: (
|
||
# (ConstPathRef | ConstRef | TopConstRef) constant,
|
||
# untyped superclass,
|
||
# BodyStmt bodystmt
|
||
# ) -> ClassDeclaration
|
||
def on_class(constant, superclass, bodystmt)
|
||
beginning = find_token(Kw, 'class')
|
||
ending = find_token(Kw, 'end')
|
||
|
||
bodystmt.bind(
|
||
find_next_statement_start((superclass || constant).location.end_char),
|
||
ending.location.start_char
|
||
)
|
||
|
||
ClassDeclaration.new(
|
||
constant: constant,
|
||
superclass: superclass,
|
||
bodystmt: bodystmt,
|
||
location: beginning.location.to(ending.location)
|
||
)
|
||
end
|
||
|
||
# Comma represents the use of the , operator.
|
||
class Comma
|
||
# [String] the comma in the string
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_comma: (String value) -> Comma
|
||
def on_comma(value)
|
||
node =
|
||
Comma.new(
|
||
value: value,
|
||
location: Location.token(line: lineno, char: char_pos, size: value.size)
|
||
)
|
||
|
||
tokens << node
|
||
node
|
||
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
|
||
# [Const | Ident] the message being sent to the implicit receiver
|
||
attr_reader :message
|
||
|
||
# [Args | ArgsAddBlock] the arguments being sent with the message
|
||
attr_reader :arguments
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(message:, arguments:, location:)
|
||
@message = message
|
||
@arguments = arguments
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('command')
|
||
|
||
q.breakable
|
||
q.pp(message)
|
||
|
||
q.breakable
|
||
q.pp(arguments)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :command,
|
||
message: message,
|
||
args: arguments,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_command: (
|
||
# (Const | Ident) message,
|
||
# (Args | ArgsAddBlock) arguments
|
||
# ) -> Command
|
||
def on_command(message, arguments)
|
||
Command.new(
|
||
message: message,
|
||
arguments: arguments,
|
||
location: message.location.to(arguments.location)
|
||
)
|
||
end
|
||
|
||
# CommandCall represents a method call on an object with arguments and no
|
||
# parentheses.
|
||
#
|
||
# object.method argument
|
||
#
|
||
class CommandCall
|
||
# [untyped] the receiver of the message
|
||
attr_reader :receiver
|
||
|
||
# [:"::" | Op | Period] the operator used to send the message
|
||
attr_reader :operator
|
||
|
||
# [Const | Ident | Op] the message being send
|
||
attr_reader :message
|
||
|
||
# [Args | ArgsAddBlock] the arguments going along with the message
|
||
attr_reader :arguments
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(receiver:, operator:, message:, arguments:, location:)
|
||
@receiver = receiver
|
||
@operator = operator
|
||
@message = message
|
||
@arguments = arguments
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('command_call')
|
||
|
||
q.breakable
|
||
q.pp(receiver)
|
||
|
||
q.breakable
|
||
q.pp(operator)
|
||
|
||
q.breakable
|
||
q.pp(message)
|
||
|
||
q.breakable
|
||
q.pp(arguments)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :command_call,
|
||
receiver: receiver,
|
||
op: operator,
|
||
message: message,
|
||
args: arguments,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_command_call: (
|
||
# untyped receiver,
|
||
# (:"::" | Op | Period) operator,
|
||
# (Const | Ident | Op) message,
|
||
# (Args | ArgsAddBlock) arguments
|
||
# ) -> CommandCall
|
||
def on_command_call(receiver, operator, message, arguments)
|
||
ending = arguments || message
|
||
|
||
CommandCall.new(
|
||
receiver: receiver,
|
||
operator: operator,
|
||
message: message,
|
||
arguments: arguments,
|
||
location: receiver.location.to(ending.location)
|
||
)
|
||
end
|
||
|
||
# Comment represents a comment in the source.
|
||
#
|
||
# # comment
|
||
#
|
||
class Comment
|
||
# [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
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, inline:, location:)
|
||
@value = value
|
||
@inline = inline
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('comment')
|
||
q.breakable
|
||
q.pp(value)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :comment,
|
||
value: value.force_encoding('UTF-8'),
|
||
inline: inline,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_comment: (String value) -> Comment
|
||
def on_comment(value)
|
||
line = lineno
|
||
comment =
|
||
Comment.new(
|
||
value: value[1..-1].chomp,
|
||
inline: value.strip != lines[line - 1],
|
||
location:
|
||
Location.token(line: line, char: char_pos, size: value.size - 1)
|
||
)
|
||
|
||
@comments << comment
|
||
comment
|
||
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
|
||
# [String] the name of the constant
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('const')
|
||
q.breakable
|
||
q.pp(value)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :const, value: value, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_const: (String value) -> Const
|
||
def on_const(value)
|
||
node =
|
||
Const.new(
|
||
value: value,
|
||
location: Location.token(line: lineno, char: char_pos, size: value.size)
|
||
)
|
||
|
||
tokens << node
|
||
node
|
||
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
|
||
# [untyped] the source of the constant
|
||
attr_reader :parent
|
||
|
||
# [Const] the constant itself
|
||
attr_reader :constant
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(parent:, constant:, location:)
|
||
@parent = parent
|
||
@constant = constant
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('const_path_field')
|
||
|
||
q.breakable
|
||
q.pp(parent)
|
||
|
||
q.breakable
|
||
q.pp(constant)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :const_path_field,
|
||
parent: parent,
|
||
constant: constant,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_const_path_field: (untyped parent, Const constant) -> ConstPathField
|
||
def on_const_path_field(parent, constant)
|
||
ConstPathField.new(
|
||
parent: parent,
|
||
constant: constant,
|
||
location: parent.location.to(constant.location)
|
||
)
|
||
end
|
||
|
||
# ConstPathRef represents referencing a constant by a path.
|
||
#
|
||
# object::Const
|
||
#
|
||
class ConstPathRef
|
||
# [untyped] the source of the constant
|
||
attr_reader :parent
|
||
|
||
# [Const] the constant itself
|
||
attr_reader :constant
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(parent:, constant:, location:)
|
||
@parent = parent
|
||
@constant = constant
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('const_path_ref')
|
||
|
||
q.breakable
|
||
q.pp(parent)
|
||
|
||
q.breakable
|
||
q.pp(constant)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :const_path_ref,
|
||
parent: parent,
|
||
constant: constant,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_const_path_ref: (untyped parent, Const constant) -> ConstPathRef
|
||
def on_const_path_ref(parent, constant)
|
||
ConstPathRef.new(
|
||
parent: parent,
|
||
constant: constant,
|
||
location: parent.location.to(constant.location)
|
||
)
|
||
end
|
||
|
||
# ConstRef represents the name of the constant being used in a class or module
|
||
# declaration.
|
||
#
|
||
# class Container
|
||
# end
|
||
#
|
||
class ConstRef
|
||
# [Const] the constant itself
|
||
attr_reader :constant
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(constant:, location:)
|
||
@constant = constant
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('const_ref')
|
||
|
||
q.breakable
|
||
q.pp(constant)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :const_ref, constant: constant, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_const_ref: (Const constant) -> ConstRef
|
||
def on_const_ref(constant)
|
||
ConstRef.new(constant: constant, location: constant.location)
|
||
end
|
||
|
||
# CVar represents the use of a class variable.
|
||
#
|
||
# @@variable
|
||
#
|
||
class CVar
|
||
# [String] the name of the class variable
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('cvar')
|
||
|
||
q.breakable
|
||
q.pp(value)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :cvar, value: value, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_cvar: (String value) -> CVar
|
||
def on_cvar(value)
|
||
node =
|
||
CVar.new(
|
||
value: value,
|
||
location: Location.token(line: lineno, char: char_pos, size: value.size)
|
||
)
|
||
|
||
tokens << node
|
||
node
|
||
end
|
||
|
||
# Def represents defining a regular method on the current self object.
|
||
#
|
||
# def method(param) result end
|
||
#
|
||
class Def
|
||
# [Backtick | Const | Ident | Kw | Op] the name of the method
|
||
attr_reader :name
|
||
|
||
# [Params | Paren] the parameter declaration for the method
|
||
attr_reader :params
|
||
|
||
# [BodyStmt] the expressions to be executed by the method
|
||
attr_reader :bodystmt
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(name:, params:, bodystmt:, location:)
|
||
@name = name
|
||
@params = params
|
||
@bodystmt = bodystmt
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('def')
|
||
|
||
q.breakable
|
||
q.pp(name)
|
||
|
||
q.breakable
|
||
q.pp(params)
|
||
|
||
q.breakable
|
||
q.pp(bodystmt)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :def,
|
||
name: name,
|
||
params: params,
|
||
bodystmt: bodystmt,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# DefEndless represents defining a single-line method since Ruby 3.0+.
|
||
#
|
||
# def method = result
|
||
#
|
||
class DefEndless
|
||
# [Backtick | Const | Ident | Kw | Op] the name of the method
|
||
attr_reader :name
|
||
|
||
# [Paren] the parameter declaration for the method
|
||
attr_reader :paren
|
||
|
||
# [untyped] the expression to be executed by the method
|
||
attr_reader :statement
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(name:, paren:, statement:, location:)
|
||
@name = name
|
||
@paren = paren
|
||
@statement = statement
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('def_endless')
|
||
|
||
q.breakable
|
||
q.pp(name)
|
||
|
||
q.breakable
|
||
q.pp(paren)
|
||
|
||
q.breakable
|
||
q.pp(statement)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :def_endless,
|
||
name: name,
|
||
paren: paren,
|
||
stmt: statement,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_def: (
|
||
# (Backtick | Const | Ident | Kw | Op) name,
|
||
# (Params | Paren) params,
|
||
# untyped bodystmt
|
||
# ) -> Def | DefEndless
|
||
def on_def(name, params, bodystmt)
|
||
# Make sure to delete this token in case you're defining something like def
|
||
# class which would lead to this being a kw and causing all kinds of trouble
|
||
tokens.delete(name)
|
||
|
||
# Find the beginning of the method definition, which works for single-line
|
||
# and normal method definitions.
|
||
beginning = find_token(Kw, 'def')
|
||
|
||
# If we don't have a bodystmt node, then we have a single-line method
|
||
unless bodystmt.is_a?(BodyStmt)
|
||
node =
|
||
DefEndless.new(
|
||
name: name,
|
||
paren: params,
|
||
statement: bodystmt,
|
||
location: beginning.location.to(bodystmt.location)
|
||
)
|
||
|
||
return node
|
||
end
|
||
|
||
# If there aren't any params then we need to correct the params node
|
||
# location information
|
||
if params.is_a?(Params) && params.empty?
|
||
end_char = name.location.end_char
|
||
location =
|
||
Location.new(
|
||
start_line: params.location.start_line,
|
||
start_char: end_char,
|
||
end_line: params.location.end_line,
|
||
end_char: end_char
|
||
)
|
||
|
||
params = Params.new(location: location)
|
||
end
|
||
|
||
ending = find_token(Kw, 'end')
|
||
bodystmt.bind(
|
||
find_next_statement_start(params.location.end_char),
|
||
ending.location.start_char
|
||
)
|
||
|
||
Def.new(
|
||
name: name,
|
||
params: params,
|
||
bodystmt: bodystmt,
|
||
location: beginning.location.to(ending.location)
|
||
)
|
||
end
|
||
|
||
# Defined represents the use of the +defined?+ operator. It can be used with
|
||
# and without parentheses.
|
||
#
|
||
# defined?(variable)
|
||
#
|
||
class Defined
|
||
# [untyped] the value being sent to the keyword
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('defined')
|
||
|
||
q.breakable
|
||
q.pp(value)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :defined, value: value, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_defined: (untyped value) -> Defined
|
||
def on_defined(value)
|
||
beginning = find_token(Kw, 'defined?')
|
||
ending = value
|
||
|
||
range = beginning.location.end_char...value.location.start_char
|
||
if source[range].include?('(')
|
||
find_token(LParen)
|
||
ending = find_token(RParen)
|
||
end
|
||
|
||
Defined.new(value: value, location: beginning.location.to(ending.location))
|
||
end
|
||
|
||
# Defs represents defining a singleton method on an object.
|
||
#
|
||
# def object.method(param) result end
|
||
#
|
||
class Defs
|
||
# [untyped] the target where the method is being defined
|
||
attr_reader :target
|
||
|
||
# [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
|
||
|
||
# [Params | Paren] the parameter declaration for the method
|
||
attr_reader :params
|
||
|
||
# [BodyStmt] the expressions to be executed by the method
|
||
attr_reader :bodystmt
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(target:, operator:, name:, params:, bodystmt:, location:)
|
||
@target = target
|
||
@operator = operator
|
||
@name = name
|
||
@params = params
|
||
@bodystmt = bodystmt
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('defs')
|
||
|
||
q.breakable
|
||
q.pp(target)
|
||
|
||
q.breakable
|
||
q.pp(operator)
|
||
|
||
q.breakable
|
||
q.pp(name)
|
||
|
||
q.breakable
|
||
q.pp(params)
|
||
|
||
q.breakable
|
||
q.pp(bodystmt)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :defs,
|
||
target: target,
|
||
op: operator,
|
||
name: name,
|
||
params: params,
|
||
bodystmt: bodystmt,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_defs: (
|
||
# untyped target,
|
||
# (Op | Period) operator,
|
||
# (Backtick | Const | Ident | Kw | Op) name,
|
||
# (Params | Paren) params,
|
||
# BodyStmt bodystmt
|
||
# ) -> Defs
|
||
def on_defs(target, operator, name, params, bodystmt)
|
||
# Make sure to delete this token in case you're defining something
|
||
# like def class which would lead to this being a kw and causing all kinds
|
||
# of trouble
|
||
tokens.delete(name)
|
||
|
||
# If there aren't any params then we need to correct the params node
|
||
# location information
|
||
if params.is_a?(Params) && params.empty?
|
||
end_char = name.location.end_char
|
||
location =
|
||
Location.new(
|
||
start_line: params.location.start_line,
|
||
start_char: end_char,
|
||
end_line: params.location.end_line,
|
||
end_char: end_char
|
||
)
|
||
|
||
params = Params.new(location: location)
|
||
end
|
||
|
||
beginning = find_token(Kw, 'def')
|
||
ending = find_token(Kw, 'end')
|
||
|
||
bodystmt.bind(
|
||
find_next_statement_start(params.location.end_char),
|
||
ending.location.start_char
|
||
)
|
||
|
||
Defs.new(
|
||
target: target,
|
||
operator: operator,
|
||
name: name,
|
||
params: params,
|
||
bodystmt: bodystmt,
|
||
location: beginning.location.to(ending.location)
|
||
)
|
||
end
|
||
|
||
# DoBlock represents passing a block to a method call using the +do+ and +end+
|
||
# keywords.
|
||
#
|
||
# method do |value|
|
||
# end
|
||
#
|
||
class DoBlock
|
||
# [Kw] the do keyword that opens this block
|
||
attr_reader :keyword
|
||
|
||
# [nil | BlockVar] the optional variable declaration within this block
|
||
attr_reader :block_var
|
||
|
||
# [BodyStmt] the expressions to be executed within this block
|
||
attr_reader :bodystmt
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(keyword:, block_var:, bodystmt:, location:)
|
||
@keyword = keyword
|
||
@block_var = block_var
|
||
@bodystmt = bodystmt
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('do_block')
|
||
|
||
if block_var
|
||
q.breakable
|
||
q.pp(block_var)
|
||
end
|
||
|
||
q.breakable
|
||
q.pp(bodystmt)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :do_block,
|
||
keyword: keyword,
|
||
block_var: block_var,
|
||
bodystmt: bodystmt,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_do_block: (BlockVar block_var, BodyStmt bodystmt) -> DoBlock
|
||
def on_do_block(block_var, bodystmt)
|
||
beginning = find_token(Kw, 'do')
|
||
ending = find_token(Kw, 'end')
|
||
|
||
bodystmt.bind(
|
||
find_next_statement_start((block_var || beginning).location.end_char),
|
||
ending.location.start_char
|
||
)
|
||
|
||
DoBlock.new(
|
||
keyword: beginning,
|
||
block_var: block_var,
|
||
bodystmt: bodystmt,
|
||
location: beginning.location.to(ending.location)
|
||
)
|
||
end
|
||
|
||
# Dot2 represents using 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 Dot2
|
||
# [nil | untyped] the left side of the expression
|
||
attr_reader :left
|
||
|
||
# [nil | untyped] the right side of the expression
|
||
attr_reader :right
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(left:, right:, location:)
|
||
@left = left
|
||
@right = right
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('dot2')
|
||
|
||
if left
|
||
q.breakable
|
||
q.pp(left)
|
||
end
|
||
|
||
if right
|
||
q.breakable
|
||
q.pp(right)
|
||
end
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :dot2, left: left, right: right, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_dot2: ((nil | untyped) left, (nil | untyped) right) -> Dot2
|
||
def on_dot2(left, right)
|
||
operator = find_token(Op, '..')
|
||
|
||
beginning = left || operator
|
||
ending = right || operator
|
||
|
||
Dot2.new(
|
||
left: left,
|
||
right: right,
|
||
location: beginning.location.to(ending.location)
|
||
)
|
||
end
|
||
|
||
# Dot3 represents using the ... operator between two expressions. Usually this
|
||
# is to create a range object. It's effectively the same event as the Dot2
|
||
# node but with this operator you're asking Ruby to omit the final value.
|
||
#
|
||
# 1...2
|
||
#
|
||
# Like Dot2 it can also be 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 Dot3
|
||
# [nil | untyped] the left side of the expression
|
||
attr_reader :left
|
||
|
||
# [nil | untyped] the right side of the expression
|
||
attr_reader :right
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(left:, right:, location:)
|
||
@left = left
|
||
@right = right
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('dot3')
|
||
|
||
if left
|
||
q.breakable
|
||
q.pp(left)
|
||
end
|
||
|
||
if right
|
||
q.breakable
|
||
q.pp(right)
|
||
end
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :dot3, left: left, right: right, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_dot3: ((nil | untyped) left, (nil | untyped) right) -> Dot3
|
||
def on_dot3(left, right)
|
||
operator = find_token(Op, '...')
|
||
|
||
beginning = left || operator
|
||
ending = right || operator
|
||
|
||
Dot3.new(
|
||
left: left,
|
||
right: right,
|
||
location: beginning.location.to(ending.location)
|
||
)
|
||
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
|
||
# [Array[ StringDVar | StringEmbExpr | TStringContent ]] the parts of the
|
||
# dynamic symbol
|
||
attr_reader :parts
|
||
|
||
# [String] the quote used to delimit the dynamic symbol
|
||
attr_reader :quote
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(parts:, quote:, location:)
|
||
@parts = parts
|
||
@quote = quote
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('dyna_symbol')
|
||
|
||
q.breakable
|
||
q.group(2, '(', ')') { q.seplist(parts) { |part| q.pp(part) } }
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :dyna_symbol, parts: parts, quote: quote, loc: location }.to_json(
|
||
*opts
|
||
)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_dyna_symbol: (StringContent string_content) -> DynaSymbol
|
||
def on_dyna_symbol(string_content)
|
||
if find_token(SymBeg, consume: false)
|
||
# A normal dynamic symbol
|
||
symbeg = find_token(SymBeg)
|
||
tstring_end = find_token(TStringEnd)
|
||
|
||
DynaSymbol.new(
|
||
quote: symbeg.value,
|
||
parts: string_content.parts,
|
||
location: symbeg.location.to(tstring_end.location)
|
||
)
|
||
else
|
||
# A dynamic symbol as a hash key
|
||
tstring_beg = find_token(TStringBeg)
|
||
label_end = find_token(LabelEnd)
|
||
|
||
DynaSymbol.new(
|
||
parts: string_content.parts,
|
||
quote: label_end.value[0],
|
||
location: tstring_beg.location.to(label_end.location)
|
||
)
|
||
end
|
||
end
|
||
|
||
# Else represents the end of an +if+, +unless+, or +case+ chain.
|
||
#
|
||
# if variable
|
||
# else
|
||
# end
|
||
#
|
||
class Else
|
||
# [Statements] the expressions to be executed
|
||
attr_reader :statements
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(statements:, location:)
|
||
@statements = statements
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('else')
|
||
|
||
q.breakable
|
||
q.pp(statements)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :else, stmts: statements, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_else: (Statements statements) -> Else
|
||
def on_else(statements)
|
||
beginning = find_token(Kw, 'else')
|
||
|
||
# else can either end with an end keyword (in which case we'll want to
|
||
# consume that event) or it can end with an ensure keyword (in which case
|
||
# we'll leave that to the ensure to handle).
|
||
index =
|
||
tokens.rindex do |token|
|
||
token.is_a?(Kw) && %w[end ensure].include?(token.value)
|
||
end
|
||
|
||
node = tokens[index]
|
||
ending = node.value == 'end' ? tokens.delete_at(index) : node
|
||
|
||
statements.bind(beginning.location.end_char, ending.location.start_char)
|
||
|
||
Else.new(
|
||
statements: statements,
|
||
location: beginning.location.to(ending.location)
|
||
)
|
||
end
|
||
|
||
# Elsif represents another clause in an +if+ or +unless+ chain.
|
||
#
|
||
# if variable
|
||
# elsif other_variable
|
||
# end
|
||
#
|
||
class Elsif
|
||
# [untyped] 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
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(predicate:, statements:, consequent:, location:)
|
||
@predicate = predicate
|
||
@statements = statements
|
||
@consequent = consequent
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('elsif')
|
||
|
||
q.breakable
|
||
q.pp(predicate)
|
||
|
||
q.breakable
|
||
q.pp(statements)
|
||
|
||
if consequent
|
||
q.breakable
|
||
q.pp(consequent)
|
||
end
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :elsif,
|
||
pred: predicate,
|
||
stmts: statements,
|
||
cons: consequent,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_elsif: (
|
||
# untyped predicate,
|
||
# Statements statements,
|
||
# (nil | Elsif | Else) consequent
|
||
# ) -> Elsif
|
||
def on_elsif(predicate, statements, consequent)
|
||
beginning = find_token(Kw, 'elsif')
|
||
ending = consequent || find_token(Kw, 'end')
|
||
|
||
statements.bind(predicate.location.end_char, ending.location.start_char)
|
||
|
||
Elsif.new(
|
||
predicate: predicate,
|
||
statements: statements,
|
||
consequent: consequent,
|
||
location: beginning.location.to(ending.location)
|
||
)
|
||
end
|
||
|
||
# EmbDoc represents a multi-line comment.
|
||
#
|
||
# =begin
|
||
# first line
|
||
# second line
|
||
# =end
|
||
#
|
||
class EmbDoc
|
||
# [String] the contents of the comment
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
|
||
def inline?
|
||
false
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('embdoc')
|
||
|
||
q.breakable
|
||
q.pp(value)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :embdoc, value: value, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_embdoc: (String value) -> EmbDoc
|
||
def on_embdoc(value)
|
||
@embdoc.value << value
|
||
@embdoc
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_embdoc_beg: (String value) -> EmbDoc
|
||
def on_embdoc_beg(value)
|
||
@embdoc =
|
||
EmbDoc.new(
|
||
value: value,
|
||
location: Location.fixed(line: lineno, char: char_pos)
|
||
)
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_embdoc_end: (String value) -> EmbDoc
|
||
def on_embdoc_end(value)
|
||
location = @embdoc.location
|
||
embdoc =
|
||
EmbDoc.new(
|
||
value: @embdoc.value << value.chomp,
|
||
location:
|
||
Location.new(
|
||
start_line: location.start_line,
|
||
start_char: location.start_char,
|
||
end_line: lineno,
|
||
end_char: char_pos + value.length - 1
|
||
)
|
||
)
|
||
|
||
@comments << embdoc
|
||
@embdoc = nil
|
||
|
||
embdoc
|
||
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
|
||
# [String] the #{ used in the string
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_embexpr_beg: (String value) -> EmbExprBeg
|
||
def on_embexpr_beg(value)
|
||
node =
|
||
EmbExprBeg.new(
|
||
value: value,
|
||
location: Location.token(line: lineno, char: char_pos, size: value.size)
|
||
)
|
||
|
||
tokens << node
|
||
node
|
||
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
|
||
# [String] the } used in the string
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_embexpr_end: (String value) -> EmbExprEnd
|
||
def on_embexpr_end(value)
|
||
node =
|
||
EmbExprEnd.new(
|
||
value: value,
|
||
location: Location.token(line: lineno, char: char_pos, size: value.size)
|
||
)
|
||
|
||
tokens << node
|
||
node
|
||
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
|
||
# [String] the # used in the string
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_embvar: (String value) -> EmbVar
|
||
def on_embvar(value)
|
||
node =
|
||
EmbVar.new(
|
||
value: value,
|
||
location: Location.token(line: lineno, char: char_pos, size: value.size)
|
||
)
|
||
|
||
tokens << node
|
||
node
|
||
end
|
||
|
||
# Ensure represents the use of the +ensure+ keyword and its subsequent
|
||
# statements.
|
||
#
|
||
# begin
|
||
# ensure
|
||
# end
|
||
#
|
||
class Ensure
|
||
# [Kw] the ensure keyword that began this node
|
||
attr_reader :keyword
|
||
|
||
# [Statements] the expressions to be executed
|
||
attr_reader :statements
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(keyword:, statements:, location:)
|
||
@keyword = keyword
|
||
@statements = statements
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('ensure')
|
||
|
||
q.breakable
|
||
q.pp(statements)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :ensure,
|
||
keyword: keyword,
|
||
stmts: statements,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_ensure: (Statements statements) -> Ensure
|
||
def on_ensure(statements)
|
||
keyword = find_token(Kw, 'ensure')
|
||
|
||
# We don't want to consume the :@kw event, because that would break
|
||
# def..ensure..end chains.
|
||
ending = find_token(Kw, 'end', consume: false)
|
||
statements.bind(
|
||
find_next_statement_start(keyword.location.end_char),
|
||
ending.location.start_char
|
||
)
|
||
|
||
Ensure.new(
|
||
keyword: keyword,
|
||
statements: statements,
|
||
location: keyword.location.to(ending.location)
|
||
)
|
||
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
|
||
# [String] the comma
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('excessed_comma')
|
||
|
||
q.breakable
|
||
q.pp(value)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :excessed_comma, value: value, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# The handler for this event accepts no parameters (though in previous
|
||
# versions of Ruby it accepted a string literal with a value of ",").
|
||
#
|
||
# :call-seq:
|
||
# on_excessed_comma: () -> ExcessedComma
|
||
def on_excessed_comma(*)
|
||
comma = find_token(Comma)
|
||
|
||
ExcessedComma.new(value: comma.value, location: comma.location)
|
||
end
|
||
|
||
# FCall represents the piece of a method call that comes before any arguments
|
||
# (i.e., just the name of the method). It is used in places where the parser
|
||
# is sure that it is a method call and not potentially a local variable.
|
||
#
|
||
# method(argument)
|
||
#
|
||
# In the above example, it's referring to the +method+ segment.
|
||
class FCall
|
||
# [Const | Ident] the name of the method
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('fcall')
|
||
|
||
q.breakable
|
||
q.pp(value)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :fcall, value: value, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_fcall: ((Const | Ident) value) -> FCall
|
||
def on_fcall(value)
|
||
FCall.new(value: value, location: value.location)
|
||
end
|
||
|
||
# Field is always the child of an assignment. It represents assigning to a
|
||
# “field” on an object.
|
||
#
|
||
# object.variable = value
|
||
#
|
||
class Field
|
||
# [untyped] 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
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(parent:, operator:, name:, location:)
|
||
@parent = parent
|
||
@operator = operator
|
||
@name = name
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('field')
|
||
|
||
q.breakable
|
||
q.pp(parent)
|
||
|
||
q.breakable
|
||
q.pp(operator)
|
||
|
||
q.breakable
|
||
q.pp(name)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :field,
|
||
parent: parent,
|
||
op: operator,
|
||
name: name,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_field: (
|
||
# untyped parent,
|
||
# (:"::" | Op | Period) operator
|
||
# (Const | Ident) name
|
||
# ) -> Field
|
||
def on_field(parent, operator, name)
|
||
Field.new(
|
||
parent: parent,
|
||
operator: operator,
|
||
name: name,
|
||
location: parent.location.to(name.location)
|
||
)
|
||
end
|
||
|
||
# FloatLiteral represents a floating point number literal.
|
||
#
|
||
# 1.0
|
||
#
|
||
class FloatLiteral
|
||
# [String] the value of the floating point number literal
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('float')
|
||
|
||
q.breakable
|
||
q.pp(value)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :float, value: value, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_float: (String value) -> FloatLiteral
|
||
def on_float(value)
|
||
node =
|
||
FloatLiteral.new(
|
||
value: value,
|
||
location: Location.token(line: lineno, char: char_pos, size: value.size)
|
||
)
|
||
|
||
tokens << node
|
||
node
|
||
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
|
||
# [nil | untyped] the optional constant wrapper
|
||
attr_reader :constant
|
||
|
||
# [VarField] the splat on the left-hand side
|
||
attr_reader :left
|
||
|
||
# [Array[ untyped ]] 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
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(constant:, left:, values:, right:, location:)
|
||
@constant = constant
|
||
@left = left
|
||
@values = values
|
||
@right = right
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('fndptn')
|
||
|
||
if constant
|
||
q.breakable
|
||
q.pp(constant)
|
||
end
|
||
|
||
q.breakable
|
||
q.pp(left)
|
||
|
||
q.breakable
|
||
q.group(2, '(', ')') { q.seplist(values) { |value| q.pp(value) } }
|
||
|
||
q.breakable
|
||
q.pp(right)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :fndptn,
|
||
constant: constant,
|
||
left: left,
|
||
values: values,
|
||
right: right,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_fndptn: (
|
||
# (nil | untyped) constant,
|
||
# VarField left,
|
||
# Array[untyped] values,
|
||
# VarField right
|
||
# ) -> FndPtn
|
||
def on_fndptn(constant, left, values, right)
|
||
beginning = constant || find_token(LBracket)
|
||
ending = find_token(RBracket)
|
||
|
||
FndPtn.new(
|
||
constant: constant,
|
||
left: left,
|
||
values: values,
|
||
right: right,
|
||
location: beginning.location.to(ending.location)
|
||
)
|
||
end
|
||
|
||
# For represents using a +for+ loop.
|
||
#
|
||
# for value in list do
|
||
# end
|
||
#
|
||
class For
|
||
# [MLHS | MLHSAddStar | VarField] the variable declaration being used to
|
||
# pull values out of the object being enumerated
|
||
attr_reader :index
|
||
|
||
# [untyped] the object being enumerated in the loop
|
||
attr_reader :collection
|
||
|
||
# [Statements] the statements to be executed
|
||
attr_reader :statements
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(index:, collection:, statements:, location:)
|
||
@index = index
|
||
@collection = collection
|
||
@statements = statements
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('for')
|
||
|
||
q.breakable
|
||
q.pp(index)
|
||
|
||
q.breakable
|
||
q.pp(collection)
|
||
|
||
q.breakable
|
||
q.pp(statements)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :for,
|
||
index: index,
|
||
collection: collection,
|
||
stmts: statements,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_for: (
|
||
# (MLHS | MLHSAddStar | VarField) value,
|
||
# untyped collection,
|
||
# Statements statements
|
||
# ) -> For
|
||
def on_for(index, collection, statements)
|
||
beginning = find_token(Kw, 'for')
|
||
ending = find_token(Kw, 'end')
|
||
|
||
# Consume the do keyword if it exists so that it doesn't get confused for
|
||
# some other block
|
||
keyword = find_token(Kw, 'do', consume: false)
|
||
if keyword && keyword.location.start_char > collection.location.end_char &&
|
||
keyword.location.end_char < ending.location.start_char
|
||
tokens.delete(keyword)
|
||
end
|
||
|
||
statements.bind(
|
||
(keyword || collection).location.end_char,
|
||
ending.location.start_char
|
||
)
|
||
|
||
For.new(
|
||
index: index,
|
||
collection: collection,
|
||
statements: statements,
|
||
location: beginning.location.to(ending.location)
|
||
)
|
||
end
|
||
|
||
# GVar represents a global variable literal.
|
||
#
|
||
# $variable
|
||
#
|
||
class GVar
|
||
# [String] the name of the global variable
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('gvar')
|
||
|
||
q.breakable
|
||
q.pp(value)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :gvar, value: value, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_gvar: (String value) -> GVar
|
||
def on_gvar(value)
|
||
node =
|
||
GVar.new(
|
||
value: value,
|
||
location: Location.token(line: lineno, char: char_pos, size: value.size)
|
||
)
|
||
|
||
tokens << node
|
||
node
|
||
end
|
||
|
||
# HashLiteral represents a hash literal.
|
||
#
|
||
# { key => value }
|
||
#
|
||
class HashLiteral
|
||
# [nil | AssocListFromArgs] the contents of the hash
|
||
attr_reader :contents
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(contents:, location:)
|
||
@contents = contents
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('hash')
|
||
|
||
q.breakable
|
||
q.pp(contents)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :hash, cnts: contents, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_hash: ((nil | AssocListFromArgs) contents) -> HashLiteral
|
||
def on_hash(contents)
|
||
lbrace = find_token(LBrace)
|
||
rbrace = find_token(RBrace)
|
||
|
||
if contents
|
||
# Here we're going to expand out the location information for the contents
|
||
# node so that it can grab up any remaining comments inside the hash.
|
||
location =
|
||
Location.new(
|
||
start_line: contents.location.start_line,
|
||
start_char: lbrace.location.end_char,
|
||
end_line: contents.location.end_line,
|
||
end_char: rbrace.location.start_char
|
||
)
|
||
|
||
contents = contents.class.new(assocs: contents.assocs, location: location)
|
||
end
|
||
|
||
HashLiteral.new(
|
||
contents: contents,
|
||
location: lbrace.location.to(rbrace.location)
|
||
)
|
||
end
|
||
|
||
# Heredoc represents a heredoc string literal.
|
||
#
|
||
# <<~DOC
|
||
# contents
|
||
# DOC
|
||
#
|
||
class Heredoc
|
||
# [HeredocBeg] the opening of the heredoc
|
||
attr_reader :beginning
|
||
|
||
# [String] the ending of the heredoc
|
||
attr_reader :ending
|
||
|
||
# [Array[ StringEmbExpr | StringDVar | TStringContent ]] the parts of the
|
||
# heredoc string literal
|
||
attr_reader :parts
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(beginning:, ending: nil, parts: [], location:)
|
||
@beginning = beginning
|
||
@ending = ending
|
||
@parts = parts
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('heredoc')
|
||
|
||
q.breakable
|
||
q.group(2, '(', ')') { q.seplist(parts) { |part| q.pp(part) } }
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :heredoc,
|
||
beging: beginning,
|
||
ending: ending,
|
||
parts: parts,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# HeredocBeg represents the beginning declaration of a heredoc.
|
||
#
|
||
# <<~DOC
|
||
# contents
|
||
# DOC
|
||
#
|
||
# In the example above the HeredocBeg node represents <<~DOC.
|
||
class HeredocBeg
|
||
# [String] the opening declaration of the heredoc
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('heredoc_beg')
|
||
|
||
q.breakable
|
||
q.pp(value)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :heredoc_beg, value: value, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_heredoc_beg: (String value) -> HeredocBeg
|
||
def on_heredoc_beg(value)
|
||
location =
|
||
Location.token(line: lineno, char: char_pos, size: value.size + 1)
|
||
|
||
# Here we're going to artificially create an extra node type so that if
|
||
# there are comments after the declaration of a heredoc, they get printed.
|
||
beginning = HeredocBeg.new(value: value, location: location)
|
||
@heredocs << Heredoc.new(beginning: beginning, location: location)
|
||
|
||
beginning
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_heredoc_dedent: (StringContent string, Integer width) -> Heredoc
|
||
def on_heredoc_dedent(string, width)
|
||
heredoc = @heredocs[-1]
|
||
|
||
@heredocs[-1] =
|
||
Heredoc.new(
|
||
beginning: heredoc.beginning,
|
||
ending: heredoc.ending,
|
||
parts: string.parts,
|
||
location: heredoc.location
|
||
)
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_heredoc_end: (String value) -> Heredoc
|
||
def on_heredoc_end(value)
|
||
heredoc = @heredocs[-1]
|
||
|
||
@heredocs[-1] =
|
||
Heredoc.new(
|
||
beginning: heredoc.beginning,
|
||
ending: value.chomp,
|
||
parts: heredoc.parts,
|
||
location:
|
||
Location.new(
|
||
start_line: heredoc.location.start_line,
|
||
start_char: heredoc.location.start_char,
|
||
end_line: lineno,
|
||
end_char: char_pos
|
||
)
|
||
)
|
||
end
|
||
|
||
# HshPtn represents matching against a hash pattern using the Ruby 2.7+
|
||
# pattern matching syntax.
|
||
#
|
||
# case value
|
||
# in { key: }
|
||
# end
|
||
#
|
||
class HshPtn
|
||
# [nil | untyped] the optional constant wrapper
|
||
attr_reader :constant
|
||
|
||
# [Array[ [Label, untyped] ]] 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
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(constant:, keywords:, keyword_rest:, location:)
|
||
@constant = constant
|
||
@keywords = keywords
|
||
@keyword_rest = keyword_rest
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('hshptn')
|
||
|
||
if constant
|
||
q.breakable
|
||
q.pp(constant)
|
||
end
|
||
|
||
if keywords.any?
|
||
q.breakable
|
||
q.group(2, '(', ')') do
|
||
q.seplist(keywords) { |keyword| q.pp(keyword) }
|
||
end
|
||
end
|
||
|
||
if keyword_rest
|
||
q.breakable
|
||
q.pp(keyword_rest)
|
||
end
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :hshptn,
|
||
constant: constant,
|
||
keywords: keywords,
|
||
kwrest: keyword_rest,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_hshptn: (
|
||
# (nil | untyped) constant,
|
||
# Array[[Label, untyped]] keywords,
|
||
# (nil | VarField) keyword_rest
|
||
# ) -> HshPtn
|
||
def on_hshptn(constant, keywords, keyword_rest)
|
||
parts = [constant, keywords, keyword_rest].flatten(2).compact
|
||
|
||
HshPtn.new(
|
||
constant: constant,
|
||
keywords: keywords,
|
||
keyword_rest: keyword_rest,
|
||
location: parts[0].location.to(parts[-1].location)
|
||
)
|
||
end
|
||
|
||
# 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
|
||
# [String] the value of the identifier
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('ident')
|
||
q.breakable
|
||
q.pp(value)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :ident,
|
||
value: value.force_encoding('UTF-8'),
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_ident: (String value) -> Ident
|
||
def on_ident(value)
|
||
node =
|
||
Ident.new(
|
||
value: value,
|
||
location: Location.token(line: lineno, char: char_pos, size: value.size)
|
||
)
|
||
|
||
tokens << node
|
||
node
|
||
end
|
||
|
||
# If represents the first clause in an +if+ chain.
|
||
#
|
||
# if predicate
|
||
# end
|
||
#
|
||
class If
|
||
# [untyped] 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
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(predicate:, statements:, consequent:, location:)
|
||
@predicate = predicate
|
||
@statements = statements
|
||
@consequent = consequent
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('if')
|
||
|
||
q.breakable
|
||
q.pp(predicate)
|
||
|
||
q.breakable
|
||
q.pp(statements)
|
||
|
||
if consequent
|
||
q.breakable
|
||
q.pp(consequent)
|
||
end
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :if,
|
||
pred: predicate,
|
||
stmts: statements,
|
||
cons: consequent,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_if: (
|
||
# untyped predicate,
|
||
# Statements statements,
|
||
# (nil | Elsif | Else) consequent
|
||
# ) -> If
|
||
def on_if(predicate, statements, consequent)
|
||
beginning = find_token(Kw, 'if')
|
||
ending = consequent || find_token(Kw, 'end')
|
||
|
||
statements.bind(predicate.location.end_char, ending.location.start_char)
|
||
|
||
If.new(
|
||
predicate: predicate,
|
||
statements: statements,
|
||
consequent: consequent,
|
||
location: beginning.location.to(ending.location)
|
||
)
|
||
end
|
||
|
||
# IfOp represents a ternary clause.
|
||
#
|
||
# predicate ? truthy : falsy
|
||
#
|
||
class IfOp
|
||
# [untyped] the expression to be checked
|
||
attr_reader :predicate
|
||
|
||
# [untyped] the expression to be executed if the predicate is truthy
|
||
attr_reader :truthy
|
||
|
||
# [untyped] the expression to be executed if the predicate is falsy
|
||
attr_reader :falsy
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(predicate:, truthy:, falsy:, location:)
|
||
@predicate = predicate
|
||
@truthy = truthy
|
||
@falsy = falsy
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('ifop')
|
||
|
||
q.breakable
|
||
q.pp(predicate)
|
||
|
||
q.breakable
|
||
q.pp(truthy)
|
||
|
||
q.breakable
|
||
q.pp(falsy)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :ifop,
|
||
pred: predicate,
|
||
tthy: truthy,
|
||
flsy: falsy,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_ifop: (untyped predicate, untyped truthy, untyped falsy) -> IfOp
|
||
def on_ifop(predicate, truthy, falsy)
|
||
IfOp.new(
|
||
predicate: predicate,
|
||
truthy: truthy,
|
||
falsy: falsy,
|
||
location: predicate.location.to(falsy.location)
|
||
)
|
||
end
|
||
|
||
# IfMod represents the modifier form of an +if+ statement.
|
||
#
|
||
# expression if predicate
|
||
#
|
||
class IfMod
|
||
# [untyped] the expression to be executed
|
||
attr_reader :statement
|
||
|
||
# [untyped] the expression to be checked
|
||
attr_reader :predicate
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(statement:, predicate:, location:)
|
||
@statement = statement
|
||
@predicate = predicate
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('if_mod')
|
||
|
||
q.breakable
|
||
q.pp(statement)
|
||
|
||
q.breakable
|
||
q.pp(predicate)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :if_mod,
|
||
stmt: statement,
|
||
pred: predicate,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_if_mod: (untyped predicate, untyped statement) -> IfMod
|
||
def on_if_mod(predicate, statement)
|
||
find_token(Kw, 'if')
|
||
|
||
IfMod.new(
|
||
statement: statement,
|
||
predicate: predicate,
|
||
location: statement.location.to(predicate.location)
|
||
)
|
||
end
|
||
|
||
# def on_ignored_nl(value)
|
||
# value
|
||
# end
|
||
|
||
# def on_ignored_sp(value)
|
||
# value
|
||
# end
|
||
|
||
# Imaginary represents an imaginary number literal.
|
||
#
|
||
# 1i
|
||
#
|
||
class Imaginary
|
||
# [String] the value of the imaginary number literal
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('imaginary')
|
||
|
||
q.breakable
|
||
q.pp(value)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :imaginary, value: value, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_imaginary: (String value) -> Imaginary
|
||
def on_imaginary(value)
|
||
node =
|
||
Imaginary.new(
|
||
value: value,
|
||
location: Location.token(line: lineno, char: char_pos, size: value.size)
|
||
)
|
||
|
||
tokens << node
|
||
node
|
||
end
|
||
|
||
# In represents using the +in+ keyword within the Ruby 2.7+ pattern matching
|
||
# syntax.
|
||
#
|
||
# case value
|
||
# in pattern
|
||
# end
|
||
#
|
||
class In
|
||
# [untyped] 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
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(pattern:, statements:, consequent:, location:)
|
||
@pattern = pattern
|
||
@statements = statements
|
||
@consequent = consequent
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('in')
|
||
|
||
q.breakable
|
||
q.pp(pattern)
|
||
|
||
q.breakable
|
||
q.pp(statements)
|
||
|
||
if consequent
|
||
q.breakable
|
||
q.pp(consequent)
|
||
end
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :in,
|
||
pattern: pattern,
|
||
stmts: statements,
|
||
cons: consequent,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_in: (RAssign pattern, nil statements, nil consequent) -> RAssign
|
||
# | (
|
||
# untyped pattern,
|
||
# Statements statements,
|
||
# (nil | In | Else) consequent
|
||
# ) -> In
|
||
def on_in(pattern, statements, consequent)
|
||
# Here we have a rightward assignment
|
||
return pattern unless statements
|
||
|
||
beginning = find_token(Kw, 'in')
|
||
ending = consequent || find_token(Kw, 'end')
|
||
|
||
statements.bind(beginning.location.end_char, ending.location.start_char)
|
||
|
||
In.new(
|
||
pattern: pattern,
|
||
statements: statements,
|
||
consequent: consequent,
|
||
location: beginning.location.to(ending.location)
|
||
)
|
||
end
|
||
|
||
# Int represents an integer number literal.
|
||
#
|
||
# 1
|
||
#
|
||
class Int
|
||
# [String] the value of the integer
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('int')
|
||
|
||
q.breakable
|
||
q.pp(value)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :int, value: value, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_int: (String value) -> Int
|
||
def on_int(value)
|
||
node =
|
||
Int.new(
|
||
value: value,
|
||
location: Location.token(line: lineno, char: char_pos, size: value.size)
|
||
)
|
||
|
||
tokens << node
|
||
node
|
||
end
|
||
|
||
# IVar represents an instance variable literal.
|
||
#
|
||
# @variable
|
||
#
|
||
class IVar
|
||
# [String] the name of the instance variable
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('ivar')
|
||
|
||
q.breakable
|
||
q.pp(value)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :ivar, value: value, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_ivar: (String value) -> IVar
|
||
def on_ivar(value)
|
||
node =
|
||
IVar.new(
|
||
value: value,
|
||
location: Location.token(line: lineno, char: char_pos, size: value.size)
|
||
)
|
||
|
||
tokens << node
|
||
node
|
||
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
|
||
# [String] the value of the keyword
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('kw')
|
||
|
||
q.breakable
|
||
q.pp(value)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :kw, value: value, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_kw: (String value) -> Kw
|
||
def on_kw(value)
|
||
node =
|
||
Kw.new(
|
||
value: value,
|
||
location: Location.token(line: lineno, char: char_pos, size: value.size)
|
||
)
|
||
|
||
tokens << node
|
||
node
|
||
end
|
||
|
||
# KwRestParam represents defining a parameter in a method definition that
|
||
# accepts all remaining keyword parameters.
|
||
#
|
||
# def method(**kwargs) end
|
||
#
|
||
class KwRestParam
|
||
# [nil | Ident] the name of the parameter
|
||
attr_reader :name
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(name:, location:)
|
||
@name = name
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('kwrest_param')
|
||
|
||
q.breakable
|
||
q.pp(name)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :kwrest_param, name: name, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_kwrest_param: ((nil | Ident) name) -> KwRestParam
|
||
def on_kwrest_param(name)
|
||
location = find_token(Op, '**').location
|
||
location = location.to(name.location) if name
|
||
|
||
KwRestParam.new(name: name, location: location)
|
||
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
|
||
# [String] the value of the label
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('label')
|
||
|
||
q.breakable
|
||
q.text(':')
|
||
q.text(value[0...-1])
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :label, value: value, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_label: (String value) -> Label
|
||
def on_label(value)
|
||
node =
|
||
Label.new(
|
||
value: value,
|
||
location: Location.token(line: lineno, char: char_pos, size: value.size)
|
||
)
|
||
|
||
tokens << node
|
||
node
|
||
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
|
||
# [String] the end of the label
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_label_end: (String value) -> LabelEnd
|
||
def on_label_end(value)
|
||
node =
|
||
LabelEnd.new(
|
||
value: value,
|
||
location: Location.token(line: lineno, char: char_pos, size: value.size)
|
||
)
|
||
|
||
tokens << node
|
||
node
|
||
end
|
||
|
||
# Lambda represents using a lambda literal (not the lambda method call).
|
||
#
|
||
# ->(value) { value * 2 }
|
||
#
|
||
class Lambda
|
||
# [Params | Paren] the parameter declaration for this lambda
|
||
attr_reader :params
|
||
|
||
# [BodyStmt | Statements] the expressions to be executed in this lambda
|
||
attr_reader :statements
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(params:, statements:, location:)
|
||
@params = params
|
||
@statements = statements
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('lambda')
|
||
|
||
q.breakable
|
||
q.pp(params)
|
||
|
||
q.breakable
|
||
q.pp(statements)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :lambda,
|
||
params: params,
|
||
stmts: statements,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_lambda: (
|
||
# (Params | Paren) params,
|
||
# (BodyStmt | Statements) statements
|
||
# ) -> Lambda
|
||
def on_lambda(params, statements)
|
||
beginning = find_token(TLambda)
|
||
|
||
if token = find_token(TLamBeg, consume: false)
|
||
opening = tokens.delete(token)
|
||
closing = find_token(RBrace)
|
||
else
|
||
opening = find_token(Kw, 'do')
|
||
closing = find_token(Kw, 'end')
|
||
end
|
||
|
||
statements.bind(opening.location.end_char, closing.location.start_char)
|
||
|
||
Lambda.new(
|
||
params: params,
|
||
statements: statements,
|
||
location: beginning.location.to(closing.location)
|
||
)
|
||
end
|
||
|
||
# LBrace represents the use of a left brace, i.e., {.
|
||
class LBrace
|
||
# [String] the left brace
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('lbrace')
|
||
|
||
q.breakable
|
||
q.pp(value)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :lbrace, value: value, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_lbrace: (String value) -> LBrace
|
||
def on_lbrace(value)
|
||
node =
|
||
LBrace.new(
|
||
value: value,
|
||
location: Location.token(line: lineno, char: char_pos, size: value.size)
|
||
)
|
||
|
||
tokens << node
|
||
node
|
||
end
|
||
|
||
# LBracket represents the use of a left bracket, i.e., [.
|
||
class LBracket
|
||
# [String] the left bracket
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_lbracket: (String value) -> LBracket
|
||
def on_lbracket(value)
|
||
node =
|
||
LBracket.new(
|
||
value: value,
|
||
location: Location.token(line: lineno, char: char_pos, size: value.size)
|
||
)
|
||
|
||
tokens << node
|
||
node
|
||
end
|
||
|
||
# LParen represents the use of a left parenthesis, i.e., (.
|
||
class LParen
|
||
# [String] the left parenthesis
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('lparen')
|
||
|
||
q.breakable
|
||
q.pp(value)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :lparen, value: value, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_lparen: (String value) -> LParen
|
||
def on_lparen(value)
|
||
node =
|
||
LParen.new(
|
||
value: value,
|
||
location: Location.token(line: lineno, char: char_pos, size: value.size)
|
||
)
|
||
|
||
tokens << node
|
||
node
|
||
end
|
||
|
||
# def on_magic_comment(key, value)
|
||
# [key, value]
|
||
# 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
|
||
# [Mlhs | MlhsAddPost | MlhsAddStar | MlhsParen] the target of the multiple
|
||
# assignment
|
||
attr_reader :target
|
||
|
||
# [untyped] the value being assigned
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(target:, value:, location:)
|
||
@target = target
|
||
@value = value
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('massign')
|
||
|
||
q.breakable
|
||
q.pp(target)
|
||
|
||
q.breakable
|
||
q.pp(value)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :massign, target: target, value: value, loc: location }.to_json(
|
||
*opts
|
||
)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_massign: (
|
||
# (Mlhs | MlhsAddPost | MlhsAddStar | MlhsParen) target,
|
||
# untyped value
|
||
# ) -> MAssign
|
||
def on_massign(target, value)
|
||
comma_range = target.location.end_char...value.location.start_char
|
||
target.comma = true if source[comma_range].strip.start_with?(',')
|
||
|
||
MAssign.new(
|
||
target: target,
|
||
value: value,
|
||
location: target.location.to(value.location)
|
||
)
|
||
end
|
||
|
||
# MethodAddArg represents a method call with arguments and parentheses.
|
||
#
|
||
# method(argument)
|
||
#
|
||
# MethodAddArg can also represent with a method on an object, as in:
|
||
#
|
||
# object.method(argument)
|
||
#
|
||
# Finally, MethodAddArg can represent calling a method with no receiver that
|
||
# ends in a ?. In this case, the parser knows it's a method call and not a
|
||
# local variable, so it uses a MethodAddArg node as opposed to a VCall node,
|
||
# as in:
|
||
#
|
||
# method?
|
||
#
|
||
class MethodAddArg
|
||
# [Call | FCall] the method call
|
||
attr_reader :call
|
||
|
||
# [ArgParen | Args | ArgsAddBlock] the arguments to the method call
|
||
attr_reader :arguments
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(call:, arguments:, location:)
|
||
@call = call
|
||
@arguments = arguments
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('method_add_arg')
|
||
|
||
q.breakable
|
||
q.pp(call)
|
||
|
||
q.breakable
|
||
q.pp(arguments)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :method_add_arg,
|
||
call: call,
|
||
args: arguments,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_method_add_arg: (
|
||
# (Call | FCall) call,
|
||
# (ArgParen | Args | ArgsAddBlock) arguments
|
||
# ) -> MethodAddArg
|
||
def on_method_add_arg(call, arguments)
|
||
location = call.location
|
||
|
||
location = location.to(arguments.location) unless arguments.is_a?(Args)
|
||
|
||
MethodAddArg.new(call: call, arguments: arguments, location: location)
|
||
end
|
||
|
||
# MethodAddBlock represents a method call with a block argument.
|
||
#
|
||
# method {}
|
||
#
|
||
class MethodAddBlock
|
||
# [Call | Command | CommandCall | FCall | MethodAddArg] the method call
|
||
attr_reader :call
|
||
|
||
# [BraceBlock | DoBlock] the block being sent with the method call
|
||
attr_reader :block
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(call:, block:, location:)
|
||
@call = call
|
||
@block = block
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('method_add_block')
|
||
|
||
q.breakable
|
||
q.pp(call)
|
||
|
||
q.breakable
|
||
q.pp(block)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :method_add_block,
|
||
call: call,
|
||
block: block,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_method_add_block: (
|
||
# (Call | Command | CommandCall | FCall | MethodAddArg) call,
|
||
# (BraceBlock | DoBlock) block
|
||
# ) -> MethodAddBlock
|
||
def on_method_add_block(call, block)
|
||
MethodAddBlock.new(
|
||
call: call,
|
||
block: block,
|
||
location: call.location.to(block.location)
|
||
)
|
||
end
|
||
|
||
# MLHS represents a list of values being destructured on the left-hand side
|
||
# of a multiple assignment.
|
||
#
|
||
# first, second, third = value
|
||
#
|
||
class MLHS
|
||
# Array[ARefField | Field | Ident | MlhsParen | 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
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(parts:, comma: false, location:)
|
||
@parts = parts
|
||
@comma = comma
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('mlhs')
|
||
|
||
q.breakable
|
||
q.group(2, '(', ')') { q.seplist(parts) { |part| q.pp(part) } }
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :mlhs, parts: parts, comma: comma, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_mlhs_add: (
|
||
# MLHS mlhs,
|
||
# (ARefField | Field | Ident | MlhsParen | VarField) part
|
||
# ) -> MLHS
|
||
def on_mlhs_add(mlhs, part)
|
||
if mlhs.parts.empty?
|
||
MLHS.new(parts: [part], location: part.location)
|
||
else
|
||
MLHS.new(
|
||
parts: mlhs.parts << part,
|
||
location: mlhs.location.to(part.location)
|
||
)
|
||
end
|
||
end
|
||
|
||
# MLHSAddPost represents adding another set of variables onto a list of
|
||
# assignments after a splat variable within a multiple assignment.
|
||
#
|
||
# left, *middle, right = values
|
||
#
|
||
class MLHSAddPost
|
||
# [MlhsAddStar] the value being starred
|
||
attr_reader :star
|
||
|
||
# [Mlhs] the values after the star
|
||
attr_reader :mlhs
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(star:, mlhs:, location:)
|
||
@star = star
|
||
@mlhs = mlhs
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('mlhs_add_post')
|
||
|
||
q.breakable
|
||
q.pp(star)
|
||
|
||
q.breakable
|
||
q.pp(mlhs)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :mlhs_add_post, star: star, mlhs: mlhs, loc: location }.to_json(
|
||
*opts
|
||
)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_mlhs_add_post: (MLHSAddStar star, MLHS mlhs) -> MLHSAddPost
|
||
def on_mlhs_add_post(star, mlhs)
|
||
MLHSAddPost.new(
|
||
star: star,
|
||
mlhs: mlhs,
|
||
location: star.location.to(mlhs.location)
|
||
)
|
||
end
|
||
|
||
# MLHSAddStar represents a splatted variable inside of a multiple assignment
|
||
# on the left hand side.
|
||
#
|
||
# first, *rest = values
|
||
#
|
||
class MLHSAddStar
|
||
# [MLHS] the values before the starred expression
|
||
attr_reader :mlhs
|
||
|
||
# [nil | ARefField | Field | Ident | VarField] the expression being
|
||
# splatted
|
||
attr_reader :star
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(mlhs:, star:, location:)
|
||
@mlhs = mlhs
|
||
@star = star
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('mlhs_add_star')
|
||
|
||
q.breakable
|
||
q.pp(mlhs)
|
||
|
||
q.breakable
|
||
q.pp(star)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :mlhs_add_star, mlhs: mlhs, star: star, loc: location }.to_json(
|
||
*opts
|
||
)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_mlhs_add_star: (
|
||
# MLHS mlhs,
|
||
# (nil | ARefField | Field | Ident | VarField) part
|
||
# ) -> MLHSAddStar
|
||
def on_mlhs_add_star(mlhs, part)
|
||
beginning = find_token(Op, '*')
|
||
ending = part || beginning
|
||
|
||
MLHSAddStar.new(
|
||
mlhs: mlhs,
|
||
star: part,
|
||
location: beginning.location.to(ending.location)
|
||
)
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_mlhs_new: () -> MLHS
|
||
def on_mlhs_new
|
||
MLHS.new(parts: [], location: Location.fixed(line: lineno, char: char_pos))
|
||
end
|
||
|
||
# MLHSParen represents parentheses being used to destruct values in a multiple
|
||
# assignment on the left hand side.
|
||
#
|
||
# (left, right) = value
|
||
#
|
||
class MLHSParen
|
||
# [Mlhs | MlhsAddPost | MlhsAddStar | MlhsParen] the contents inside of the
|
||
# parentheses
|
||
attr_reader :contents
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(contents:, location:)
|
||
@contents = contents
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('mlhs_paren')
|
||
|
||
q.breakable
|
||
q.pp(contents)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :mlhs_paren, cnts: contents, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_mlhs_paren: (
|
||
# (Mlhs | MlhsAddPost | MlhsAddStar | MlhsParen) contents
|
||
# ) -> MLHSParen
|
||
def on_mlhs_paren(contents)
|
||
lparen = find_token(LParen)
|
||
rparen = find_token(RParen)
|
||
|
||
comma_range = lparen.location.end_char...rparen.location.start_char
|
||
contents.comma = true if source[comma_range].strip.end_with?(',')
|
||
|
||
MLHSParen.new(
|
||
contents: contents,
|
||
location: lparen.location.to(rparen.location)
|
||
)
|
||
end
|
||
|
||
# ModuleDeclaration represents defining a module using the +module+ keyword.
|
||
#
|
||
# module Namespace
|
||
# end
|
||
#
|
||
class ModuleDeclaration
|
||
# [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
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(constant:, bodystmt:, location:)
|
||
@constant = constant
|
||
@bodystmt = bodystmt
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('module')
|
||
|
||
q.breakable
|
||
q.pp(constant)
|
||
|
||
q.breakable
|
||
q.pp(bodystmt)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :module,
|
||
constant: constant,
|
||
bodystmt: bodystmt,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_module: (
|
||
# (ConstPathRef | ConstRef | TopConstRef) constant,
|
||
# BodyStmt bodystmt
|
||
# ) -> ModuleDeclaration
|
||
def on_module(constant, bodystmt)
|
||
beginning = find_token(Kw, 'module')
|
||
ending = find_token(Kw, 'end')
|
||
|
||
bodystmt.bind(
|
||
find_next_statement_start(constant.location.end_char),
|
||
ending.location.start_char
|
||
)
|
||
|
||
ModuleDeclaration.new(
|
||
constant: constant,
|
||
bodystmt: bodystmt,
|
||
location: beginning.location.to(ending.location)
|
||
)
|
||
end
|
||
|
||
# MRHS represents the values that are being assigned on the right-hand side of
|
||
# a multiple assignment.
|
||
#
|
||
# values = first, second, third
|
||
#
|
||
class MRHS
|
||
# Array[untyped] the parts that are being assigned
|
||
attr_reader :parts
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(parts:, location:)
|
||
@parts = parts
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('mrhs')
|
||
|
||
q.breakable
|
||
q.group(2, '(', ')') { q.seplist(parts) { |part| q.pp(part) } }
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :mrhs, parts: parts, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_mrhs_new: () -> MRHS
|
||
def on_mrhs_new
|
||
MRHS.new(parts: [], location: Location.fixed(line: lineno, char: char_pos))
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_mrhs_add: (MRHS mrhs, untyped part) -> MRHS
|
||
def on_mrhs_add(mrhs, part)
|
||
if mrhs.is_a?(MRHSNewFromArgs)
|
||
MRHS.new(
|
||
parts: [*mrhs.arguments.parts, part],
|
||
location: mrhs.location.to(part.location)
|
||
)
|
||
elsif mrhs.parts.empty?
|
||
MRHS.new(parts: [part], location: mrhs.location)
|
||
else
|
||
MRHS.new(parts: mrhs.parts << part, loc: mrhs.location.to(part.location))
|
||
end
|
||
end
|
||
|
||
# MRHSAddStar represents using the splat operator to expand out a value on the
|
||
# right hand side of a multiple assignment.
|
||
#
|
||
# values = first, *rest
|
||
#
|
||
class MRHSAddStar
|
||
# [MRHS | MRHSNewFromArgs] the values before the splatted expression
|
||
attr_reader :mrhs
|
||
|
||
# [untyped] the splatted expression
|
||
attr_reader :star
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(mrhs:, star:, location:)
|
||
@mrhs = mrhs
|
||
@star = star
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('mrhs_add_star')
|
||
|
||
q.breakable
|
||
q.pp(mrhs)
|
||
|
||
q.breakable
|
||
q.pp(star)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :mrhs_add_star, mrhs: mrhs, star: star, loc: location }.to_json(
|
||
*opts
|
||
)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_mrhs_add_star: (
|
||
# (MRHS | MRHSNewFromArgs) mrhs,
|
||
# untyped star
|
||
# ) -> MRHSAddStar
|
||
def on_mrhs_add_star(mrhs, star)
|
||
beginning = find_token(Op, '*')
|
||
ending = star || beginning
|
||
|
||
MRHSAddStar.new(
|
||
mrhs: mrhs,
|
||
star: star,
|
||
location: beginning.location.to(ending.location)
|
||
)
|
||
end
|
||
|
||
# MRHSNewFromArgs represents the shorthand of a multiple assignment that
|
||
# allows you to assign values using just commas as opposed to assigning from
|
||
# an array.
|
||
#
|
||
# values = first, second, third
|
||
#
|
||
class MRHSNewFromArgs
|
||
# [Args] the arguments being used in the assignment
|
||
attr_reader :arguments
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(arguments:, location:)
|
||
@arguments = arguments
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('mrhs_new_from_args')
|
||
|
||
q.breakable
|
||
q.pp(arguments)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :mrhs_new_from_args, args: arguments, loc: location }.to_json(
|
||
*opts
|
||
)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_mrhs_new_from_args: (Args arguments) -> MRHSNewFromArgs
|
||
def on_mrhs_new_from_args(arguments)
|
||
MRHSNewFromArgs.new(arguments: arguments, location: arguments.location)
|
||
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
|
||
# [Args | ArgsAddBlock] the arguments passed to the next keyword
|
||
attr_reader :arguments
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(arguments:, location:)
|
||
@arguments = arguments
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('next')
|
||
|
||
q.breakable
|
||
q.pp(arguments)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :next, args: arguments, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_next: ((Args | ArgsAddBlock) arguments) -> Next
|
||
def on_next(arguments)
|
||
keyword = find_token(Kw, 'next')
|
||
|
||
location = keyword.location
|
||
location = location.to(arguments.location) unless arguments.is_a?(Args)
|
||
|
||
Next.new(arguments: arguments, location: location)
|
||
end
|
||
|
||
# def on_nl(value)
|
||
# value
|
||
# end
|
||
|
||
# def on_nokw_param(value)
|
||
# value
|
||
# end
|
||
|
||
# Op represents an operator literal in the source.
|
||
#
|
||
# 1 + 2
|
||
#
|
||
# In the example above, the Op node represents the + operator.
|
||
class Op
|
||
# [String] the operator
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('op')
|
||
|
||
q.breakable
|
||
q.pp(value)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :op, value: value, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_op: (String value) -> Op
|
||
def on_op(value)
|
||
node =
|
||
Op.new(
|
||
value: value,
|
||
location: Location.token(line: lineno, char: char_pos, size: value.size)
|
||
)
|
||
|
||
tokens << node
|
||
node
|
||
end
|
||
|
||
# OpAssign represents assigning a value to a variable or constant using an
|
||
# operator like += or ||=.
|
||
#
|
||
# variable += value
|
||
#
|
||
class OpAssign
|
||
# [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
|
||
|
||
# [untyped] the expression to be assigned
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(target:, operator:, value:, location:)
|
||
@target = target
|
||
@operator = operator
|
||
@value = value
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('opassign')
|
||
|
||
q.breakable
|
||
q.pp(target)
|
||
|
||
q.breakable
|
||
q.pp(operator)
|
||
|
||
q.breakable
|
||
q.pp(value)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :opassign,
|
||
target: target,
|
||
op: operator,
|
||
value: value,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_opassign: (
|
||
# (ARefField | ConstPathField | Field | TopConstField | VarField) target,
|
||
# Op operator,
|
||
# untyped value
|
||
# ) -> OpAssign
|
||
def on_opassign(target, operator, value)
|
||
OpAssign.new(
|
||
target: target,
|
||
operator: operator,
|
||
value: value,
|
||
location: target.location.to(value.location)
|
||
)
|
||
end
|
||
|
||
# def on_operator_ambiguous(value)
|
||
# value
|
||
# end
|
||
|
||
# Params represents defining parameters on a method or lambda.
|
||
#
|
||
# def method(param) end
|
||
#
|
||
class Params
|
||
# [Array[ Ident ]] any required parameters
|
||
attr_reader :requireds
|
||
|
||
# [Array[ [ Ident, untyped ] ]] any optional parameters and their default
|
||
# values
|
||
attr_reader :optionals
|
||
|
||
# [nil | ArgsForward | ExcessedComma | RestParam] the optional rest
|
||
# parameter
|
||
attr_reader :rest
|
||
|
||
# [Array[ Ident ]] any positional parameters that exist after a rest
|
||
# parameter
|
||
attr_reader :posts
|
||
|
||
# [Array[ [ Ident, nil | untyped ] ]] any keyword parameters and their
|
||
# optional default values
|
||
attr_reader :keywords
|
||
|
||
# [nil | :nil | KwRestParam] the optional keyword rest parameter
|
||
attr_reader :keyword_rest
|
||
|
||
# [nil | BlockArg] the optional block parameter
|
||
attr_reader :block
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(
|
||
requireds: [],
|
||
optionals: [],
|
||
rest: nil,
|
||
posts: [],
|
||
keywords: [],
|
||
keyword_rest: nil,
|
||
block: nil,
|
||
location:
|
||
)
|
||
@requireds = requireds
|
||
@optionals = optionals
|
||
@rest = rest
|
||
@posts = posts
|
||
@keywords = keywords
|
||
@keyword_rest = keyword_rest
|
||
@block = block
|
||
@location = location
|
||
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 pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('params')
|
||
|
||
if requireds.any?
|
||
q.breakable
|
||
q.group(2, '(', ')') { q.seplist(requireds) { |name| q.pp(name) } }
|
||
end
|
||
|
||
if optionals.any?
|
||
q.breakable
|
||
q.group(2, '(', ')') do
|
||
q.seplist(optionals) do |(name, default)|
|
||
q.pp(name)
|
||
q.text('=')
|
||
q.group(2) do
|
||
q.breakable('')
|
||
q.pp(default)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
if rest
|
||
q.breakable
|
||
q.pp(rest)
|
||
end
|
||
|
||
if posts.any?
|
||
q.breakable
|
||
q.group(2, '(', ')') { q.seplist(posts) { |value| q.pp(value) } }
|
||
end
|
||
|
||
if keywords.any?
|
||
q.breakable
|
||
q.group(2, '(', ')') do
|
||
q.seplist(keywords) do |(name, default)|
|
||
q.pp(name)
|
||
|
||
if default
|
||
q.text('=')
|
||
q.group(2) do
|
||
q.breakable('')
|
||
q.pp(default)
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
if keyword_rest
|
||
q.breakable
|
||
q.pp(keyword_rest)
|
||
end
|
||
|
||
if block
|
||
q.breakable
|
||
q.pp(block)
|
||
end
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :params,
|
||
reqs: requireds,
|
||
opts: optionals,
|
||
rest: rest,
|
||
posts: posts,
|
||
keywords: keywords,
|
||
kwrest: keyword_rest,
|
||
block: block,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_params: (
|
||
# (nil | Array[Ident]) requireds,
|
||
# (nil | Array[[Ident, untyped]]) optionals,
|
||
# (nil | ArgsForward | ExcessedComma | RestParam) rest,
|
||
# (nil | Array[Ident]) posts,
|
||
# (nil | Array[[Ident, nil | untyped]]) keywords,
|
||
# (nil | :nil | KwRestParam) keyword_rest,
|
||
# (nil | BlockArg) block
|
||
# ) -> Params
|
||
def on_params(
|
||
requireds,
|
||
optionals,
|
||
rest,
|
||
posts,
|
||
keywords,
|
||
keyword_rest,
|
||
block
|
||
)
|
||
parts = [
|
||
*requireds,
|
||
*optionals&.flatten(1),
|
||
rest,
|
||
*posts,
|
||
*keywords&.flat_map { |(key, value)| [key, value || nil] },
|
||
(keyword_rest if keyword_rest != :nil),
|
||
block
|
||
].compact
|
||
|
||
location =
|
||
if parts.any?
|
||
parts[0].location.to(parts[-1].location)
|
||
else
|
||
Location.fixed(line: lineno, char: char_pos)
|
||
end
|
||
|
||
Params.new(
|
||
requireds: requireds || [],
|
||
optionals: optionals || [],
|
||
rest: rest,
|
||
posts: posts || [],
|
||
keywords: keywords || [],
|
||
keyword_rest: keyword_rest,
|
||
block: block,
|
||
location: location
|
||
)
|
||
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
|
||
# [LParen] the left parenthesis that opened this statement
|
||
attr_reader :lparen
|
||
|
||
# [untyped] the expression inside the parentheses
|
||
attr_reader :contents
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(lparen:, contents:, location:)
|
||
@lparen = lparen
|
||
@contents = contents
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('paren')
|
||
|
||
q.breakable
|
||
q.pp(contents)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :paren, lparen: lparen, cnts: contents, loc: location }.to_json(
|
||
*opts
|
||
)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_paren: (untyped contents) -> Paren
|
||
def on_paren(contents)
|
||
lparen = find_token(LParen)
|
||
rparen = find_token(RParen)
|
||
|
||
if contents && contents.is_a?(Params)
|
||
location = contents.location
|
||
location =
|
||
Location.new(
|
||
start_line: location.start_line,
|
||
start_char: find_next_statement_start(lparen.location.end_char),
|
||
end_line: location.end_line,
|
||
end_char: rparen.location.start_char
|
||
)
|
||
|
||
contents =
|
||
Params.new(
|
||
requireds: contents.requireds,
|
||
optionals: contents.optionals,
|
||
rest: contents.rest,
|
||
posts: contents.posts,
|
||
keywords: contents.keywords,
|
||
keyword_rest: contents.keyword_rest,
|
||
block: contents.block,
|
||
location: location
|
||
)
|
||
end
|
||
|
||
Paren.new(
|
||
lparen: lparen,
|
||
contents: contents,
|
||
location: lparen.location.to(rparen.location)
|
||
)
|
||
end
|
||
|
||
# If we encounter a parse error, just immediately bail out so that our runner
|
||
# can catch it.
|
||
def on_parse_error(error, *)
|
||
raise ParseError.new(error, lineno, column)
|
||
end
|
||
alias on_alias_error on_parse_error
|
||
alias on_assign_error on_parse_error
|
||
alias on_class_name_error on_parse_error
|
||
alias on_param_error on_parse_error
|
||
|
||
# Period represents the use of the +.+ operator. It is usually found in method
|
||
# calls.
|
||
class Period
|
||
# [String] the period
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('period')
|
||
|
||
q.breakable
|
||
q.pp(value)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :period, value: value, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_period: (String value) -> Period
|
||
def on_period(value)
|
||
Period.new(
|
||
value: value,
|
||
location: Location.token(line: lineno, char: char_pos, size: value.size)
|
||
)
|
||
end
|
||
|
||
# Program represents the overall syntax tree.
|
||
class Program
|
||
# [Statements] the top-level expressions of the program
|
||
attr_reader :statements
|
||
|
||
# [Array[ Comment | EmbDoc ]] the comments inside the program
|
||
attr_reader :comments
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(statements:, comments:, location:)
|
||
@statements = statements
|
||
@comments = comments
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('program')
|
||
|
||
q.breakable
|
||
q.pp(statements)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :program,
|
||
stmts: statements,
|
||
comments: comments,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_program: (Statements statements) -> Program
|
||
def on_program(statements)
|
||
location =
|
||
Location.new(
|
||
start_line: 1,
|
||
start_char: 0,
|
||
end_line: lines.length,
|
||
end_char: source.length
|
||
)
|
||
|
||
statements.body << @__end__ if @__end__
|
||
statements.bind(0, source.length)
|
||
|
||
Program.new(statements: statements, comments: @comments, location: location)
|
||
end
|
||
|
||
# QSymbols represents a symbol literal array without interpolation.
|
||
#
|
||
# %i[one two three]
|
||
#
|
||
class QSymbols
|
||
# [Array[ TStringContent ]] the elements of the array
|
||
attr_reader :elements
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(elements:, location:)
|
||
@elements = elements
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('qsymbols')
|
||
|
||
q.breakable
|
||
q.group(2, '(', ')') { q.seplist(elements) { |element| q.pp(element) } }
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :qsymbols, elems: elements, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_qsymbols_add: (QSymbols qsymbols, TStringContent element) -> QSymbols
|
||
def on_qsymbols_add(qsymbols, element)
|
||
QSymbols.new(
|
||
elements: qsymbols.elements << element,
|
||
location: qsymbols.location.to(element.location)
|
||
)
|
||
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
|
||
# [String] the beginning of the array literal
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_qsymbols_beg: (String value) -> QSymbolsBeg
|
||
def on_qsymbols_beg(value)
|
||
node =
|
||
QSymbolsBeg.new(
|
||
value: value,
|
||
location: Location.token(line: lineno, char: char_pos, size: value.size)
|
||
)
|
||
|
||
tokens << node
|
||
node
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_qsymbols_new: () -> QSymbols
|
||
def on_qsymbols_new
|
||
qsymbols_beg = find_token(QSymbolsBeg)
|
||
|
||
QSymbols.new(elements: [], location: qsymbols_beg.location)
|
||
end
|
||
|
||
# QWords represents a string literal array without interpolation.
|
||
#
|
||
# %w[one two three]
|
||
#
|
||
class QWords
|
||
# [Array[ TStringContent ]] the elements of the array
|
||
attr_reader :elements
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(elements:, location:)
|
||
@elements = elements
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('qwords')
|
||
|
||
q.breakable
|
||
q.group(2, '(', ')') { q.seplist(elements) { |element| q.pp(element) } }
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :qwords, elems: elements, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_qwords_add: (QWords qwords, TStringContent element) -> QWords
|
||
def on_qwords_add(qwords, element)
|
||
QWords.new(
|
||
elements: qwords.elements << element,
|
||
location: qwords.location.to(element.location)
|
||
)
|
||
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
|
||
# [String] the beginning of the array literal
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_qwords_beg: (String value) -> QWordsBeg
|
||
def on_qwords_beg(value)
|
||
node =
|
||
QWordsBeg.new(
|
||
value: value,
|
||
location: Location.token(line: lineno, char: char_pos, size: value.size)
|
||
)
|
||
|
||
tokens << node
|
||
node
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_qwords_new: () -> QWords
|
||
def on_qwords_new
|
||
qwords_beg = find_token(QWordsBeg)
|
||
|
||
QWords.new(elements: [], location: qwords_beg.location)
|
||
end
|
||
|
||
# RationalLiteral represents the use of a rational number literal.
|
||
#
|
||
# 1r
|
||
#
|
||
class RationalLiteral
|
||
# [String] the rational number literal
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('rational')
|
||
|
||
q.breakable
|
||
q.pp(value)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :rational, value: value, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_rational: (String value) -> RationalLiteral
|
||
def on_rational(value)
|
||
node =
|
||
RationalLiteral.new(
|
||
value: value,
|
||
location: Location.token(line: lineno, char: char_pos, size: value.size)
|
||
)
|
||
|
||
tokens << node
|
||
node
|
||
end
|
||
|
||
# RBrace represents the use of a right brace, i.e., +++.
|
||
class RBrace
|
||
# [String] the right brace
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_rbrace: (String value) -> RBrace
|
||
def on_rbrace(value)
|
||
node =
|
||
RBrace.new(
|
||
value: value,
|
||
location: Location.token(line: lineno, char: char_pos, size: value.size)
|
||
)
|
||
|
||
tokens << node
|
||
node
|
||
end
|
||
|
||
# RBracket represents the use of a right bracket, i.e., +]+.
|
||
class RBracket
|
||
# [String] the right bracket
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_rbracket: (String value) -> RBracket
|
||
def on_rbracket(value)
|
||
node =
|
||
RBracket.new(
|
||
value: value,
|
||
location: Location.token(line: lineno, char: char_pos, size: value.size)
|
||
)
|
||
|
||
tokens << node
|
||
node
|
||
end
|
||
|
||
# Redo represents the use of the +redo+ keyword.
|
||
#
|
||
# redo
|
||
#
|
||
class Redo
|
||
# [String] the value of the keyword
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('redo')
|
||
|
||
q.breakable
|
||
q.pp(value)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :redo, value: value, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_redo: () -> Redo
|
||
def on_redo
|
||
keyword = find_token(Kw, 'redo')
|
||
|
||
Redo.new(value: keyword.value, location: keyword.location)
|
||
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
|
||
# [String] the opening of the regular expression
|
||
attr_reader :beginning
|
||
|
||
# [Array[ StringDVar | StringEmbExpr | TStringContent ]] the parts of the
|
||
# regular expression
|
||
attr_reader :parts
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(beginning:, parts:, location:)
|
||
@beginning = beginning
|
||
@parts = parts
|
||
@location = location
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_regexp_add: (
|
||
# RegexpContent regexp_content,
|
||
# (StringDVar | StringEmbExpr | TStringContent) part
|
||
# ) -> RegexpContent
|
||
def on_regexp_add(regexp_content, part)
|
||
RegexpContent.new(
|
||
beginning: regexp_content.beginning,
|
||
parts: regexp_content.parts << part,
|
||
location: regexp_content.location.to(part.location)
|
||
)
|
||
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
|
||
# [String] the beginning of the regular expression
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_regexp_beg: (String value) -> RegexpBeg
|
||
def on_regexp_beg(value)
|
||
node =
|
||
RegexpBeg.new(
|
||
value: value,
|
||
location: Location.token(line: lineno, char: char_pos, size: value.size)
|
||
)
|
||
|
||
tokens << node
|
||
node
|
||
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
|
||
# [String] the end of the regular expression
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_regexp_end: (String value) -> RegexpEnd
|
||
def on_regexp_end(value)
|
||
RegexpEnd.new(
|
||
value: value,
|
||
location: Location.token(line: lineno, char: char_pos, size: value.size)
|
||
)
|
||
end
|
||
|
||
# RegexpLiteral represents a regular expression literal.
|
||
#
|
||
# /.+/
|
||
#
|
||
class RegexpLiteral
|
||
# [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
|
||
|
||
# [Locatione] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(beginning:, ending:, parts:, location:)
|
||
@beginning = beginning
|
||
@ending = ending
|
||
@parts = parts
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('regexp_literal')
|
||
|
||
q.breakable
|
||
q.group(2, '(', ')') { q.seplist(parts) { |part| q.pp(part) } }
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :regexp_literal,
|
||
beging: beginning,
|
||
ending: ending,
|
||
parts: parts,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_regexp_literal: (
|
||
# RegexpContent regexp_content,
|
||
# RegexpEnd ending
|
||
# ) -> RegexpLiteral
|
||
def on_regexp_literal(regexp_content, ending)
|
||
RegexpLiteral.new(
|
||
beginning: regexp_content.beginning,
|
||
ending: ending.value,
|
||
parts: regexp_content.parts,
|
||
location: regexp_content.location.to(ending.location)
|
||
)
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_regexp_new: () -> RegexpContent
|
||
def on_regexp_new
|
||
regexp_beg = find_token(RegexpBeg)
|
||
|
||
RegexpContent.new(
|
||
beginning: regexp_beg.value,
|
||
parts: [],
|
||
location: regexp_beg.location
|
||
)
|
||
end
|
||
|
||
# RescueEx represents the list of exceptions being rescued in a rescue clause.
|
||
#
|
||
# begin
|
||
# rescue Exception => exception
|
||
# end
|
||
#
|
||
class RescueEx
|
||
# [untyped] the list of exceptions being rescued
|
||
attr_reader :exceptions
|
||
|
||
# [nil | Field | VarField] the expression being used to capture the raised
|
||
# exception
|
||
attr_reader :variable
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(exceptions:, variable:, location:)
|
||
@exceptions = exceptions
|
||
@variable = variable
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('rescue_ex')
|
||
|
||
q.breakable
|
||
q.pp(exceptions)
|
||
|
||
q.breakable
|
||
q.pp(variable)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :rescue_ex,
|
||
extns: exceptions,
|
||
var: variable,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# Rescue represents the use of the rescue keyword inside of a BodyStmt node.
|
||
#
|
||
# begin
|
||
# rescue
|
||
# end
|
||
#
|
||
class Rescue
|
||
# [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
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(exception:, statements:, consequent:, location:)
|
||
@exception = exception
|
||
@statements = statements
|
||
@consequent = consequent
|
||
@location = location
|
||
end
|
||
|
||
def bind_end(end_char)
|
||
@location =
|
||
Location.new(
|
||
start_line: location.start_line,
|
||
start_char: location.start_char,
|
||
end_line: location.end_line,
|
||
end_char: end_char
|
||
)
|
||
|
||
if consequent
|
||
consequent.bind_end(end_char)
|
||
statements.bind_end(consequent.location.start_char)
|
||
else
|
||
statements.bind_end(end_char)
|
||
end
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('rescue')
|
||
|
||
if exception
|
||
q.breakable
|
||
q.pp(exception)
|
||
end
|
||
|
||
q.breakable
|
||
q.pp(statements)
|
||
|
||
if consequent
|
||
q.breakable
|
||
q.pp(consequent)
|
||
end
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :rescue,
|
||
extn: exception,
|
||
stmts: statements,
|
||
cons: consequent,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_rescue: (
|
||
# (nil | [untyped] | MRHS | MRHSAddStar) exceptions,
|
||
# (nil | Field | VarField) variable,
|
||
# Statements statements,
|
||
# (nil | Rescue) consequent
|
||
# ) -> Rescue
|
||
def on_rescue(exceptions, variable, statements, consequent)
|
||
keyword = find_token(Kw, 'rescue')
|
||
exceptions = exceptions[0] if exceptions.is_a?(Array)
|
||
|
||
last_node = variable || exceptions || keyword
|
||
statements.bind(
|
||
find_next_statement_start(last_node.location.end_char),
|
||
char_pos
|
||
)
|
||
|
||
# We add an additional inner node here that ripper doesn't provide so that
|
||
# we have a nice place to attach inline comments. But we only need it if we
|
||
# have an exception or a variable that we're rescuing.
|
||
rescue_ex =
|
||
if exceptions || variable
|
||
RescueEx.new(
|
||
exceptions: exceptions,
|
||
variable: variable,
|
||
location:
|
||
Location.new(
|
||
start_line: keyword.location.start_line,
|
||
start_char: keyword.location.end_char + 1,
|
||
end_line: last_node.location.end_line,
|
||
end_char: last_node.location.end_char
|
||
)
|
||
)
|
||
end
|
||
|
||
Rescue.new(
|
||
exception: rescue_ex,
|
||
statements: statements,
|
||
consequent: consequent,
|
||
location:
|
||
Location.new(
|
||
start_line: keyword.location.start_line,
|
||
start_char: keyword.location.start_char,
|
||
end_line: lineno,
|
||
end_char: char_pos
|
||
)
|
||
)
|
||
end
|
||
|
||
# RescueMod represents the use of the modifier form of a +rescue+ clause.
|
||
#
|
||
# expression rescue value
|
||
#
|
||
class RescueMod
|
||
# [untyped] the expression to execute
|
||
attr_reader :statement
|
||
|
||
# [untyped] the value to use if the executed expression raises an error
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(statement:, value:, location:)
|
||
@statement = statement
|
||
@value = value
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('rescue_mod')
|
||
|
||
q.breakable
|
||
q.pp(statement)
|
||
|
||
q.breakable
|
||
q.pp(value)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :rescue_mod,
|
||
stmt: statement,
|
||
value: value,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_rescue_mod: (untyped statement, untyped value) -> RescueMod
|
||
def on_rescue_mod(statement, value)
|
||
find_token(Kw, 'rescue')
|
||
|
||
RescueMod.new(
|
||
statement: statement,
|
||
value: value,
|
||
location: statement.location.to(value.location)
|
||
)
|
||
end
|
||
|
||
# RestParam represents defining a parameter in a method definition that
|
||
# accepts all remaining positional parameters.
|
||
#
|
||
# def method(*rest) end
|
||
#
|
||
class RestParam
|
||
# [nil | Ident] the name of the parameter
|
||
attr_reader :name
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(name:, location:)
|
||
@name = name
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('rest_param')
|
||
|
||
q.breakable
|
||
q.pp(name)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :rest_param, name: name, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_rest_param: ((nil | Ident) name) -> RestParam
|
||
def on_rest_param(name)
|
||
location = find_token(Op, '*').location
|
||
location = location.to(name.location) if name
|
||
|
||
RestParam.new(name: name, location: location)
|
||
end
|
||
|
||
# Retry represents the use of the +retry+ keyword.
|
||
#
|
||
# retry
|
||
#
|
||
class Retry
|
||
# [String] the value of the keyword
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('retry')
|
||
|
||
q.breakable
|
||
q.pp(value)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :retry, value: value, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_retry: () -> Retry
|
||
def on_retry
|
||
keyword = find_token(Kw, 'retry')
|
||
|
||
Retry.new(value: keyword.value, location: keyword.location)
|
||
end
|
||
|
||
# Return represents using the +return+ keyword with arguments.
|
||
#
|
||
# return value
|
||
#
|
||
class Return
|
||
# [Args | ArgsAddBlock] the arguments being passed to the keyword
|
||
attr_reader :arguments
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(arguments:, location:)
|
||
@arguments = arguments
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('return')
|
||
|
||
q.breakable
|
||
q.pp(arguments)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :return, args: arguments, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_return: ((Args | ArgsAddBlock) arguments) -> Return
|
||
def on_return(arguments)
|
||
keyword = find_token(Kw, 'return')
|
||
|
||
Return.new(
|
||
arguments: arguments,
|
||
location: keyword.location.to(arguments.location)
|
||
)
|
||
end
|
||
|
||
# Return0 represents the bare +return+ keyword with no arguments.
|
||
#
|
||
# return
|
||
#
|
||
class Return0
|
||
# [String] the value of the keyword
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('return0')
|
||
|
||
q.breakable
|
||
q.pp(value)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :return0, value: value, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_return0: () -> Return0
|
||
def on_return0
|
||
keyword = find_token(Kw, 'return')
|
||
|
||
Return0.new(value: keyword.value, location: keyword.location)
|
||
end
|
||
|
||
# RParen represents the use of a right parenthesis, i.e., +)+.
|
||
class RParen
|
||
# [String] the parenthesis
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_rparen: (String value) -> RParen
|
||
def on_rparen(value)
|
||
node =
|
||
RParen.new(
|
||
value: value,
|
||
location: Location.token(line: lineno, char: char_pos, size: value.size)
|
||
)
|
||
|
||
tokens << node
|
||
node
|
||
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
|
||
# [untyped] the target of the singleton class to enter
|
||
attr_reader :target
|
||
|
||
# [BodyStmt] the expressions to be executed
|
||
attr_reader :bodystmt
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(target:, bodystmt:, location:)
|
||
@target = target
|
||
@bodystmt = bodystmt
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('sclass')
|
||
|
||
q.breakable
|
||
q.pp(target)
|
||
|
||
q.breakable
|
||
q.pp(bodystmt)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :sclass,
|
||
target: target,
|
||
bodystmt: bodystmt,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_sclass: (untyped target, BodyStmt bodystmt) -> SClass
|
||
def on_sclass(target, bodystmt)
|
||
beginning = find_token(Kw, 'class')
|
||
ending = find_token(Kw, 'end')
|
||
|
||
bodystmt.bind(
|
||
find_next_statement_start(target.location.end_char),
|
||
ending.location.start_char
|
||
)
|
||
|
||
SClass.new(
|
||
target: target,
|
||
bodystmt: bodystmt,
|
||
location: beginning.location.to(ending.location)
|
||
)
|
||
end
|
||
|
||
# def on_semicolon(value)
|
||
# value
|
||
# end
|
||
|
||
# def on_sp(value)
|
||
# value
|
||
# end
|
||
|
||
# stmts_add is a parser event that represents a single statement inside a
|
||
# list of statements within any lexical block. It accepts as arguments the
|
||
# parent stmts node as well as an stmt which can be any expression in
|
||
# Ruby.
|
||
def on_stmts_add(statements, statement)
|
||
statements << statement
|
||
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
|
||
# [SyntaxTree] the parser that created this node
|
||
attr_reader :parser
|
||
|
||
# [Array[ untyped ]] the list of expressions contained within this node
|
||
attr_reader :body
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(parser:, body:, location:)
|
||
@parser = parser
|
||
@body = body
|
||
@location = location
|
||
end
|
||
|
||
def bind(start_char, end_char)
|
||
@location =
|
||
Location.new(
|
||
start_line: location.start_line,
|
||
start_char: start_char,
|
||
end_line: location.end_line,
|
||
end_char: end_char
|
||
)
|
||
|
||
if body[0].is_a?(VoidStmt)
|
||
location = body[0].location
|
||
location =
|
||
Location.new(
|
||
start_line: location.start_line,
|
||
start_char: start_char,
|
||
end_line: location.end_line,
|
||
end_char: start_char
|
||
)
|
||
|
||
body[0] = VoidStmt.new(location: location)
|
||
end
|
||
|
||
attach_comments(start_char, end_char)
|
||
end
|
||
|
||
def bind_end(end_char)
|
||
@location =
|
||
Location.new(
|
||
start_line: location.start_line,
|
||
start_char: location.start_char,
|
||
end_line: location.end_line,
|
||
end_char: end_char
|
||
)
|
||
end
|
||
|
||
def <<(statement)
|
||
@location =
|
||
body.any? ? location.to(statement.location) : statement.location
|
||
|
||
body << statement
|
||
self
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('statements')
|
||
|
||
q.breakable
|
||
q.seplist(body) { |statement| q.pp(statement) }
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :statements, body: body, loc: location }.to_json(*opts)
|
||
end
|
||
|
||
private
|
||
|
||
def attach_comments(start_char, end_char)
|
||
attachable =
|
||
parser.comments.select do |comment|
|
||
!comment.inline? && start_char <= comment.location.start_char &&
|
||
end_char >= comment.location.end_char &&
|
||
!comment.value.include?('prettier-ignore')
|
||
end
|
||
|
||
return if attachable.empty?
|
||
|
||
parser.comments -= attachable
|
||
@body = (body + attachable).sort_by! { |node| node.location.start_char }
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_stmts_new: () -> Statements
|
||
def on_stmts_new
|
||
Statements.new(
|
||
parser: self,
|
||
body: [],
|
||
location: Location.fixed(line: lineno, char: char_pos)
|
||
)
|
||
end
|
||
|
||
# StringContent represents the contents of a string-like value.
|
||
#
|
||
# "string"
|
||
#
|
||
class StringContent
|
||
# [Array[ StringEmbExpr | StringDVar | TStringContent ]] the parts of the
|
||
# string
|
||
attr_reader :parts
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(parts:, location:)
|
||
@parts = parts
|
||
@location = location
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_string_add: (
|
||
# String string,
|
||
# (StringEmbExpr | StringDVar | TStringContent) part
|
||
# ) -> StringContent
|
||
def on_string_add(string, part)
|
||
location =
|
||
string.parts.any? ? string.location.to(part.location) : part.location
|
||
|
||
StringContent.new(parts: string.parts << part, location: location)
|
||
end
|
||
|
||
# StringConcat represents concatenating two strings together using a backward
|
||
# slash.
|
||
#
|
||
# "first" \
|
||
# "second"
|
||
#
|
||
class StringConcat
|
||
# [StringConcat | StringLiteral] the left side of the concatenation
|
||
attr_reader :left
|
||
|
||
# [StringLiteral] the right side of the concatenation
|
||
attr_reader :right
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(left:, right:, location:)
|
||
@left = left
|
||
@right = right
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('string_concat')
|
||
|
||
q.breakable
|
||
q.pp(left)
|
||
|
||
q.breakable
|
||
q.pp(right)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :string_concat, left: left, right: right, loc: location }.to_json(
|
||
*opts
|
||
)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_string_concat: (
|
||
# (StringConcat | StringLiteral) left,
|
||
# StringLiteral right
|
||
# ) -> StringConcat
|
||
def on_string_concat(left, right)
|
||
StringConcat.new(
|
||
left: left,
|
||
right: right,
|
||
location: left.location.to(right.location)
|
||
)
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_string_content: () -> StringContent
|
||
def on_string_content
|
||
StringContent.new(
|
||
parts: [],
|
||
location: Location.fixed(line: lineno, char: char_pos)
|
||
)
|
||
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
|
||
# [Backref | VarRef] the variable being interpolated
|
||
attr_reader :variable
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(variable:, location:)
|
||
@variable = variable
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('string_dvar')
|
||
|
||
q.breakable
|
||
q.pp(variable)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :string_dvar, var: variable, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_string_dvar: ((Backref | VarRef) variable) -> StringDVar
|
||
def on_string_dvar(variable)
|
||
embvar = find_token(EmbVar)
|
||
|
||
StringDVar.new(
|
||
variable: variable,
|
||
location: embvar.location.to(variable.location)
|
||
)
|
||
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
|
||
# [Statements] the expressions to be interpolated
|
||
attr_reader :statements
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(statements:, location:)
|
||
@statements = statements
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('string_embexpr')
|
||
|
||
q.breakable
|
||
q.pp(statements)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :string_embexpr, stmts: statements, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_string_embexpr: (Statements statements) -> StringEmbExpr
|
||
def on_string_embexpr(statements)
|
||
embexpr_beg = find_token(EmbExprBeg)
|
||
embexpr_end = find_token(EmbExprEnd)
|
||
|
||
statements.bind(
|
||
embexpr_beg.location.end_char,
|
||
embexpr_end.location.start_char
|
||
)
|
||
|
||
StringEmbExpr.new(
|
||
statements: statements,
|
||
location: embexpr_beg.location.to(embexpr_end.location)
|
||
)
|
||
end
|
||
|
||
# StringLiteral represents a string literal.
|
||
#
|
||
# "string"
|
||
#
|
||
class StringLiteral
|
||
# [Array[ StringEmbExpr | StringDVar | TStringContent ]] the parts of the
|
||
# string literal
|
||
attr_reader :parts
|
||
|
||
# [String] which quote was used by the string literal
|
||
attr_reader :quote
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(parts:, quote:, location:)
|
||
@parts = parts
|
||
@quote = quote
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('string_literal')
|
||
|
||
q.breakable
|
||
q.group(2, '(', ')') { q.seplist(parts) { |part| q.pp(part) } }
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :string_literal,
|
||
parts: parts,
|
||
quote: quote,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_string_literal: (String string) -> Heredoc | StringLiteral
|
||
def on_string_literal(string)
|
||
heredoc = @heredocs[-1]
|
||
|
||
if heredoc && heredoc.ending
|
||
heredoc = @heredocs.pop
|
||
|
||
Heredoc.new(
|
||
beginning: heredoc.beginning,
|
||
ending: heredoc.ending,
|
||
parts: string.parts,
|
||
location: heredoc.location
|
||
)
|
||
else
|
||
tstring_beg = find_token(TStringBeg)
|
||
tstring_end = find_token(TStringEnd)
|
||
|
||
StringLiteral.new(
|
||
parts: string.parts,
|
||
quote: tstring_beg.value,
|
||
location: tstring_beg.location.to(tstring_end.location)
|
||
)
|
||
end
|
||
end
|
||
|
||
# Super represents using the +super+ keyword with arguments. It can optionally
|
||
# use parentheses.
|
||
#
|
||
# super(value)
|
||
#
|
||
class Super
|
||
# [ArgParen | Args | ArgsAddBlock] the arguments to the keyword
|
||
attr_reader :arguments
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(arguments:, location:)
|
||
@arguments = arguments
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('super')
|
||
|
||
q.breakable
|
||
q.pp(arguments)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :super, args: arguments, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_super: ((ArgParen | Args | ArgsAddBlock) arguments) -> Super
|
||
def on_super(arguments)
|
||
keyword = find_token(Kw, 'super')
|
||
|
||
Super.new(
|
||
arguments: arguments,
|
||
location: keyword.location.to(arguments.location)
|
||
)
|
||
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
|
||
# [String] the beginning of the symbol
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
end
|
||
|
||
# symbeg is a token that represents the beginning of a symbol literal.
|
||
# In most cases it will contain just ":" as in the value, but if its a dynamic
|
||
# symbol being defined it will contain ":'" or ":\"".
|
||
def on_symbeg(value)
|
||
node =
|
||
SymBeg.new(
|
||
value: value,
|
||
location: Location.token(line: lineno, char: char_pos, size: value.size)
|
||
)
|
||
|
||
tokens << node
|
||
node
|
||
end
|
||
|
||
# SymbolContent represents symbol contents and is always the child of a
|
||
# SymbolLiteral node.
|
||
#
|
||
# :symbol
|
||
#
|
||
class SymbolContent
|
||
# [Backtick | Const | CVar | GVar | Ident | IVar | Kw | Op] the value of the
|
||
# symbol
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_symbol: (
|
||
# (Backtick | Const | CVar | GVar | Ident | IVar | Kw | Op) value
|
||
# ) -> SymbolContent
|
||
def on_symbol(value)
|
||
tokens.pop
|
||
|
||
SymbolContent.new(value: value, location: value.location)
|
||
end
|
||
|
||
# SymbolLiteral represents a symbol in the system with no interpolation
|
||
# (as opposed to a DynaSymbol which has interpolation).
|
||
#
|
||
# :symbol
|
||
#
|
||
class SymbolLiteral
|
||
# [Backtick | Const | CVar | GVar | Ident | IVar | Kw | Op] the value of the
|
||
# symbol
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('symbol_literal')
|
||
|
||
q.breakable
|
||
q.pp(value)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :symbol_literal, value: value, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_symbol_literal: (
|
||
# (
|
||
# Backtick | Const | CVar | GVar | Ident |
|
||
# IVar | Kw | Op | SymbolContent
|
||
# ) value
|
||
# ) -> SymbolLiteral
|
||
def on_symbol_literal(value)
|
||
if tokens[-1] == value
|
||
SymbolLiteral.new(value: tokens.pop, location: value.location)
|
||
else
|
||
symbeg = find_token(SymBeg)
|
||
|
||
SymbolLiteral.new(
|
||
value: value.value,
|
||
location: symbeg.location.to(value.location)
|
||
)
|
||
end
|
||
end
|
||
|
||
# Symbols represents a symbol array literal with interpolation.
|
||
#
|
||
# %I[one two three]
|
||
#
|
||
class Symbols
|
||
# [Array[ Word ]] the words in the symbol array literal
|
||
attr_reader :elements
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(elements:, location:)
|
||
@elements = elements
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('symbols')
|
||
|
||
q.breakable
|
||
q.group(2, '(', ')') { q.seplist(elements) { |element| q.pp(element) } }
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :symbols, elems: elements, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_symbols_add: (Symbols symbols, Word word) -> Symbols
|
||
def on_symbols_add(symbols, word)
|
||
Symbols.new(
|
||
elements: symbols.elements << word,
|
||
location: symbols.location.to(word.location)
|
||
)
|
||
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
|
||
# [String] the beginning of the symbol literal array
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_symbols_beg: (String value) -> SymbolsBeg
|
||
def on_symbols_beg(value)
|
||
node =
|
||
SymbolsBeg.new(
|
||
value: value,
|
||
location: Location.token(line: lineno, char: char_pos, size: value.size)
|
||
)
|
||
|
||
tokens << node
|
||
node
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_symbols_new: () -> Symbols
|
||
def on_symbols_new
|
||
symbols_beg = find_token(SymbolsBeg)
|
||
|
||
Symbols.new(elements: [], location: symbols_beg.location)
|
||
end
|
||
|
||
# TLambda represents the beginning of a lambda literal.
|
||
#
|
||
# -> { value }
|
||
#
|
||
# In the example above the TLambda represents the +->+ operator.
|
||
class TLambda
|
||
# [String] the beginning of the lambda literal
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_tlambda: (String value) -> TLambda
|
||
def on_tlambda(value)
|
||
node =
|
||
TLambda.new(
|
||
value: value,
|
||
location: Location.token(line: lineno, char: char_pos, size: value.size)
|
||
)
|
||
|
||
tokens << node
|
||
node
|
||
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
|
||
# [String] the beginning of the body of the lambda literal
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_tlambeg: (String value) -> TLamBeg
|
||
def on_tlambeg(value)
|
||
node =
|
||
TLamBeg.new(
|
||
value: value,
|
||
location: Location.token(line: lineno, char: char_pos, size: value.size)
|
||
)
|
||
|
||
tokens << node
|
||
node
|
||
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
|
||
# [Const] the constant being assigned
|
||
attr_reader :constant
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(constant:, location:)
|
||
@constant = constant
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('top_const_field')
|
||
|
||
q.breakable
|
||
q.pp(constant)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :top_const_field, constant: constant, loc: location }.to_json(
|
||
*opts
|
||
)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_top_const_field: (Const constant) -> TopConstRef
|
||
def on_top_const_field(constant)
|
||
operator = find_colon2_before(constant)
|
||
|
||
TopConstField.new(
|
||
constant: constant,
|
||
location: operator.location.to(constant.location)
|
||
)
|
||
end
|
||
|
||
# TopConstRef is very similar to TopConstField except that it is not involved
|
||
# in an assignment.
|
||
#
|
||
# ::Constant
|
||
#
|
||
class TopConstRef
|
||
# [Const] the constant being referenced
|
||
attr_reader :constant
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(constant:, location:)
|
||
@constant = constant
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('top_const_ref')
|
||
|
||
q.breakable
|
||
q.pp(constant)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :top_const_ref, constant: constant, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_top_const_ref: (Const constant) -> TopConstRef
|
||
def on_top_const_ref(constant)
|
||
operator = find_colon2_before(constant)
|
||
|
||
TopConstRef.new(
|
||
constant: constant,
|
||
location: operator.location.to(constant.location)
|
||
)
|
||
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
|
||
# [String] the beginning of the string
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_tstring_beg: (String value) -> TStringBeg
|
||
def on_tstring_beg(value)
|
||
node =
|
||
TStringBeg.new(
|
||
value: value,
|
||
location: Location.token(line: lineno, char: char_pos, size: value.size)
|
||
)
|
||
|
||
tokens << node
|
||
node
|
||
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
|
||
# [String] the content of the string
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('tstring_content')
|
||
|
||
q.breakable
|
||
q.pp(value)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :tstring_content,
|
||
value: value.force_encoding('UTF-8'),
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_tstring_content: (String value) -> TStringContent
|
||
def on_tstring_content(value)
|
||
TStringContent.new(
|
||
value: value,
|
||
location: Location.token(line: lineno, char: char_pos, size: value.size)
|
||
)
|
||
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
|
||
# [String] the end of the string
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_tstring_end: (String value) -> TStringEnd
|
||
def on_tstring_end(value)
|
||
node =
|
||
TStringEnd.new(
|
||
value: value,
|
||
location: Location.token(line: lineno, char: char_pos, size: value.size)
|
||
)
|
||
|
||
tokens << node
|
||
node
|
||
end
|
||
|
||
# Not represents the unary +not+ method being called on an expression.
|
||
#
|
||
# not value
|
||
#
|
||
class Not
|
||
# [untyped] the statement on which to operate
|
||
attr_reader :statement
|
||
|
||
# [boolean] whether or not parentheses were used
|
||
attr_reader :parentheses
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(statement:, parentheses:, location:)
|
||
@statement = statement
|
||
@parentheses = parentheses
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('not')
|
||
|
||
q.breakable
|
||
q.pp(statement)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :not,
|
||
value: statement,
|
||
paren: parentheses,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# Unary represents a unary method being called on an expression, as in +!+ or
|
||
# +~+.
|
||
#
|
||
# !value
|
||
#
|
||
class Unary
|
||
# [String] the operator being used
|
||
attr_reader :operator
|
||
|
||
# [untyped] the statement on which to operate
|
||
attr_reader :statement
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(operator:, statement:, location:)
|
||
@operator = operator
|
||
@statement = statement
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('unary')
|
||
|
||
q.breakable
|
||
q.pp(operator)
|
||
|
||
q.breakable
|
||
q.pp(statement)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :unary, op: operator, value: statement, loc: location }.to_json(
|
||
*opts
|
||
)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_unary: (:not operator, untyped statement) -> Not
|
||
# | (Symbol operator, untyped statement) -> Unary
|
||
def on_unary(operator, statement)
|
||
if operator == :not
|
||
# We have somewhat special handling of the not operator since if it has
|
||
# parentheses they don't get reported as a paren node for some reason.
|
||
|
||
beginning = find_token(Kw, 'not')
|
||
ending = statement
|
||
|
||
range = beginning.location.end_char...statement.location.start_char
|
||
paren = source[range].include?('(')
|
||
|
||
if paren
|
||
find_token(LParen)
|
||
ending = find_token(RParen)
|
||
end
|
||
|
||
Not.new(
|
||
statement: statement,
|
||
parentheses: paren,
|
||
location: beginning.location.to(ending.location)
|
||
)
|
||
else
|
||
# Special case instead of using find_token here. It turns out that
|
||
# if you have a range that goes from a negative number to a negative
|
||
# number then you can end up with a .. or a ... that's higher in the
|
||
# stack. So we need to explicitly disallow those operators.
|
||
index =
|
||
tokens.rindex do |token|
|
||
token.is_a?(Op) &&
|
||
token.location.start_char < statement.location.start_char &&
|
||
!%w[.. ...].include?(token.value)
|
||
end
|
||
|
||
beginning = tokens.delete_at(index)
|
||
|
||
Unary.new(
|
||
operator: operator[0], # :+@ -> "+"
|
||
statement: statement,
|
||
location: beginning.location.to(statement.location)
|
||
)
|
||
end
|
||
end
|
||
|
||
# Undef represents the use of the +undef+ keyword.
|
||
#
|
||
# undef method
|
||
#
|
||
class Undef
|
||
# [Array[ DynaSymbol | SymbolLiteral ]] the symbols to undefine
|
||
attr_reader :symbols
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(symbols:, location:)
|
||
@symbols = symbols
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('undef')
|
||
|
||
q.breakable
|
||
q.group(2, '(', ')') { q.seplist(symbols) { |symbol| q.pp(symbol) } }
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :undef, syms: symbols, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_undef: (Array[DynaSymbol | SymbolLiteral] symbols) -> Undef
|
||
def on_undef(symbols)
|
||
keyword = find_token(Kw, 'undef')
|
||
|
||
Undef.new(
|
||
symbols: symbols,
|
||
location: keyword.location.to(symbols.last.location)
|
||
)
|
||
end
|
||
|
||
# Unless represents the first clause in an +unless+ chain.
|
||
#
|
||
# unless predicate
|
||
# end
|
||
#
|
||
class Unless
|
||
# [untyped] 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
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(predicate:, statements:, consequent:, location:)
|
||
@predicate = predicate
|
||
@statements = statements
|
||
@consequent = consequent
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('unless')
|
||
|
||
q.breakable
|
||
q.pp(predicate)
|
||
|
||
q.breakable
|
||
q.pp(statements)
|
||
|
||
if consequent
|
||
q.breakable
|
||
q.pp(consequent)
|
||
end
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :unless,
|
||
pred: predicate,
|
||
stmts: statements,
|
||
cons: consequent,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_unless: (
|
||
# untyped predicate,
|
||
# Statements statements,
|
||
# ((nil | Elsif | Else) consequent)
|
||
# ) -> Unless
|
||
def on_unless(predicate, statements, consequent)
|
||
beginning = find_token(Kw, 'unless')
|
||
ending = consequent || find_token(Kw, 'end')
|
||
|
||
statements.bind(predicate.location.end_char, ending.location.start_char)
|
||
|
||
Unless.new(
|
||
predicate: predicate,
|
||
statements: statements,
|
||
consequent: consequent,
|
||
location: beginning.location.to(ending.location)
|
||
)
|
||
end
|
||
|
||
# UnlessMod represents the modifier form of an +unless+ statement.
|
||
#
|
||
# expression unless predicate
|
||
#
|
||
class UnlessMod
|
||
# [untyped] the expression to be executed
|
||
attr_reader :statement
|
||
|
||
# [untyped] the expression to be checked
|
||
attr_reader :predicate
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(statement:, predicate:, location:)
|
||
@statement = statement
|
||
@predicate = predicate
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('unless_mod')
|
||
|
||
q.breakable
|
||
q.pp(statement)
|
||
|
||
q.breakable
|
||
q.pp(predicate)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :unless_mod,
|
||
stmt: statement,
|
||
pred: predicate,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_unless_mod: (untyped predicate, untyped statement) -> UnlessMod
|
||
def on_unless_mod(predicate, statement)
|
||
find_token(Kw, 'unless')
|
||
|
||
UnlessMod.new(
|
||
statement: statement,
|
||
predicate: predicate,
|
||
location: statement.location.to(predicate.location)
|
||
)
|
||
end
|
||
|
||
# Until represents an +until+ loop.
|
||
#
|
||
# until predicate
|
||
# end
|
||
#
|
||
class Until
|
||
# [untyped] the expression to be checked
|
||
attr_reader :predicate
|
||
|
||
# [Statements] the expressions to be executed
|
||
attr_reader :statements
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(predicate:, statements:, location:)
|
||
@predicate = predicate
|
||
@statements = statements
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('until')
|
||
|
||
q.breakable
|
||
q.pp(predicate)
|
||
|
||
q.breakable
|
||
q.pp(statements)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :until,
|
||
pred: predicate,
|
||
stmts: statements,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_until: (untyped predicate, Statements statements) -> Until
|
||
def on_until(predicate, statements)
|
||
beginning = find_token(Kw, 'until')
|
||
ending = find_token(Kw, 'end')
|
||
|
||
# Consume the do keyword if it exists so that it doesn't get confused for
|
||
# some other block
|
||
keyword = find_token(Kw, 'do', consume: false)
|
||
if keyword && keyword.location.start_char > predicate.location.end_char &&
|
||
keyword.location.end_char < ending.location.start_char
|
||
tokens.delete(keyword)
|
||
end
|
||
|
||
# Update the Statements location information
|
||
statements.bind(predicate.location.end_char, ending.location.start_char)
|
||
|
||
Until.new(
|
||
predicate: predicate,
|
||
statements: statements,
|
||
location: beginning.location.to(ending.location)
|
||
)
|
||
end
|
||
|
||
# UntilMod represents the modifier form of a +until+ loop.
|
||
#
|
||
# expression until predicate
|
||
#
|
||
class UntilMod
|
||
# [untyped] the expression to be executed
|
||
attr_reader :statement
|
||
|
||
# [untyped] the expression to be checked
|
||
attr_reader :predicate
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(statement:, predicate:, location:)
|
||
@statement = statement
|
||
@predicate = predicate
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('until_mod')
|
||
|
||
q.breakable
|
||
q.pp(statement)
|
||
|
||
q.breakable
|
||
q.pp(predicate)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :until_mod,
|
||
stmt: statement,
|
||
pred: predicate,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_until_mod: (untyped predicate, untyped statement) -> UntilMod
|
||
def on_until_mod(predicate, statement)
|
||
find_token(Kw, 'until')
|
||
|
||
UntilMod.new(
|
||
statement: statement,
|
||
predicate: predicate,
|
||
location: statement.location.to(predicate.location)
|
||
)
|
||
end
|
||
|
||
# VarAlias represents when you're using the +alias+ keyword with global
|
||
# variable arguments.
|
||
#
|
||
# alias $new $old
|
||
#
|
||
class VarAlias
|
||
# [GVar] the new alias of the variable
|
||
attr_reader :left
|
||
|
||
# [Backref | GVar] the current name of the variable to be aliased
|
||
attr_reader :right
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(left:, right:, location:)
|
||
@left = left
|
||
@right = right
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('var_alias')
|
||
|
||
q.breakable
|
||
q.pp(left)
|
||
|
||
q.breakable
|
||
q.pp(right)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :var_alias, left: left, right: right, loc: location }.to_json(
|
||
*opts
|
||
)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_var_alias: (GVar left, (Backref | GVar) right) -> VarAlias
|
||
def on_var_alias(left, right)
|
||
keyword = find_token(Kw, 'alias')
|
||
|
||
VarAlias.new(
|
||
left: left,
|
||
right: right,
|
||
location: keyword.location.to(right.location)
|
||
)
|
||
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
|
||
# [nil | Const | CVar | GVar | Ident | IVar] the target of this node
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('var_field')
|
||
|
||
q.breakable
|
||
q.pp(value)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :var_field, value: value, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_var_field: (
|
||
# (nil | Const | CVar | GVar | Ident | IVar) value
|
||
# ) -> VarField
|
||
def on_var_field(value)
|
||
location =
|
||
if value
|
||
value.location
|
||
else
|
||
# You can hit this pattern if you're assigning to a splat using pattern
|
||
# matching syntax in Ruby 2.7+
|
||
Location.fixed(line: lineno, char: char_pos)
|
||
end
|
||
|
||
VarField.new(value: value, location: location)
|
||
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
|
||
# [Const | CVar | GVar | Ident | IVar | Kw] the value of this node
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('var_ref')
|
||
|
||
q.breakable
|
||
q.pp(value)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :var_ref, value: value, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_var_ref: ((Const | CVar | GVar | Ident | IVar | Kw) value) -> VarRef
|
||
def on_var_ref(value)
|
||
VarRef.new(value: value, location: value.location)
|
||
end
|
||
|
||
# AccessCtrl represents a call to a method visibility control, i.e., +public+,
|
||
# +protected+, or +private+.
|
||
#
|
||
# private
|
||
#
|
||
class AccessCtrl
|
||
# [Ident] the value of this expression
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('access_ctrl')
|
||
|
||
q.breakable
|
||
q.pp(value)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :access_ctrl, value: value, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# VCall represent any plain named object with Ruby that could be either a
|
||
# local variable or a method call.
|
||
#
|
||
# variable
|
||
#
|
||
class VCall
|
||
# [Ident] the value of this expression
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('vcall')
|
||
|
||
q.breakable
|
||
q.pp(value)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :vcall, value: value, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_vcall: (Ident ident) -> AccessCtrl | VCall
|
||
def on_vcall(ident)
|
||
@controls ||= %w[private protected public].freeze
|
||
|
||
if @controls.include?(ident.value) && ident.value == lines[lineno - 1].strip
|
||
# Access controls like private, protected, and public are reported as
|
||
# vcall nodes since they're technically method calls. We want to be able
|
||
# add new lines around them as necessary, so here we're going to
|
||
# explicitly track those as a different node type.
|
||
AccessCtrl.new(value: ident, location: ident.location)
|
||
else
|
||
VCall.new(value: ident, location: ident.location)
|
||
end
|
||
end
|
||
|
||
# VoidStmt represents an empty lexical block of code.
|
||
#
|
||
# ;;
|
||
#
|
||
class VoidStmt
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(location:)
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') { q.text('void_stmt') }
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :void_stmt, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_void_stmt: () -> VoidStmt
|
||
def on_void_stmt
|
||
VoidStmt.new(location: Location.fixed(line: lineno, char: char_pos))
|
||
end
|
||
|
||
# When represents a +when+ clause in a +case+ chain.
|
||
#
|
||
# case value
|
||
# when predicate
|
||
# end
|
||
#
|
||
class When
|
||
# [untyped] 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
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(arguments:, statements:, consequent:, location:)
|
||
@arguments = arguments
|
||
@statements = statements
|
||
@consequent = consequent
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('when')
|
||
|
||
q.breakable
|
||
q.pp(arguments)
|
||
|
||
q.breakable
|
||
q.pp(statements)
|
||
|
||
if consequent
|
||
q.breakable
|
||
q.pp(consequent)
|
||
end
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :when,
|
||
args: arguments,
|
||
stmts: statements,
|
||
cons: consequent,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_when: (
|
||
# untyped arguments,
|
||
# Statements statements,
|
||
# (nil | Else | When) consequent
|
||
# ) -> When
|
||
def on_when(arguments, statements, consequent)
|
||
beginning = find_token(Kw, 'when')
|
||
ending = consequent || find_token(Kw, 'end')
|
||
|
||
statements.bind(arguments.location.end_char, ending.location.start_char)
|
||
|
||
When.new(
|
||
arguments: arguments,
|
||
statements: statements,
|
||
consequent: consequent,
|
||
location: beginning.location.to(ending.location)
|
||
)
|
||
end
|
||
|
||
# While represents a +while+ loop.
|
||
#
|
||
# while predicate
|
||
# end
|
||
#
|
||
class While
|
||
# [untyped] the expression to be checked
|
||
attr_reader :predicate
|
||
|
||
# [Statements] the expressions to be executed
|
||
attr_reader :statements
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(predicate:, statements:, location:)
|
||
@predicate = predicate
|
||
@statements = statements
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('while')
|
||
|
||
q.breakable
|
||
q.pp(predicate)
|
||
|
||
q.breakable
|
||
q.pp(statements)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :while,
|
||
pred: predicate,
|
||
stmts: statements,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_while: (untyped predicate, Statements statements) -> While
|
||
def on_while(predicate, statements)
|
||
beginning = find_token(Kw, 'while')
|
||
ending = find_token(Kw, 'end')
|
||
|
||
# Consume the do keyword if it exists so that it doesn't get confused for
|
||
# some other block
|
||
keyword = find_token(Kw, 'do', consume: false)
|
||
if keyword && keyword.location.start_char > predicate.location.end_char &&
|
||
keyword.location.end_char < ending.location.start_char
|
||
tokens.delete(keyword)
|
||
end
|
||
|
||
# Update the Statements location information
|
||
statements.bind(predicate.location.end_char, ending.location.start_char)
|
||
|
||
While.new(
|
||
predicate: predicate,
|
||
statements: statements,
|
||
location: beginning.location.to(ending.location)
|
||
)
|
||
end
|
||
|
||
# WhileMod represents the modifier form of a +while+ loop.
|
||
#
|
||
# expression while predicate
|
||
#
|
||
class WhileMod
|
||
# [untyped] the expression to be executed
|
||
attr_reader :statement
|
||
|
||
# [untyped] the expression to be checked
|
||
attr_reader :predicate
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(statement:, predicate:, location:)
|
||
@statement = statement
|
||
@predicate = predicate
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('while_mod')
|
||
|
||
q.breakable
|
||
q.pp(statement)
|
||
|
||
q.breakable
|
||
q.pp(predicate)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{
|
||
type: :while_mod,
|
||
stmt: statement,
|
||
pred: predicate,
|
||
loc: location
|
||
}.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_while_mod: (untyped predicate, untyped statement) -> WhileMod
|
||
def on_while_mod(predicate, statement)
|
||
find_token(Kw, 'while')
|
||
|
||
WhileMod.new(
|
||
statement: statement,
|
||
predicate: predicate,
|
||
location: statement.location.to(predicate.location)
|
||
)
|
||
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
|
||
# [Array[ StringEmbExpr | StringDVar | TStringContent ]] the parts of the
|
||
# word
|
||
attr_reader :parts
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(parts:, location:)
|
||
@parts = parts
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('word')
|
||
|
||
q.breakable
|
||
q.group(2, '(', ')') { q.seplist(parts) { |part| q.pp(part) } }
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :word, parts: parts, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_word_add: (
|
||
# Word word,
|
||
# (StringEmbExpr | StringDVar | TStringContent) part
|
||
# ) -> Word
|
||
def on_word_add(word, part)
|
||
location =
|
||
word.parts.empty? ? part.location : word.location.to(part.location)
|
||
|
||
Word.new(parts: word.parts << part, location: location)
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_word_new: () -> Word
|
||
def on_word_new
|
||
Word.new(parts: [], location: Location.fixed(line: lineno, char: char_pos))
|
||
end
|
||
|
||
# Words represents a string literal array with interpolation.
|
||
#
|
||
# %W[one two three]
|
||
#
|
||
class Words
|
||
# [Array[ Word ]] the elements of this array
|
||
attr_reader :elements
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(elements:, location:)
|
||
@elements = elements
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('words')
|
||
|
||
q.breakable
|
||
q.group(2, '(', ')') { q.seplist(elements) { |element| q.pp(element) } }
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :words, elems: elements, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_words_add: (Words words, Word word) -> Words
|
||
def on_words_add(words, word)
|
||
Words.new(
|
||
elements: words.elements << word,
|
||
location: words.location.to(word.location)
|
||
)
|
||
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
|
||
# [String] the start of the word literal array
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_words_beg: (String value) -> WordsBeg
|
||
def on_words_beg(value)
|
||
node =
|
||
WordsBeg.new(
|
||
value: value,
|
||
location: Location.token(line: lineno, char: char_pos, size: value.size)
|
||
)
|
||
|
||
tokens << node
|
||
node
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_words_new: () -> Words
|
||
def on_words_new
|
||
words_beg = find_token(WordsBeg)
|
||
|
||
Words.new(elements: [], location: words_beg.location)
|
||
end
|
||
|
||
# def on_words_sep(value)
|
||
# value
|
||
# end
|
||
|
||
# XString represents the contents of an XStringLiteral.
|
||
#
|
||
# `ls`
|
||
#
|
||
class XString
|
||
# [Array[ StringEmbExpr | StringDVar | TStringContent ]] the parts of the
|
||
# xstring
|
||
attr_reader :parts
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(parts:, location:)
|
||
@parts = parts
|
||
@location = location
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_xstring_add: (
|
||
# XString xstring,
|
||
# (StringEmbExpr | StringDVar | TStringContent) part
|
||
# ) -> XString
|
||
def on_xstring_add(xstring, part)
|
||
XString.new(
|
||
parts: xstring.parts << part,
|
||
location: xstring.location.to(part.location)
|
||
)
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_xstring_new: () -> XString
|
||
def on_xstring_new
|
||
heredoc = @heredocs[-1]
|
||
|
||
location =
|
||
if heredoc && heredoc.beginning.value.include?('`')
|
||
heredoc.location
|
||
else
|
||
find_token(Backtick).location
|
||
end
|
||
|
||
XString.new(parts: [], location: location)
|
||
end
|
||
|
||
# XStringLiteral represents a string that gets executed.
|
||
#
|
||
# `ls`
|
||
#
|
||
class XStringLiteral
|
||
# [Array[ StringEmbExpr | StringDVar | TStringContent ]] the parts of the
|
||
# xstring
|
||
attr_reader :parts
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(parts:, location:)
|
||
@parts = parts
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('xstring_literal')
|
||
|
||
q.breakable
|
||
q.group(2, '(', ')') { q.seplist(parts) { |part| q.pp(part) } }
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :xstring_literal, parts: parts, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_xstring_literal: (XString xstring) -> Heredoc | XStringLiteral
|
||
def on_xstring_literal(xstring)
|
||
heredoc = @heredocs[-1]
|
||
|
||
if heredoc && heredoc.beginning.value.include?('`')
|
||
Heredoc.new(
|
||
beginning: heredoc.beginning,
|
||
ending: heredoc.ending,
|
||
parts: xstring.parts,
|
||
location: heredoc.location
|
||
)
|
||
else
|
||
ending = find_token(TStringEnd)
|
||
|
||
XStringLiteral.new(
|
||
parts: xstring.parts,
|
||
location: xstring.location.to(ending.location)
|
||
)
|
||
end
|
||
end
|
||
|
||
# Yield represents using the +yield+ keyword with arguments.
|
||
#
|
||
# yield value
|
||
#
|
||
class Yield
|
||
# [ArgsAddBlock | Paren] the arguments passed to the yield
|
||
attr_reader :arguments
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(arguments:, location:)
|
||
@arguments = arguments
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('yield')
|
||
|
||
q.breakable
|
||
q.pp(arguments)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :yield, args: arguments, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_yield: ((ArgsAddBlock | Paren) arguments) -> Yield
|
||
def on_yield(arguments)
|
||
keyword = find_token(Kw, 'yield')
|
||
|
||
Yield.new(
|
||
arguments: arguments,
|
||
location: keyword.location.to(arguments.location)
|
||
)
|
||
end
|
||
|
||
# Yield0 represents the bare +yield+ keyword with no arguments.
|
||
#
|
||
# yield
|
||
#
|
||
class Yield0
|
||
# [String] the value of the keyword
|
||
attr_reader :value
|
||
|
||
# [Location] the location of this node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('yield0')
|
||
|
||
q.breakable
|
||
q.pp(value)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :yield0, value: value, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_yield0: () -> Yield0
|
||
def on_yield0
|
||
keyword = find_token(Kw, 'yield')
|
||
|
||
Yield0.new(value: keyword.value, location: keyword.location)
|
||
end
|
||
|
||
# ZSuper represents the bare +super+ keyword with no arguments.
|
||
#
|
||
# super
|
||
#
|
||
class ZSuper
|
||
# [String] the value of the keyword
|
||
attr_reader :value
|
||
|
||
# [Location] the location of the node
|
||
attr_reader :location
|
||
|
||
def initialize(value:, location:)
|
||
@value = value
|
||
@location = location
|
||
end
|
||
|
||
def pretty_print(q)
|
||
q.group(2, '(', ')') do
|
||
q.text('zsuper')
|
||
|
||
q.breakable
|
||
q.pp(value)
|
||
end
|
||
end
|
||
|
||
def to_json(*opts)
|
||
{ type: :zsuper, value: value, loc: location }.to_json(*opts)
|
||
end
|
||
end
|
||
|
||
# :call-seq:
|
||
# on_zsuper: () -> ZSuper
|
||
def on_zsuper
|
||
keyword = find_token(Kw, 'super')
|
||
|
||
ZSuper.new(value: keyword.value, location: keyword.location)
|
||
end
|
||
end
|