diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml
new file mode 100644
index 00000000..8adc1b45
--- /dev/null
+++ b/.github/workflows/gh-pages.yml
@@ -0,0 +1,26 @@
+name: Github Pages (rdoc)
+on: [push]
+jobs:
+ build-and-deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout 🛎️
+ uses: actions/checkout@master
+
+ - name: Set up Ruby 💎
+ uses: ruby/setup-ruby@v1
+ with:
+ bundler-cache: true
+ ruby-version: '3.1'
+
+ - name: Install rdoc and generate docs 🔧
+ run: |
+ gem install rdoc
+ rdoc --main README.md --op rdocs --exclude={Gemfile,Rakefile,"coverage/*","vendor/*","bin/*","test/*","tmp/*"}
+ cp -r doc rdocs/doc
+
+ - name: Deploy 🚀
+ uses: peaceiris/actions-gh-pages@v3
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ publish_dir: ./rdocs
diff --git a/.gitignore b/.gitignore
index 76aa4f36..24bbb786 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,8 +2,8 @@
/.yardoc
/_yardoc/
/coverage/
-/doc/
/pkg/
+/rdocs/
/spec/reports/
/tmp/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6b049bd1..2d8ceaa2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,23 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a
## [Unreleased]
+## [2.0.0] - 2022-03-31
+
+### Added
+
+- The new `SyntaxTree.register_handler` hook for plugins.
+- The new `--plugins=` option on the CLI.
+
+### Changed
+
+- Changed `SyntaxTree` from being a class to being a module. The parser functionality is moved into `SyntaxTree::Parser`.
+- There is now a parent class for all of the nodes named `SyntaxTree::Node`.
+- The `Implicits` class has been renamed to `InlayHints` to match the new LSP spec.
+
+### Removed
+
+- The disassembly code action has been removed to limit the scope of this project overall.
+
## [1.2.0] - 2022-01-09
### Added
diff --git a/Gemfile.lock b/Gemfile.lock
index 8dcdb592..e6eceae8 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,19 +1,19 @@
PATH
remote: .
specs:
- syntax_tree (1.2.0)
+ syntax_tree (2.0.0)
GEM
remote: https://rubygems.org/
specs:
ast (2.4.2)
- benchmark-ips (2.9.2)
+ benchmark-ips (2.10.0)
docile (1.4.0)
minitest (5.15.0)
- parser (3.1.0.0)
+ parser (3.1.1.0)
ast (~> 2.4.1)
rake (13.0.6)
- ruby_parser (3.18.1)
+ ruby_parser (3.19.0)
sexp_processor (~> 4.16)
sexp_processor (4.16.0)
simplecov (0.21.2)
@@ -22,7 +22,7 @@ GEM
simplecov_json_formatter (~> 0.1)
simplecov-html (0.12.3)
simplecov_json_formatter (0.1.3)
- stackprof (0.2.17)
+ stackprof (0.2.19)
PLATFORMS
x86_64-darwin-19
diff --git a/README.md b/README.md
index 0103db27..52acbfcf 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,7 @@
+
+

+
+
# SyntaxTree
[](https://github.com/kddnewton/syntax_tree/actions)
diff --git a/bin/bench b/bin/bench
index 307d3044..f918daea 100755
--- a/bin/bench
+++ b/bin/bench
@@ -1,23 +1,23 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
-require 'bundler/setup'
-require 'benchmark/ips'
+require "bundler/setup"
+require "benchmark/ips"
-require_relative '../lib/syntax_tree'
-require 'ruby_parser'
-require 'parser/current'
+require_relative "../lib/syntax_tree"
+require "ruby_parser"
+require "parser/current"
def compare(filepath)
- prefix = "#{File.expand_path('..', __dir__)}/"
+ prefix = "#{File.expand_path("..", __dir__)}/"
puts "=== #{filepath.delete_prefix(prefix)} ==="
source = File.read(filepath)
Benchmark.ips do |x|
- x.report('syntax_tree') { SyntaxTree.new(source).parse }
- x.report('parser') { Parser::CurrentRuby.parse(source) }
- x.report('ruby_parser') { RubyParser.new.parse(source) }
+ x.report("syntax_tree") { SyntaxTree.parse(source) }
+ x.report("parser") { Parser::CurrentRuby.parse(source) }
+ x.report("ruby_parser") { RubyParser.new.parse(source) }
x.compare!
end
end
@@ -29,8 +29,8 @@ filepaths = ARGV
# file).
if filepaths.empty?
filepaths = [
- File.expand_path('bench', __dir__),
- File.expand_path('../lib/syntax_tree.rb', __dir__)
+ File.expand_path("bench", __dir__),
+ File.expand_path("../lib/syntax_tree.rb", __dir__)
]
end
diff --git a/bin/console b/bin/console
index e19966f5..1c18bd62 100755
--- a/bin/console
+++ b/bin/console
@@ -1,8 +1,8 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
-require 'bundler/setup'
-require 'syntax_tree'
+require "bundler/setup"
+require "syntax_tree"
-require 'irb'
+require "irb"
IRB.start(__FILE__)
diff --git a/bin/profile b/bin/profile
index 86c025e4..550402e3 100755
--- a/bin/profile
+++ b/bin/profile
@@ -1,10 +1,10 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
-require 'bundler/setup'
-require 'stackprof'
+require "bundler/setup"
+require "stackprof"
-filepath = File.expand_path('../lib/syntax_tree', __dir__)
+filepath = File.expand_path("../lib/syntax_tree", __dir__)
require_relative filepath
GC.disable
diff --git a/doc/logo.svg b/doc/logo.svg
new file mode 100644
index 00000000..21c4209c
--- /dev/null
+++ b/doc/logo.svg
@@ -0,0 +1,284 @@
+
+
+
diff --git a/exe/stree b/exe/stree
index 6ec4acae..3bae88e9 100755
--- a/exe/stree
+++ b/exe/stree
@@ -2,6 +2,7 @@
# frozen_string_literal: true
$:.unshift(File.expand_path("../lib", __dir__))
+
require "syntax_tree"
require "syntax_tree/cli"
diff --git a/lib/syntax_tree.rb b/lib/syntax_tree.rb
index 22cc283a..14db905c 100644
--- a/lib/syntax_tree.rb
+++ b/lib/syntax_tree.rb
@@ -5,6 +5,9 @@
require "ripper"
require "stringio"
+require_relative "syntax_tree/formatter"
+require_relative "syntax_tree/node"
+require_relative "syntax_tree/parser"
require_relative "syntax_tree/version"
# If PrettyPrint::Align isn't defined, then we haven't gotten the updated
@@ -21,277 +24,21 @@
end
end
-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
- attr_reader :start
-
- 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
- attr_reader :start, :indices
-
- def initialize(start, line)
- @start = start
- @indices = []
-
- line.each_char.with_index(start) do |char, index|
- char.bytesize.times { @indices << index }
- end
- end
-
- # Technically it's possible for the column index to be a negative value if
- # there's a BOM at the beginning of the file, which is the reason we need to
- # compare it to 0 here.
- def [](byteindex)
- indices[byteindex < 0 ? 0 : 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: [end_line, other.end_line].max,
- 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
-
- # A slightly enhanced PP that knows how to format recursively including
- # comments.
- class Formatter < PP
- COMMENT_PRIORITY = 1
- HEREDOC_PRIORITY = 2
-
- attr_reader :source, :stack, :quote
-
- def initialize(source, ...)
- super(...)
-
- @source = source
- @stack = []
- @quote = "\""
- end
-
- def format(node, stackable: true)
- stack << node if stackable
- doc = nil
-
- # If there are comments, then we're going to format them around the node
- # so that they get printed properly.
- if node.comments.any?
- leading, trailing = node.comments.partition(&:leading?)
-
- # Print all comments that were found before the node.
- leading.each do |comment|
- comment.format(self)
- breakable(force: true)
- end
-
- # If the node has a stree-ignore comment right before it, then we're
- # going to just print out the node as it was seen in the source.
- if leading.last&.ignore?
- doc = text(source[node.location.start_char...node.location.end_char])
- else
- doc = node.format(self)
- end
-
- # Print all comments that were found after the node.
- trailing.each do |comment|
- line_suffix(priority: COMMENT_PRIORITY) do
- text(" ")
- comment.format(self)
- break_parent
- end
- end
- else
- doc = node.format(self)
- end
-
- stack.pop if stackable
- doc
- end
-
- def format_each(nodes)
- nodes.each { |node| format(node) }
- end
-
- def parent
- stack[-2]
- end
-
- def parents
- stack[0...-1].reverse_each
- end
- end
-
- # [String] the source being parsed
- attr_reader :source
-
- # [Array[ String ]] the list of lines in the source
- attr_reader :lines
-
- # [Array[ SingleByteString | MultiByteString ]] the list of objects that
- # represent the start of each line in character offsets
- attr_reader :line_counts
-
- # [Array[ untyped ]] a running list of tokens that have been found in the
- # source. This list changes a lot as certain nodes will "consume" these tokens
- # to determine their bounds.
- attr_reader :tokens
-
- # [Array[ Comment | EmbDoc ]] the list of comments that have been found while
- # parsing the source.
- attr_reader :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(/\r?\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
-
- # Make sure line counts is filled out with the first and last line at
- # minimum so that it has something to compare against if the parser is in a
- # lineno=2 state for an empty file.
- @line_counts << SingleByteString.new(0) if @line_counts.empty?
- @line_counts << SingleByteString.new(last_index)
- end
-
+module SyntaxTree
+ # Parses the given source and returns the syntax tree.
def self.parse(source)
- parser = new(source)
+ parser = Parser.new(source)
response = parser.parse
response unless parser.error?
end
+ # Parses the given source and returns the formatted source.
def self.format(source)
- output = []
-
- formatter = Formatter.new(source, output)
+ formatter = Formatter.new(source, [])
parse(source).format(formatter)
formatter.flush
- output.join
+ formatter.output.join
end
# Returns the source from the given filepath taking into account any potential
@@ -306,13701 +53,4 @@ def self.read(filepath)
File.read(filepath, encoding: encoding)
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, location: nil)
- 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
- token = value == :any ? type.name.split("::", 2).last : value
- message = "Cannot find expected #{token}"
-
- if location
- lineno = location.start_line
- column = location.start_char - line_counts[lineno - 1].start
- raise ParseError.new(message, lineno, column)
- else
- raise ParseError.new(message, lineno, column)
- end
- 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(lbrace:, statements:, location:, comments: [])
- @lbrace = lbrace
- @statements = statements
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [lbrace, statements]
- end
-
- def format(q)
- q.group do
- q.text("BEGIN ")
- q.format(lbrace)
- q.indent do
- q.breakable
- q.format(statements)
- end
- q.breakable
- q.text("}")
- end
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("BEGIN")
-
- q.breakable
- q.pp(statements)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :BEGIN,
- lbrace: lbrace,
- stmts: statements,
- loc: location,
- cmts: comments
- }.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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:, comments: [])
- @value = value
- @location = location
- @comments = comments
- end
-
- def child_nodes
- []
- end
-
- def format(q)
- if value.length != 2
- q.text(value)
- else
- q.text(q.quote)
- q.text(value[1] == "\"" ? "\\\"" : value[1])
- q.text(q.quote)
- end
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("CHAR")
-
- q.breakable
- q.pp(value)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :CHAR, value: value, loc: location, cmts: comments }.to_json(
- *opts
- )
- end
- end
-
- # :call-seq:
- # on_CHAR: (String value) -> CHAR
- def on_CHAR(value)
- CHAR.new(
- value: value,
- location: Location.token(line: lineno, char: char_pos, size: value.size)
- )
- 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(lbrace:, statements:, location:, comments: [])
- @lbrace = lbrace
- @statements = statements
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [lbrace, statements]
- end
-
- def format(q)
- q.group do
- q.text("END ")
- q.format(lbrace)
- q.indent do
- q.breakable
- q.format(statements)
- end
- q.breakable
- q.text("}")
- end
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("END")
-
- q.breakable
- q.pp(statements)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :END,
- lbrace: lbrace,
- stmts: statements,
- loc: location,
- cmts: comments
- }.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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:, comments: [])
- @value = value
- @location = location
- @comments = comments
- end
-
- def child_nodes
- []
- end
-
- def format(q)
- q.text("__END__")
- q.breakable(force: true)
-
- separator = -> { q.breakable(indent: false, force: true) }
- q.seplist(value.split(/\r?\n/, -1), separator) { |line| q.text(line) }
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("__end__")
-
- q.breakable
- q.pp(value)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :__end__, value: value, loc: location, cmts: comments }.to_json(
- *opts
- )
- end
- end
-
- # :call-seq:
- # on___end__: (String value) -> EndContent
- def on___end__(value)
- @__end__ =
- EndContent.new(
- value: source[(char_pos + value.length)..-1],
- 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
- class AliasArgumentFormatter
- # [DynaSymbol | SymbolLiteral] the argument being passed to alias
- attr_reader :argument
-
- def initialize(argument)
- @argument = argument
- end
-
- def comments
- if argument.is_a?(SymbolLiteral)
- argument.comments + argument.value.comments
- else
- argument.comments
- end
- end
-
- def format(q)
- if argument.is_a?(SymbolLiteral)
- q.format(argument.value)
- else
- q.format(argument)
- end
- end
- end
-
- # [DynaSymbol | 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(left:, right:, location:, comments: [])
- @left = left
- @right = right
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [left, right]
- end
-
- def format(q)
- keyword = "alias "
- left_argument = AliasArgumentFormatter.new(left)
-
- q.group do
- q.text(keyword)
- q.format(left_argument, stackable: false)
- q.group do
- q.nest(keyword.length) do
- q.breakable(force: left_argument.comments.any?)
- q.format(AliasArgumentFormatter.new(right), stackable: false)
- end
- end
- end
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("alias")
-
- q.breakable
- q.pp(left)
-
- q.breakable
- q.pp(right)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :alias,
- left: left,
- right: right,
- loc: location,
- cmts: comments
- }.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] the value being passed within the brackets
- attr_reader :index
-
- # [Location] the location of this node
- attr_reader :location
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(collection:, index:, location:, comments: [])
- @collection = collection
- @index = index
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [collection, index]
- end
-
- def format(q)
- q.group do
- q.format(collection)
- q.text("[")
-
- if index
- q.indent do
- q.breakable("")
- q.format(index)
- end
- q.breakable("")
- end
-
- q.text("]")
- end
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("aref")
-
- q.breakable
- q.pp(collection)
-
- q.breakable
- q.pp(index)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :aref,
- collection: collection,
- index: index,
- loc: location,
- cmts: comments
- }.to_json(*opts)
- end
- end
-
- # :call-seq:
- # on_aref: (untyped collection, (nil | Args) 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 | Args] the value being passed within the brackets
- attr_reader :index
-
- # [Location] the location of this node
- attr_reader :location
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(collection:, index:, location:, comments: [])
- @collection = collection
- @index = index
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [collection, index]
- end
-
- def format(q)
- q.group do
- q.format(collection)
- q.text("[")
-
- if index
- q.indent do
- q.breakable("")
- q.format(index)
- end
- q.breakable("")
- end
-
- q.text("]")
- end
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("aref_field")
-
- q.breakable
- q.pp(collection)
-
- q.breakable
- q.pp(index)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :aref_field,
- collection: collection,
- index: index,
- loc: location,
- cmts: comments
- }.to_json(*opts)
- end
- end
-
- # :call-seq:
- # on_aref_field: (
- # untyped collection,
- # (nil | Args) 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 Args node
- # that represents the set of arguments being sent to the method method. The
- # argument child node can be +nil+ if no arguments were passed, as in:
- #
- # method()
- #
- class ArgParen
- # [nil | Args | ArgsForward] the arguments inside the
- # parentheses
- attr_reader :arguments
-
- # [Location] the location of this node
- attr_reader :location
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(arguments:, location:, comments: [])
- @arguments = arguments
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [arguments]
- end
-
- def format(q)
- unless arguments
- q.text("()")
- return
- end
-
- q.group(0, "(", ")") do
- q.indent do
- q.breakable("")
- q.format(arguments)
- end
- q.breakable("")
- end
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("arg_paren")
-
- q.breakable
- q.pp(arguments)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :arg_paren,
- args: arguments,
- loc: location,
- cmts: comments
- }.to_json(*opts)
- end
- end
-
- # :call-seq:
- # on_arg_paren: (
- # (nil | Args | 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(parts:, location:, comments: [])
- @parts = parts
- @location = location
- @comments = comments
- end
-
- def child_nodes
- parts
- end
-
- def format(q)
- q.seplist(parts) { |part| q.format(part) }
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("args")
-
- q.breakable
- q.group(2, "(", ")") { q.seplist(parts) { |part| q.pp(part) } }
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :args, parts: parts, loc: location, cmts: comments }.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
-
- # ArgBlock represents using a block operator on an expression.
- #
- # method(&expression)
- #
- class ArgBlock
- # [nil | untyped] the expression being turned into a block
- attr_reader :value
-
- # [Location] the location of this node
- attr_reader :location
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:, comments: [])
- @value = value
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [value]
- end
-
- def format(q)
- q.text("&")
- q.format(value) if value
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("arg_block")
-
- if value
- q.breakable
- q.pp(value)
- end
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :arg_block, value: value, loc: location, cmts: comments }.to_json(
- *opts
- )
- end
- end
-
- # :call-seq:
- # on_args_add_block: (
- # Args arguments,
- # (false | untyped) block
- # ) -> Args
- def on_args_add_block(arguments, block)
- operator = find_token(Op, "&", consume: false)
-
- # If we can't find the & operator, then there's no block to add to the list,
- # so we're just going to return the arguments as-is.
- return arguments unless operator
-
- # Now we know we have an & operator, so we're going to delete it from the
- # list of tokens to make sure it doesn't get confused with anything else.
- tokens.delete(operator)
-
- # Construct the location that represents the block argument.
- location = operator.location
- location = operator.location.to(block.location) if block
-
- # If there are any arguments and the operator we found from the list is not
- # after them, then we're going to return the arguments as-is because we're
- # looking at an & that occurs before the arguments are done.
- if arguments.parts.any? && location.start_char < arguments.location.end_char
- return arguments
- end
-
- # Otherwise, we're looking at an actual block argument (with or without a
- # block, which could be missing because it could be a bare & since 3.1.0).
- arg_block = ArgBlock.new(value: block, location: location)
-
- Args.new(
- parts: arguments.parts << arg_block,
- location: arguments.location.to(location)
- )
- end
-
- # Star represents using a splat operator on an expression.
- #
- # method(*arguments)
- #
- class ArgStar
- # [nil | untyped] the expression being splatted
- attr_reader :value
-
- # [Location] the location of this node
- attr_reader :location
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:, comments: [])
- @value = value
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [value]
- end
-
- def format(q)
- q.text("*")
- q.format(value) if value
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("arg_star")
-
- q.breakable
- q.pp(value)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :arg_star, value: value, loc: location, cmts: comments }.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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:, comments: [])
- @value = value
- @location = location
- @comments = comments
- end
-
- def child_nodes
- []
- end
-
- def format(q)
- q.text(value)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("args_forward")
-
- q.breakable
- q.pp(value)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :args_forward,
- value: value,
- loc: location,
- cmts: comments
- }.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 an array literal, which can optionally contain
- # elements.
- #
- # []
- # [one, two, three]
- #
- class ArrayLiteral
- class QWordsFormatter
- # [Args] the contents of the array
- attr_reader :contents
-
- def initialize(contents)
- @contents = contents
- end
-
- def format(q)
- q.group(0, "%w[", "]") do
- q.indent do
- q.breakable("")
- q.seplist(contents.parts, -> { q.breakable }) do |part|
- if part.is_a?(StringLiteral)
- q.format(part.parts.first)
- else
- q.text(part.value[1..-1])
- end
- end
- end
- q.breakable("")
- end
- end
- end
-
- class QSymbolsFormatter
- # [Args] the contents of the array
- attr_reader :contents
-
- def initialize(contents)
- @contents = contents
- end
-
- def format(q)
- q.group(0, "%i[", "]") do
- q.indent do
- q.breakable("")
- q.seplist(contents.parts, -> { q.breakable }) do |part|
- q.format(part.value)
- end
- end
- q.breakable("")
- end
- end
- end
-
- class VarRefsFormatter
- # [Args] the contents of the array
- attr_reader :contents
-
- def initialize(contents)
- @contents = contents
- end
-
- def format(q)
- q.group(0, "[", "]") do
- q.indent do
- q.breakable("")
-
- separator = -> do
- q.text(",")
- q.fill_breakable
- end
-
- q.seplist(contents.parts, separator) { |part| q.format(part) }
- end
- q.breakable("")
- end
- end
- end
-
- # [LBracket] the bracket that opens this array
- attr_reader :lbracket
-
- # [nil | Args] the contents of the array
- attr_reader :contents
-
- # [Location] the location of this node
- attr_reader :location
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(lbracket:, contents:, location:, comments: [])
- @lbracket = lbracket
- @contents = contents
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [lbracket, contents]
- end
-
- def format(q)
- if qwords?
- QWordsFormatter.new(contents).format(q)
- return
- end
-
- if qsymbols?
- QSymbolsFormatter.new(contents).format(q)
- return
- end
-
- if var_refs?(q)
- VarRefsFormatter.new(contents).format(q)
- return
- end
-
- q.group do
- q.format(lbracket)
-
- if contents
- q.indent do
- q.breakable("")
- q.format(contents)
- end
- end
-
- q.breakable("")
- q.text("]")
- end
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("array")
-
- q.breakable
- q.pp(contents)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :array, cnts: contents, loc: location, cmts: comments }.to_json(
- *opts
- )
- end
-
- private
-
- def qwords?
- lbracket.comments.empty? && contents && contents.comments.empty? &&
- contents.parts.length > 1 &&
- contents.parts.all? do |part|
- case part
- when StringLiteral
- part.comments.empty? && part.parts.length == 1 &&
- part.parts.first.is_a?(TStringContent) &&
- !part.parts.first.value.match?(/[\s\[\]\\]/)
- when CHAR
- !part.value.match?(/[\[\]\\]/)
- else
- false
- end
- end
- end
-
- def qsymbols?
- lbracket.comments.empty? && contents && contents.comments.empty? &&
- contents.parts.length > 1 &&
- contents.parts.all? do |part|
- part.is_a?(SymbolLiteral) && part.comments.empty?
- end
- end
-
- def var_refs?(q)
- lbracket.comments.empty? && contents && contents.comments.empty? &&
- contents.parts.all? do |part|
- part.is_a?(VarRef) && part.comments.empty?
- end &&
- (
- contents.parts.sum { |part| part.value.value.length + 2 } >
- q.maxwidth * 2
- )
- end
- end
-
- # :call-seq:
- # on_array: ((nil | Args) contents) ->
- # ArrayLiteral | QSymbols | QWords | Symbols | Words
- def on_array(contents)
- if !contents || contents.is_a?(Args)
- lbracket = find_token(LBracket)
- rbracket = find_token(RBracket)
-
- ArrayLiteral.new(
- lbracket: lbracket,
- contents: contents,
- location: lbracket.location.to(rbracket.location)
- )
- else
- tstring_end =
- find_token(TStringEnd, location: contents.beginning.location)
-
- contents.class.new(
- beginning: contents.beginning,
- elements: contents.elements,
- location: contents.location.to(tstring_end.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
- class RestFormatter
- # [VarField] the identifier that represents the remaining positionals
- attr_reader :value
-
- def initialize(value)
- @value = value
- end
-
- def comments
- value.comments
- end
-
- def format(q)
- q.text("*")
- q.format(value)
- end
- end
-
- # [nil | VarRef] 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(
- constant:,
- requireds:,
- rest:,
- posts:,
- location:,
- comments: []
- )
- @constant = constant
- @requireds = requireds
- @rest = rest
- @posts = posts
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [constant, *requireds, rest, *posts]
- end
-
- def format(q)
- parts = [*requireds]
- parts << RestFormatter.new(rest) if rest
- parts += posts
-
- if constant
- q.group do
- q.format(constant)
- q.text("[")
- q.seplist(parts) { |part| q.format(part) }
- q.text("]")
- end
-
- return
- end
-
- parent = q.parent
- if parts.length == 1 || PATTERNS.include?(parent.class)
- q.text("[")
- q.seplist(parts) { |part| q.format(part) }
- q.text("]")
- else
- q.group { q.seplist(parts) { |part| q.format(part) } }
- end
- 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
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :aryptn,
- constant: constant,
- reqs: requireds,
- rest: rest,
- posts: posts,
- loc: location,
- cmts: comments
- }.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
-
- # Determins if the following value should be indented or not.
- module AssignFormatting
- def self.skip_indent?(value)
- (value.is_a?(Call) && skip_indent?(value.receiver)) ||
- [
- ArrayLiteral,
- HashLiteral,
- Heredoc,
- Lambda,
- QSymbols,
- QWords,
- Symbols,
- Words
- ].include?(value.class)
- end
- end
-
- # Assign represents assigning something to a variable or constant. Generally,
- # the left side of the assignment is going to be any node that ends with the
- # name "Field".
- #
- # variable = value
- #
- class Assign
- # [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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(target:, value:, location:, comments: [])
- @target = target
- @value = value
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [target, value]
- end
-
- def format(q)
- q.group do
- q.format(target)
- q.text(" =")
-
- if skip_indent?
- q.text(" ")
- q.format(value)
- else
- q.indent do
- q.breakable
- q.format(value)
- end
- end
- end
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("assign")
-
- q.breakable
- q.pp(target)
-
- q.breakable
- q.pp(value)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :assign,
- target: target,
- value: value,
- loc: location,
- cmts: comments
- }.to_json(*opts)
- end
-
- private
-
- def skip_indent?
- target.comments.empty? &&
- (target.is_a?(ARefField) || AssignFormatting.skip_indent?(value))
- 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(key:, value:, location:, comments: [])
- @key = key
- @value = value
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [key, value]
- end
-
- def format(q)
- if value&.is_a?(HashLiteral)
- format_contents(q)
- else
- q.group { format_contents(q) }
- end
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("assoc")
-
- q.breakable
- q.pp(key)
-
- if value
- q.breakable
- q.pp(value)
- end
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :assoc,
- key: key,
- value: value,
- loc: location,
- cmts: comments
- }.to_json(*opts)
- end
-
- private
-
- def format_contents(q)
- q.parent.format_key(q, key)
- return unless value
-
- if key.comments.empty? && AssignFormatting.skip_indent?(value)
- q.text(" ")
- q.format(value)
- else
- q.indent do
- q.breakable
- q.format(value)
- end
- end
- end
- end
-
- # :call-seq:
- # on_assoc_new: (untyped key, untyped value) -> Assoc
- def on_assoc_new(key, value)
- location = key.location
- location = location.to(value.location) if value
-
- Assoc.new(key: key, value: value, location: 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:, comments: [])
- @value = value
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [value]
- end
-
- def format(q)
- q.text("**")
- q.format(value)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("assoc_splat")
-
- q.breakable
- q.pp(value)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :assoc_splat,
- value: value,
- loc: location,
- cmts: comments
- }.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
-
- # def on_assoclist_from_args(assocs)
- # assocs
- # 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:, comments: [])
- @value = value
- @location = location
- @comments = comments
- end
-
- def child_nodes
- []
- end
-
- def format(q)
- q.text(value)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("backref")
-
- q.breakable
- q.pp(value)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :backref, value: value, loc: location, cmts: comments }.to_json(
- *opts
- )
- end
- end
-
- # :call-seq:
- # on_backref: (String value) -> Backref
- def on_backref(value)
- Backref.new(
- value: value,
- location: Location.token(line: lineno, char: char_pos, size: value.size)
- )
- 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:, comments: [])
- @value = value
- @location = location
- @comments = comments
- end
-
- def child_nodes
- []
- end
-
- def format(q)
- q.text(value)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("backtick")
-
- q.breakable
- q.pp(value)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :backtick, value: value, loc: location, cmts: comments }.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
-
- # This module is responsible for formatting the assocs contained within a
- # hash or bare hash. It first determines if every key in the hash can use
- # labels. If it can, it uses labels. Otherwise it uses hash rockets.
- module HashKeyFormatter
- class Labels
- def format_key(q, key)
- case key
- when Label
- q.format(key)
- when SymbolLiteral
- q.format(key.value)
- q.text(":")
- when DynaSymbol
- q.format(key)
- q.text(":")
- end
- end
- end
-
- class Rockets
- def format_key(q, key)
- case key
- when Label
- q.text(":")
- q.text(key.value.chomp(":"))
- when DynaSymbol
- q.text(":")
- q.format(key)
- else
- q.format(key)
- end
-
- q.text(" =>")
- end
- end
-
- def self.for(container)
- labels =
- container.assocs.all? do |assoc|
- next true if assoc.is_a?(AssocSplat)
-
- case assoc.key
- when Label
- true
- when SymbolLiteral
- # When attempting to convert a hash rocket into a hash label,
- # you need to take care because only certain patterns are
- # allowed. Ruby source says that they have to match keyword
- # arguments to methods, but don't specify what that is. After
- # some experimentation, it looks like it's:
- value = assoc.key.value.value
- value.match?(/^[_A-Za-z]/) && !value.end_with?("=")
- when DynaSymbol
- true
- else
- false
- end
- end
-
- (labels ? Labels : Rockets).new
- end
- end
-
- # BareAssocHash represents a hash of contents being passed as a method
- # argument (and therefore has omitted braces). It's very similar to an
- # AssocListFromArgs node.
- #
- # method(key1: value1, key2: value2)
- #
- class BareAssocHash
- # [Array[ AssocNew | AssocSplat ]]
- attr_reader :assocs
-
- # [Location] the location of this node
- attr_reader :location
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(assocs:, location:, comments: [])
- @assocs = assocs
- @location = location
- @comments = comments
- end
-
- def child_nodes
- assocs
- end
-
- def format(q)
- q.seplist(assocs) { |assoc| q.format(assoc) }
- end
-
- def format_key(q, key)
- (@key_formatter ||= HashKeyFormatter.for(self)).format_key(q, key)
- 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) } }
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :bare_assoc_hash,
- assocs: assocs,
- loc: location,
- cmts: comments
- }.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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(bodystmt:, location:, comments: [])
- @bodystmt = bodystmt
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [bodystmt]
- end
-
- def format(q)
- q.text("begin")
-
- unless bodystmt.empty?
- q.indent do
- q.breakable(force: true) unless bodystmt.statements.empty?
- q.format(bodystmt)
- end
- end
-
- q.breakable(force: true)
- q.text("end")
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("begin")
-
- q.breakable
- q.pp(bodystmt)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :begin,
- bodystmt: bodystmt,
- loc: location,
- cmts: comments
- }.to_json(*opts)
- end
- end
-
- # PinnedBegin represents a pinning a nested statement within pattern matching.
- #
- # case value
- # in ^(statement)
- # end
- #
- class PinnedBegin
- # [untyped] the expression being pinned
- attr_reader :statement
-
- # [Location] the location of this node
- attr_reader :location
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(statement:, location:, comments: [])
- @statement = statement
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [statement]
- end
-
- def format(q)
- q.group do
- q.text("^(")
- q.nest(1) do
- q.indent do
- q.breakable("")
- q.format(statement)
- end
- q.breakable("")
- q.text(")")
- end
- end
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("pinned_begin")
-
- q.breakable
- q.pp(statement)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :pinned_begin,
- stmt: statement,
- loc: location,
- cmts: comments
- }.to_json(*opts)
- end
- end
-
- # :call-seq:
- # on_begin: (untyped bodystmt) -> Begin | PinnedBegin
- def on_begin(bodystmt)
- pin = find_token(Op, "^", consume: false)
-
- if pin && pin.location.start_char < bodystmt.location.start_char
- tokens.delete(pin)
- find_token(LParen)
-
- rparen = find_token(RParen)
- location = pin.location.to(rparen.location)
-
- PinnedBegin.new(statement: bodystmt, location: location)
- else
- 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)
- location = keyword.location.to(bodystmt.location)
-
- Begin.new(bodystmt: bodystmt, location: location)
- end
- end
-
- # Binary represents any expression that involves two sub-expressions with an
- # operator in between. This can be something that looks like a mathematical
- # operation:
- #
- # 1 + 1
- #
- # but can also be something like pushing a value onto an array:
- #
- # array << value
- #
- class Binary
- # [untyped] the left-hand side of the expression
- attr_reader :left
-
- # [Symbol] 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(left:, operator:, right:, location:, comments: [])
- @left = left
- @operator = operator
- @right = right
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [left, right]
- end
-
- def format(q)
- power = operator == :**
-
- q.group do
- q.group { q.format(left) }
- q.text(" ") unless power
-
- q.group do
- q.text(operator)
-
- q.indent do
- q.breakable(power ? "" : " ")
- q.format(right)
- end
- end
- end
- 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)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :binary,
- left: left,
- op: operator,
- right: right,
- loc: location,
- cmts: comments
- }.to_json(*opts)
- end
- end
-
- # :call-seq:
- # on_binary: (untyped left, (Op | Symbol) operator, untyped right) -> Binary
- def on_binary(left, operator, right)
- if operator.is_a?(Symbol)
- # Here, we're going to search backward for the token that's between the
- # two operands that matches the operator so we can delete it from the
- # list.
- index =
- tokens.rindex do |token|
- location = token.location
-
- token.is_a?(Op) &&
- token.value == operator.to_s &&
- location.start_char > left.location.end_char &&
- location.end_char < right.location.start_char
- end
-
- tokens.delete_at(index) if index
- else
- # 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
- end
-
- Binary.new(
- left: left,
- operator: operator,
- right: right,
- location: left.location.to(right.location)
- )
- end
-
- # This module will remove any breakables from the list of contents so that no
- # newlines are present in the output.
- module RemoveBreaks
- class << self
- def call(doc)
- marker = Object.new
- stack = [doc]
-
- while stack.any?
- doc = stack.pop
-
- if doc == marker
- stack.pop
- next
- end
-
- stack += [doc, marker]
-
- case doc
- when PrettyPrint::Align, PrettyPrint::Indent, PrettyPrint::Group
- doc.contents.map! { |child| remove_breaks(child) }
- stack += doc.contents.reverse
- when PrettyPrint::IfBreak
- doc.flat_contents.map! { |child| remove_breaks(child) }
- stack += doc.flat_contents.reverse
- end
- end
- end
-
- private
-
- def remove_breaks(doc)
- case doc
- when PrettyPrint::Breakable
- text = PrettyPrint::Text.new
- text.add(object: doc.force? ? "; " : doc.separator, width: doc.width)
- text
- when PrettyPrint::IfBreak
- PrettyPrint::Align.new(indent: 0, contents: doc.flat_contents)
- else
- doc
- end
- end
- end
- end
-
- # BlockVar represents the parameters being declared for a block. Effectively
- # this node is everything contained within the pipes. This includes all of the
- # various parameter types, as well as block-local variable declarations.
- #
- # method do |positional, optional = value, keyword:, █ local|
- # end
- #
- class BlockVar
- # [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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(params:, locals:, location:, comments: [])
- @params = params
- @locals = locals
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [params, *locals]
- end
-
- def format(q)
- q.group(0, "|", "|") do
- doc = q.format(params)
- RemoveBreaks.call(doc)
-
- if locals.any?
- q.text("; ")
- q.seplist(locals, -> { q.text(", ") }) { |local| q.format(local) }
- end
- end
- 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
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :block_var,
- params: params,
- locals: locals,
- loc: location,
- cmts: comments
- }.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
- # [nil | Ident] the name of the block argument
- attr_reader :name
-
- # [Location] the location of this node
- attr_reader :location
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(name:, location:, comments: [])
- @name = name
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [name]
- end
-
- def format(q)
- q.text("&")
- q.format(name) if name
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("blockarg")
-
- if name
- q.breakable
- q.pp(name)
- end
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :blockarg, name: name, loc: location, cmts: comments }.to_json(
- *opts
- )
- end
- end
-
- # :call-seq:
- # on_blockarg: (Ident name) -> BlockArg
- def on_blockarg(name)
- operator = find_token(Op, "&")
-
- location = operator.location
- location = location.to(name.location) if name
-
- BlockArg.new(name: name, location: 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(
- statements:,
- rescue_clause:,
- else_clause:,
- ensure_clause:,
- location:,
- comments: []
- )
- @statements = statements
- @rescue_clause = rescue_clause
- @else_clause = else_clause
- @ensure_clause = ensure_clause
- @location = location
- @comments = comments
- 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 empty?
- statements.empty? && !rescue_clause && !else_clause && !ensure_clause
- end
-
- def child_nodes
- [statements, rescue_clause, else_clause, ensure_clause]
- end
-
- def format(q)
- q.group do
- q.format(statements) unless statements.empty?
-
- if rescue_clause
- q.nest(-2) do
- q.breakable(force: true)
- q.format(rescue_clause)
- end
- end
-
- if else_clause
- q.nest(-2) do
- q.breakable(force: true)
- q.text("else")
- end
- q.breakable(force: true)
- q.format(else_clause)
- end
-
- if ensure_clause
- q.nest(-2) do
- q.breakable(force: true)
- q.format(ensure_clause)
- end
- end
- 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
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :bodystmt,
- stmts: statements,
- rsc: rescue_clause,
- els: else_clause,
- ens: ensure_clause,
- loc: location,
- cmts: comments
- }.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
-
- # Responsible for formatting either a BraceBlock or a DoBlock.
- class BlockFormatter
- class BlockOpenFormatter
- # [String] the actual output that should be printed
- attr_reader :text
-
- # [LBrace | Keyword] the node that is being represented
- attr_reader :node
-
- def initialize(text, node)
- @text = text
- @node = node
- end
-
- def comments
- node.comments
- end
-
- def format(q)
- q.text(text)
- end
- end
-
- # [BraceBlock | DoBlock] the block node to be formatted
- attr_reader :node
-
- # [LBrace | Keyword] the node that opens the block
- attr_reader :block_open
-
- # [String] the string that closes the block
- attr_reader :block_close
-
- # [BodyStmt | Statements] the statements inside the block
- attr_reader :statements
-
- def initialize(node, block_open, block_close, statements)
- @node = node
- @block_open = block_open
- @block_close = block_close
- @statements = statements
- end
-
- def format(q)
- # If this is nested anywhere inside of a Command or CommandCall node, then
- # we can't change which operators we're using for the bounds of the block.
- break_opening, break_closing, flat_opening, flat_closing =
- if unchangeable_bounds?(q)
- [block_open.value, block_close, block_open.value, block_close]
- elsif forced_do_end_bounds?(q)
- %w[do end do end]
- elsif forced_brace_bounds?(q)
- %w[{ } { }]
- else
- %w[do end { }]
- end
-
- # If the receiver of this block a Command or CommandCall node, then there
- # are no parentheses around the arguments to that command, so we need to
- # break the block.
- receiver = q.parent.call
- if receiver.is_a?(Command) || receiver.is_a?(CommandCall)
- q.break_parent
- format_break(q, break_opening, break_closing)
- return
- end
-
- q.group do
- q.if_break { format_break(q, break_opening, break_closing) }.if_flat do
- format_flat(q, flat_opening, flat_closing)
- end
- end
- end
-
- private
-
- # If this is nested anywhere inside certain nodes, then we can't change
- # which operators/keywords we're using for the bounds of the block.
- def unchangeable_bounds?(q)
- q.parents.any? do |parent|
- # If we hit a statements, then we're safe to use whatever since we
- # know for certain we're going to get split over multiple lines
- # anyway.
- break false if parent.is_a?(Statements)
-
- [Command, CommandCall].include?(parent.class)
- end
- end
-
- # If we're a sibling of a control-flow keyword, then we're going to have to
- # use the do..end bounds.
- def forced_do_end_bounds?(q)
- [Break, Next, Return, Super].include?(q.parent.call.class)
- end
-
- # If we're the predicate of a loop or conditional, then we're going to have
- # to go with the {..} bounds.
- def forced_brace_bounds?(q)
- parents = q.parents.to_a
- parents.each_with_index.any? do |parent, index|
- # If we hit certain breakpoints then we know we're safe.
- break false if [Paren, Statements].include?(parent.class)
-
- [
- If,
- IfMod,
- IfOp,
- Unless,
- UnlessMod,
- While,
- WhileMod,
- Until,
- UntilMod
- ].include?(parent.class) && parent.predicate == parents[index - 1]
- end
- end
-
- def format_break(q, opening, closing)
- q.text(" ")
- q.format(BlockOpenFormatter.new(opening, block_open), stackable: false)
-
- if node.block_var
- q.text(" ")
- q.format(node.block_var)
- end
-
- unless statements.empty?
- q.indent do
- q.breakable
- q.format(statements)
- end
- end
-
- q.breakable
- q.text(closing)
- end
-
- def format_flat(q, opening, closing)
- q.text(" ")
- q.format(BlockOpenFormatter.new(opening, block_open), stackable: false)
-
- if node.block_var
- q.breakable
- q.format(node.block_var)
- q.breakable
- end
-
- if statements.empty?
- q.text(" ") if opening == "do"
- else
- q.breakable unless node.block_var
- q.format(statements)
- q.breakable
- end
-
- q.text(closing)
- end
- 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(lbrace:, block_var:, statements:, location:, comments: [])
- @lbrace = lbrace
- @block_var = block_var
- @statements = statements
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [lbrace, block_var, statements]
- end
-
- def format(q)
- BlockFormatter.new(self, lbrace, "}", statements).format(q)
- 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)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :brace_block,
- lbrace: lbrace,
- block_var: block_var,
- stmts: statements,
- loc: location,
- cmts: comments
- }.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
-
- # Formats either a Break or Next node.
- class FlowControlFormatter
- # [String] the keyword to print
- attr_reader :keyword
-
- # [Break | Next] the node being formatted
- attr_reader :node
-
- def initialize(keyword, node)
- @keyword = keyword
- @node = node
- end
-
- def format(q)
- arguments = node.arguments
-
- q.group do
- q.text(keyword)
-
- if arguments.parts.any?
- if arguments.parts.length == 1
- part = arguments.parts.first
-
- if part.is_a?(Paren)
- q.format(arguments)
- elsif part.is_a?(ArrayLiteral)
- q.text(" ")
- q.format(arguments)
- else
- format_arguments(q, "(", ")")
- end
- else
- format_arguments(q, " [", "]")
- end
- end
- end
- end
-
- private
-
- def format_arguments(q, opening, closing)
- q.if_break { q.text(opening) }
- q.indent do
- q.breakable(" ")
- q.format(node.arguments)
- end
- q.breakable("")
- q.if_break { q.text(closing) }
- end
- end
-
- # Break represents using the +break+ keyword.
- #
- # break
- #
- # It can also optionally accept arguments, as in:
- #
- # break 1
- #
- class Break
- # [Args] the arguments being sent to the keyword
- attr_reader :arguments
-
- # [Location] the location of this node
- attr_reader :location
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(arguments:, location:, comments: [])
- @arguments = arguments
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [arguments]
- end
-
- def format(q)
- FlowControlFormatter.new("break", self).format(q)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("break")
-
- q.breakable
- q.pp(arguments)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :break, args: arguments, loc: location, cmts: comments }.to_json(
- *opts
- )
- end
- end
-
- # :call-seq:
- # on_break: (Args arguments) -> Break
- def on_break(arguments)
- keyword = find_token(Kw, "break")
-
- location = keyword.location
- location = location.to(arguments.location) if arguments.parts.any?
-
- Break.new(arguments: arguments, location: location)
- end
-
- # Wraps a call operator (which can be a string literal :: or an Op node or a
- # Period node) and formats it when called.
- class CallOperatorFormatter
- # [:"::" | Op | Period] the operator being formatted
- attr_reader :operator
-
- def initialize(operator)
- @operator = operator
- end
-
- def comments
- operator == :"::" ? [] : operator.comments
- end
-
- def format(q)
- if operator == :"::" || (operator.is_a?(Op) && operator.value == "::")
- q.text(".")
- else
- operator.format(q)
- end
- end
- end
-
- # Call represents a method call.
- #
- # 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
-
- # [nil | ArgParen | Args] the arguments to the method call
- attr_reader :arguments
-
- # [Location] the location of this node
- attr_reader :location
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(
- receiver:,
- operator:,
- message:,
- arguments:,
- location:,
- comments: []
- )
- @receiver = receiver
- @operator = operator
- @message = message
- @arguments = arguments
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [
- receiver,
- (operator if operator != :"::"),
- (message if message != :call),
- arguments
- ]
- end
-
- def format(q)
- call_operator = CallOperatorFormatter.new(operator)
-
- q.group do
- q.format(receiver)
-
- # If there are trailing comments on the call operator, then we need to
- # use the trailing form as opposed to the leading form.
- q.format(call_operator) if call_operator.comments.any?
-
- q.group do
- q.indent do
- if receiver.comments.any? || call_operator.comments.any?
- q.breakable(force: true)
- end
-
- if call_operator.comments.empty?
- q.format(call_operator, stackable: false)
- end
-
- q.format(message) if message != :call
- end
-
- q.format(arguments) if arguments
- end
- end
- 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)
-
- if arguments
- q.breakable
- q.pp(arguments)
- end
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :call,
- receiver: receiver,
- op: operator,
- message: message,
- args: arguments,
- loc: location,
- cmts: comments
- }.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,
- arguments: nil,
- location: receiver.location.to(ending.location)
- )
- end
-
- # Case represents the beginning of a case chain.
- #
- # case value
- # when 1
- # "one"
- # when 2
- # "two"
- # else
- # "number"
- # end
- #
- class Case
- # [Kw] the keyword that opens this expression
- attr_reader :keyword
-
- # [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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(keyword:, value:, consequent:, location:, comments: [])
- @keyword = keyword
- @value = value
- @consequent = consequent
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [keyword, value, consequent]
- end
-
- def format(q)
- q.group do
- q.format(keyword)
-
- if value
- q.text(" ")
- q.format(value)
- end
-
- q.breakable(force: true)
- q.format(consequent)
- q.breakable(force: true)
-
- q.text("end")
- end
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("case")
-
- q.breakable
- q.pp(keyword)
-
- if value
- q.breakable
- q.pp(value)
- end
-
- q.breakable
- q.pp(consequent)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :case,
- value: value,
- cons: consequent,
- loc: location,
- cmts: comments
- }.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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, operator:, pattern:, location:, comments: [])
- @value = value
- @operator = operator
- @pattern = pattern
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [value, operator, pattern]
- end
-
- def format(q)
- q.group do
- q.format(value)
- q.text(" ")
- q.format(operator)
- q.group do
- q.indent do
- q.breakable
- q.format(pattern)
- end
- end
- end
- 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)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :rassign,
- value: value,
- op: operator,
- pattern: pattern,
- loc: location,
- cmts: comments
- }.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(
- keyword: keyword,
- 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(constant:, superclass:, bodystmt:, location:, comments: [])
- @constant = constant
- @superclass = superclass
- @bodystmt = bodystmt
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [constant, superclass, bodystmt]
- end
-
- def format(q)
- declaration = -> do
- q.group do
- q.text("class ")
- q.format(constant)
-
- if superclass
- q.text(" < ")
- q.format(superclass)
- end
- end
- end
-
- if bodystmt.empty?
- q.group do
- declaration.call
- q.breakable(force: true)
- q.text("end")
- end
- else
- q.group do
- declaration.call
-
- q.indent do
- q.breakable(force: true)
- q.format(bodystmt)
- end
-
- q.breakable(force: true)
- q.text("end")
- end
- end
- 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)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :class,
- constant: constant,
- superclass: superclass,
- bodystmt: bodystmt,
- loc: location,
- cmts: comments
- }.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] the arguments being sent with the message
- attr_reader :arguments
-
- # [Location] the location of this node
- attr_reader :location
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(message:, arguments:, location:, comments: [])
- @message = message
- @arguments = arguments
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [message, arguments]
- end
-
- def format(q)
- q.group do
- q.format(message)
- q.text(" ")
- q.nest(message.value.length + 1) { q.format(arguments) }
- end
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("command")
-
- q.breakable
- q.pp(message)
-
- q.breakable
- q.pp(arguments)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :command,
- message: message,
- args: arguments,
- loc: location,
- cmts: comments
- }.to_json(*opts)
- end
- end
-
- # :call-seq:
- # on_command: ((Const | Ident) message, Args 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
-
- # [nil | Args] the arguments going along with the message
- attr_reader :arguments
-
- # [Location] the location of this node
- attr_reader :location
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(
- receiver:,
- operator:,
- message:,
- arguments:,
- location:,
- comments: []
- )
- @receiver = receiver
- @operator = operator
- @message = message
- @arguments = arguments
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [receiver, message, arguments]
- end
-
- def format(q)
- q.group do
- doc =
- q.nest(0) do
- q.format(receiver)
- q.format(CallOperatorFormatter.new(operator), stackable: false)
- q.format(message)
- end
-
- if arguments
- q.text(" ")
- q.nest(argument_alignment(q, doc)) { q.format(arguments) }
- end
- end
- 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)
-
- if arguments
- q.breakable
- q.pp(arguments)
- end
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :command_call,
- receiver: receiver,
- op: operator,
- message: message,
- args: arguments,
- loc: location,
- cmts: comments
- }.to_json(*opts)
- end
-
- private
-
- # This is a somewhat naive method that is attempting to sum up the width of
- # the doc nodes that make up the given doc node. This is used to align
- # content.
- def doc_width(parent)
- queue = [parent]
- width = 0
-
- until queue.empty?
- doc = queue.shift
-
- case doc
- when PrettyPrint::Text
- width += doc.width
- when PrettyPrint::Indent, PrettyPrint::Align, PrettyPrint::Group
- queue = doc.contents + queue
- when PrettyPrint::IfBreak
- queue = doc.break_contents + queue
- when PrettyPrint::Breakable
- width = 0
- end
- end
-
- width
- end
-
- def argument_alignment(q, doc)
- # Very special handling case for rspec matchers. In general with rspec
- # matchers you expect to see something like:
- #
- # expect(foo).to receive(:bar).with(
- # 'one',
- # 'two',
- # 'three',
- # 'four',
- # 'five'
- # )
- #
- # In this case the arguments are aligned to the left side as opposed to
- # being aligned with the `receive` call.
- if %w[to not_to to_not].include?(message.value)
- 0
- else
- width = doc_width(doc) + 1
- width > (q.maxwidth / 2) ? 0 : width
- end
- end
- end
-
- # :call-seq:
- # on_command_call: (
- # untyped receiver,
- # (:"::" | Op | Period) operator,
- # (Const | Ident | Op) message,
- # (nil | Args) 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
- class List
- # [Array[ Comment ]] the list of comments this list represents
- attr_reader :comments
-
- def initialize(comments)
- @comments = comments
- end
-
- def pretty_print(q)
- return if comments.empty?
-
- q.breakable
- q.group(2, "(", ")") { q.seplist(comments) { |comment| q.pp(comment) } }
- end
- end
-
- # [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
-
- @leading = false
- @trailing = false
- end
-
- def leading!
- @leading = true
- end
-
- def leading?
- @leading
- end
-
- def trailing!
- @trailing = true
- end
-
- def trailing?
- @trailing
- end
-
- def ignore?
- value[1..-1].strip == "stree-ignore"
- end
-
- def comments
- []
- end
-
- def child_nodes
- []
- end
-
- def format(q)
- q.text(value)
- 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.chomp,
- inline: value.strip != lines[line - 1].strip,
- 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:, comments: [])
- @value = value
- @location = location
- @comments = comments
- end
-
- def child_nodes
- []
- end
-
- def format(q)
- q.text(value)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("const")
-
- q.breakable
- q.pp(value)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :const, value: value, loc: location, cmts: comments }.to_json(
- *opts
- )
- end
- end
-
- # :call-seq:
- # on_const: (String value) -> Const
- def on_const(value)
- Const.new(
- value: value,
- location: Location.token(line: lineno, char: char_pos, size: value.size)
- )
- 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(parent:, constant:, location:, comments: [])
- @parent = parent
- @constant = constant
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [parent, constant]
- end
-
- def format(q)
- q.format(parent)
- q.text("::")
- q.format(constant)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("const_path_field")
-
- q.breakable
- q.pp(parent)
-
- q.breakable
- q.pp(constant)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :const_path_field,
- parent: parent,
- constant: constant,
- loc: location,
- cmts: comments
- }.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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(parent:, constant:, location:, comments: [])
- @parent = parent
- @constant = constant
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [parent, constant]
- end
-
- def format(q)
- q.format(parent)
- q.text("::")
- q.format(constant)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("const_path_ref")
-
- q.breakable
- q.pp(parent)
-
- q.breakable
- q.pp(constant)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :const_path_ref,
- parent: parent,
- constant: constant,
- loc: location,
- cmts: comments
- }.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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(constant:, location:, comments: [])
- @constant = constant
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [constant]
- end
-
- def format(q)
- q.format(constant)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("const_ref")
-
- q.breakable
- q.pp(constant)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :const_ref,
- constant: constant,
- loc: location,
- cmts: comments
- }.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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:, comments: [])
- @value = value
- @location = location
- @comments = comments
- end
-
- def child_nodes
- []
- end
-
- def format(q)
- q.text(value)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("cvar")
-
- q.breakable
- q.pp(value)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :cvar, value: value, loc: location, cmts: comments }.to_json(
- *opts
- )
- end
- end
-
- # :call-seq:
- # on_cvar: (String value) -> CVar
- def on_cvar(value)
- CVar.new(
- value: value,
- location: Location.token(line: lineno, char: char_pos, size: value.size)
- )
- 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(name:, params:, bodystmt:, location:, comments: [])
- @name = name
- @params = params
- @bodystmt = bodystmt
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [name, params, bodystmt]
- end
-
- def format(q)
- q.group do
- q.group do
- q.text("def ")
- q.format(name)
- q.format(params) if !params.is_a?(Params) || !params.empty?
- end
-
- unless bodystmt.empty?
- q.indent do
- q.breakable(force: true)
- q.format(bodystmt)
- end
- end
-
- q.breakable(force: true)
- q.text("end")
- end
- 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)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :def,
- name: name,
- params: params,
- bodystmt: bodystmt,
- loc: location,
- cmts: comments
- }.to_json(*opts)
- end
- end
-
- # DefEndless represents defining a single-line method since Ruby 3.0+.
- #
- # def method = result
- #
- class DefEndless
- # [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
-
- # [nil | Params | 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(
- target:,
- operator:,
- name:,
- paren:,
- statement:,
- location:,
- comments: []
- )
- @target = target
- @operator = operator
- @name = name
- @paren = paren
- @statement = statement
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [target, operator, name, paren, statement]
- end
-
- def format(q)
- q.group do
- q.text("def ")
-
- if target
- q.format(target)
- q.format(CallOperatorFormatter.new(operator), stackable: false)
- end
-
- q.format(name)
-
- if paren
- params = paren
- params = params.contents if params.is_a?(Paren)
- q.format(paren) unless params.empty?
- end
-
- q.text(" =")
- q.group do
- q.indent do
- q.breakable
- q.format(statement)
- end
- end
- end
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("def_endless")
-
- if target
- q.breakable
- q.pp(target)
-
- q.breakable
- q.pp(operator)
- end
-
- q.breakable
- q.pp(name)
-
- if paren
- q.breakable
- q.pp(paren)
- end
-
- q.breakable
- q.pp(statement)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :def_endless,
- name: name,
- paren: paren,
- stmt: statement,
- loc: location,
- cmts: comments
- }.to_json(*opts)
- end
- end
-
- # :call-seq:
- # on_def: (
- # (Backtick | Const | Ident | Kw | Op) name,
- # (nil | 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 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", consume: false)
-
- if ending
- tokens.delete(ending)
- 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)
- )
- else
- # In Ruby >= 3.1.0, this is a BodyStmt that wraps a single statement in
- # the statements list. Before, it was just the individual statement.
- statement = bodystmt.is_a?(BodyStmt) ? bodystmt.statements : bodystmt
-
- DefEndless.new(
- target: nil,
- operator: nil,
- name: name,
- paren: params,
- statement: statement,
- location: beginning.location.to(bodystmt.location)
- )
- end
- 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:, comments: [])
- @value = value
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [value]
- end
-
- def format(q)
- q.group(0, "defined?(", ")") do
- q.indent do
- q.breakable("")
- q.format(value)
- end
- q.breakable("")
- end
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("defined")
-
- q.breakable
- q.pp(value)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :defined, value: value, loc: location, cmts: comments }.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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(
- target:,
- operator:,
- name:,
- params:,
- bodystmt:,
- location:,
- comments: []
- )
- @target = target
- @operator = operator
- @name = name
- @params = params
- @bodystmt = bodystmt
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [target, operator, name, params, bodystmt]
- end
-
- def format(q)
- q.group do
- q.group do
- q.text("def ")
- q.format(target)
- q.format(CallOperatorFormatter.new(operator), stackable: false)
- q.format(name)
- q.format(params) if !params.is_a?(Params) || !params.empty?
- end
-
- unless bodystmt.empty?
- q.indent do
- q.breakable(force: true)
- q.format(bodystmt)
- end
- end
-
- q.breakable(force: true)
- q.text("end")
- end
- 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)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :defs,
- target: target,
- op: operator,
- name: name,
- params: params,
- bodystmt: bodystmt,
- loc: location,
- cmts: comments
- }.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", consume: false)
-
- if ending
- tokens.delete(ending)
- 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)
- )
- else
- # In Ruby >= 3.1.0, this is a BodyStmt that wraps a single statement in
- # the statements list. Before, it was just the individual statement.
- statement = bodystmt.is_a?(BodyStmt) ? bodystmt.statements : bodystmt
-
- DefEndless.new(
- target: target,
- operator: operator,
- name: name,
- paren: params,
- statement: statement,
- location: beginning.location.to(bodystmt.location)
- )
- end
- 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(keyword:, block_var:, bodystmt:, location:, comments: [])
- @keyword = keyword
- @block_var = block_var
- @bodystmt = bodystmt
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [keyword, block_var, bodystmt]
- end
-
- def format(q)
- BlockFormatter.new(self, keyword, "end", bodystmt).format(q)
- 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)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :do_block,
- keyword: keyword,
- block_var: block_var,
- bodystmt: bodystmt,
- loc: location,
- cmts: comments
- }.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
-
- # Responsible for formatting Dot2 and Dot3 nodes.
- class DotFormatter
- # [String] the operator to display
- attr_reader :operator
-
- # [Dot2 | Dot3] the node that is being formatter
- attr_reader :node
-
- def initialize(operator, node)
- @operator = operator
- @node = node
- end
-
- def format(q)
- space = [If, IfMod, Unless, UnlessMod].include?(q.parent.class)
-
- left = node.left
- right = node.right
-
- q.format(left) if left
- q.text(" ") if space
- q.text(operator)
- q.text(" ") if space
- q.format(right) if right
- end
- 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(left:, right:, location:, comments: [])
- @left = left
- @right = right
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [left, right]
- end
-
- def format(q)
- DotFormatter.new("..", self).format(q)
- 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
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :dot2,
- left: left,
- right: right,
- loc: location,
- cmts: comments
- }.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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(left:, right:, location:, comments: [])
- @left = left
- @right = right
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [left, right]
- end
-
- def format(q)
- DotFormatter.new("...", self).format(q)
- 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
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :dot3,
- left: left,
- right: right,
- loc: location,
- cmts: comments
- }.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
-
- # Responsible for providing information about quotes to be used for strings
- # and dynamic symbols.
- module Quotes
- # The matching pairs of quotes that can be used with % literals.
- PAIRS = { "(" => ")", "[" => "]", "{" => "}", "<" => ">" }.freeze
-
- # If there is some part of this string that matches an escape sequence or
- # that contains the interpolation pattern ("#{"), then we are locked into
- # whichever quote the user chose. (If they chose single quotes, then double
- # quoting would activate the escape sequence, and if they chose double
- # quotes, then single quotes would deactivate it.)
- def self.locked?(node)
- node.parts.any? do |part|
- part.is_a?(TStringContent) && part.value.match?(/#[@${]|[\\]/)
- end
- end
-
- # Find the matching closing quote for the given opening quote.
- def self.matching(quote)
- PAIRS.fetch(quote) { quote }
- end
-
- # Escape and unescape single and double quotes as needed to be able to
- # enclose +content+ with +enclosing+.
- def self.normalize(content, enclosing)
- return content if enclosing != "\"" && enclosing != "'"
-
- content.gsub(/\\([\s\S])|(['"])/) do
- _match, escaped, quote = Regexp.last_match.to_a
-
- if quote == enclosing
- "\\#{quote}"
- elsif quote
- quote
- else
- "\\#{escaped}"
- end
- end
- end
- end
-
- # DynaSymbol represents a symbol literal that uses quotes to dynamically
- # define its value.
- #
- # :"#{variable}"
- #
- # They can also be used as a special kind of dynamic hash key, as in:
- #
- # { "#{key}": value }
- #
- class DynaSymbol
- # [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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(parts:, quote:, location:, comments: [])
- @parts = parts
- @quote = quote
- @location = location
- @comments = comments
- end
-
- def child_nodes
- parts
- end
-
- def format(q)
- opening_quote, closing_quote = quotes(q)
-
- q.group(0, opening_quote, closing_quote) do
- parts.each do |part|
- if part.is_a?(TStringContent)
- value = Quotes.normalize(part.value, closing_quote)
- separator = -> { q.breakable(force: true, indent: false) }
- q.seplist(value.split(/\r?\n/, -1), separator) do |text|
- q.text(text)
- end
- else
- q.format(part)
- end
- end
- end
- 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) } }
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :dyna_symbol,
- parts: parts,
- quote: quote,
- loc: location,
- cmts: comments
- }.to_json(*opts)
- end
-
- private
-
- # Here we determine the quotes to use for a dynamic symbol. It's bound by a
- # lot of rules because it could be in many different contexts with many
- # different kinds of escaping.
- def quotes(q)
- # If we're inside of an assoc node as the key, then it will handle
- # printing the : on its own since it could change sides.
- parent = q.parent
- hash_key = parent.is_a?(Assoc) && parent.key == self
-
- if quote.start_with?("%s")
- # Here we're going to check if there is a closing character, a new line,
- # or a quote in the content of the dyna symbol. If there is, then
- # quoting could get weird, so just bail out and stick to the original
- # quotes in the source.
- matching = Quotes.matching(quote[2])
- pattern = /[\n#{Regexp.escape(matching)}'"]/
-
- if parts.any? { |part|
- part.is_a?(TStringContent) && part.value.match?(pattern)
- }
- [quote, matching]
- elsif Quotes.locked?(self)
- ["#{":" unless hash_key}'", "'"]
- else
- ["#{":" unless hash_key}#{q.quote}", q.quote]
- end
- elsif Quotes.locked?(self)
- if quote.start_with?(":")
- [hash_key ? quote[1..-1] : quote, quote[1..-1]]
- else
- [hash_key ? quote : ":#{quote}", quote]
- end
- else
- [hash_key ? q.quote : ":#{q.quote}", q.quote]
- end
- 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, location: symbeg.location)
-
- 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(statements:, location:, comments: [])
- @statements = statements
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [statements]
- end
-
- def format(q)
- q.group do
- q.text("else")
-
- unless statements.empty?
- q.indent do
- q.breakable(force: true)
- q.format(statements)
- end
- end
- end
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("else")
-
- q.breakable
- q.pp(statements)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :else, stmts: statements, loc: location, cmts: comments }.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
- # ending = 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(
- predicate:,
- statements:,
- consequent:,
- location:,
- comments: []
- )
- @predicate = predicate
- @statements = statements
- @consequent = consequent
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [predicate, statements, consequent]
- end
-
- def format(q)
- q.group do
- q.group do
- q.text("elsif ")
- q.nest("elsif".length - 1) { q.format(predicate) }
- end
-
- unless statements.empty?
- q.indent do
- q.breakable(force: true)
- q.format(statements)
- end
- end
-
- if consequent
- q.group do
- q.breakable(force: true)
- q.format(consequent)
- end
- end
- end
- 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
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :elsif,
- pred: predicate,
- stmts: statements,
- cons: consequent,
- loc: location,
- cmts: comments
- }.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 ignore?
- false
- end
-
- def comments
- []
- end
-
- def child_nodes
- []
- end
-
- def format(q)
- q.trim
- q.text(value)
- 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(keyword:, statements:, location:, comments: [])
- @keyword = keyword
- @statements = statements
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [keyword, statements]
- end
-
- def format(q)
- q.format(keyword)
-
- unless statements.empty?
- q.indent do
- q.breakable(force: true)
- q.format(statements)
- end
- end
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("ensure")
-
- q.breakable
- q.pp(statements)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :ensure,
- keyword: keyword,
- stmts: statements,
- loc: location,
- cmts: comments
- }.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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:, comments: [])
- @value = value
- @location = location
- @comments = comments
- end
-
- def child_nodes
- []
- end
-
- def format(q)
- q.text(value)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("excessed_comma")
-
- q.breakable
- q.pp(value)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :excessed_comma,
- value: value,
- loc: location,
- cmts: comments
- }.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
-
- # [nil | ArgParen | Args] the arguments to the method call
- attr_reader :arguments
-
- # [Location] the location of this node
- attr_reader :location
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, arguments:, location:, comments: [])
- @value = value
- @arguments = arguments
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [value, arguments]
- end
-
- def format(q)
- q.format(value)
- q.format(arguments)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("fcall")
-
- q.breakable
- q.pp(value)
-
- if arguments
- q.breakable
- q.pp(arguments)
- end
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :fcall,
- value: value,
- args: arguments,
- loc: location,
- cmts: comments
- }.to_json(*opts)
- end
- end
-
- # :call-seq:
- # on_fcall: ((Const | Ident) value) -> FCall
- def on_fcall(value)
- FCall.new(value: value, arguments: nil, 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(parent:, operator:, name:, location:, comments: [])
- @parent = parent
- @operator = operator
- @name = name
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [parent, (operator if operator != :"::"), name]
- end
-
- def format(q)
- q.group do
- q.format(parent)
- q.format(CallOperatorFormatter.new(operator), stackable: false)
- q.format(name)
- end
- 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)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :field,
- parent: parent,
- op: operator,
- name: name,
- loc: location,
- cmts: comments
- }.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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:, comments: [])
- @value = value
- @location = location
- @comments = comments
- end
-
- def child_nodes
- []
- end
-
- def format(q)
- q.text(value)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("float")
-
- q.breakable
- q.pp(value)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :float, value: value, loc: location, cmts: comments }.to_json(
- *opts
- )
- end
- end
-
- # :call-seq:
- # on_float: (String value) -> FloatLiteral
- def on_float(value)
- FloatLiteral.new(
- value: value,
- location: Location.token(line: lineno, char: char_pos, size: value.size)
- )
- 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(constant:, left:, values:, right:, location:, comments: [])
- @constant = constant
- @left = left
- @values = values
- @right = right
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [constant, left, *values, right]
- end
-
- def format(q)
- q.format(constant) if constant
- q.group(0, "[", "]") do
- q.text("*")
- q.format(left)
- q.comma_breakable
-
- q.seplist(values) { |value| q.format(value) }
- q.comma_breakable
-
- q.text("*")
- q.format(right)
- end
- 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)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :fndptn,
- constant: constant,
- left: left,
- values: values,
- right: right,
- loc: location,
- cmts: comments
- }.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 | 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(index:, collection:, statements:, location:, comments: [])
- @index = index
- @collection = collection
- @statements = statements
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [index, collection, statements]
- end
-
- def format(q)
- q.group do
- q.text("for ")
- q.group { q.format(index) }
- q.text(" in ")
- q.format(collection)
-
- unless statements.empty?
- q.indent do
- q.breakable(force: true)
- q.format(statements)
- end
- end
-
- q.breakable(force: true)
- q.text("end")
- end
- 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)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :for,
- index: index,
- collection: collection,
- stmts: statements,
- loc: location,
- cmts: comments
- }.to_json(*opts)
- end
- end
-
- # :call-seq:
- # on_for: (
- # (MLHS | VarField) value,
- # untyped collection,
- # Statements statements
- # ) -> For
- def on_for(index, collection, statements)
- beginning = find_token(Kw, "for")
- in_keyword = find_token(Kw, "in")
- 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
- )
-
- if index.is_a?(MLHS)
- comma_range = index.location.end_char...in_keyword.location.start_char
- index.comma = true if source[comma_range].strip.start_with?(",")
- end
-
- 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:, comments: [])
- @value = value
- @location = location
- @comments = comments
- end
-
- def child_nodes
- []
- end
-
- def format(q)
- q.text(value)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("gvar")
-
- q.breakable
- q.pp(value)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :gvar, value: value, loc: location, cmts: comments }.to_json(
- *opts
- )
- end
- end
-
- # :call-seq:
- # on_gvar: (String value) -> GVar
- def on_gvar(value)
- GVar.new(
- value: value,
- location: Location.token(line: lineno, char: char_pos, size: value.size)
- )
- end
-
- # HashLiteral represents a hash literal.
- #
- # { key => value }
- #
- class HashLiteral
- # [LBrace] the left brace that opens this hash
- attr_reader :lbrace
-
- # [Array[ AssocNew | AssocSplat ]] the optional contents of the hash
- attr_reader :assocs
-
- # [Location] the location of this node
- attr_reader :location
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(lbrace:, assocs:, location:, comments: [])
- @lbrace = lbrace
- @assocs = assocs
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [lbrace] + assocs
- end
-
- def format(q)
- if q.parent.is_a?(Assoc)
- format_contents(q)
- else
- q.group { format_contents(q) }
- end
- end
-
- def format_key(q, key)
- (@key_formatter ||= HashKeyFormatter.for(self)).format_key(q, key)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("hash")
-
- if assocs.any?
- q.breakable
- q.group(2, "(", ")") { q.seplist(assocs) { |assoc| q.pp(assoc) } }
- end
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :hash, assocs: assocs, loc: location, cmts: comments }.to_json(
- *opts
- )
- end
-
- private
-
- def format_contents(q)
- q.format(lbrace)
-
- if assocs.empty?
- q.breakable("")
- else
- q.indent do
- q.breakable
- q.seplist(assocs) { |assoc| q.format(assoc) }
- end
- q.breakable
- end
-
- q.text("}")
- end
- end
-
- # :call-seq:
- # on_hash: ((nil | Array[AssocNew | AssocSplat]) assocs) -> HashLiteral
- def on_hash(assocs)
- lbrace = find_token(LBrace)
- rbrace = find_token(RBrace)
-
- HashLiteral.new(
- lbrace: lbrace,
- assocs: assocs || [],
- 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(beginning:, ending: nil, parts: [], location:, comments: [])
- @beginning = beginning
- @ending = ending
- @parts = parts
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [beginning, *parts]
- end
-
- def format(q)
- # This is a very specific behavior that should probably be included in the
- # prettyprint module. It's when you want to force a newline, but don't
- # want to force the break parent.
- breakable = -> do
- q.target <<
- PrettyPrint::Breakable.new(" ", 1, indent: false, force: true)
- end
-
- q.group do
- q.format(beginning)
-
- q.line_suffix(priority: Formatter::HEREDOC_PRIORITY) do
- q.group do
- breakable.call
-
- parts.each do |part|
- if part.is_a?(TStringContent)
- texts = part.value.split(/\r?\n/, -1)
- q.seplist(texts, breakable) { |text| q.text(text) }
- else
- q.format(part)
- end
- end
-
- q.text(ending)
- end
- end
- end
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("heredoc")
-
- q.breakable
- q.group(2, "(", ")") { q.seplist(parts) { |part| q.pp(part) } }
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :heredoc,
- beging: beginning,
- ending: ending,
- parts: parts,
- loc: location,
- cmts: comments
- }.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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:, comments: [])
- @value = value
- @location = location
- @comments = comments
- end
-
- def child_nodes
- []
- end
-
- def format(q)
- q.text(value)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("heredoc_beg")
-
- q.breakable
- q.pp(value)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :heredoc_beg,
- value: value,
- loc: location,
- cmts: comments
- }.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
- class KeywordFormatter
- # [Label] the keyword being used
- attr_reader :key
-
- # [untyped] the optional value for the keyword
- attr_reader :value
-
- def initialize(key, value)
- @key = key
- @value = value
- end
-
- def comments
- []
- end
-
- def format(q)
- q.format(key)
-
- if value
- q.text(" ")
- q.format(value)
- end
- end
- end
-
- class KeywordRestFormatter
- # [VarField] the parameter that matches the remaining keywords
- attr_reader :keyword_rest
-
- def initialize(keyword_rest)
- @keyword_rest = keyword_rest
- end
-
- def comments
- []
- end
-
- def format(q)
- q.text("**")
- q.format(keyword_rest)
- end
- end
-
- # [nil | 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(constant:, keywords:, keyword_rest:, location:, comments: [])
- @constant = constant
- @keywords = keywords
- @keyword_rest = keyword_rest
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [constant, *keywords.flatten(1), keyword_rest]
- end
-
- def format(q)
- parts = keywords.map { |(key, value)| KeywordFormatter.new(key, value) }
- parts << KeywordRestFormatter.new(keyword_rest) if keyword_rest
- contents = -> do
- q.seplist(parts) { |part| q.format(part, stackable: false) }
- end
-
- if constant
- q.format(constant)
- q.text("[")
- contents.call
- q.text("]")
- return
- end
-
- parent = q.parent
- if PATTERNS.include?(parent.class)
- q.text("{ ")
- contents.call
- q.text(" }")
- else
- contents.call
- end
- 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) do |(key, value)|
- q.group(2, "(", ")") do
- q.pp(key)
-
- if value
- q.breakable
- q.pp(value)
- end
- end
- end
- end
- end
-
- if keyword_rest
- q.breakable
- q.pp(keyword_rest)
- end
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :hshptn,
- constant: constant,
- keywords: keywords,
- kwrest: keyword_rest,
- loc: location,
- cmts: comments
- }.to_json(*opts)
- end
- end
-
- # The list of nodes that represent patterns inside of pattern matching so that
- # when a pattern is being printed it knows if it's nested.
- PATTERNS = [AryPtn, Binary, FndPtn, HshPtn, RAssign]
-
- # :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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:, comments: [])
- @value = value
- @location = location
- @comments = comments
- end
-
- def child_nodes
- []
- end
-
- def format(q)
- q.text(value)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("ident")
-
- q.breakable
- q.pp(value)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :ident,
- value: value.force_encoding("UTF-8"),
- loc: location,
- cmts: comments
- }.to_json(*opts)
- end
- end
-
- # :call-seq:
- # on_ident: (String value) -> Ident
- def on_ident(value)
- Ident.new(
- value: value,
- location: Location.token(line: lineno, char: char_pos, size: value.size)
- )
- end
-
- # If the predicate of a conditional or loop contains an assignment (in which
- # case we can't know for certain that that assignment doesn't impact the
- # statements inside the conditional) then we can't use the modifier form
- # and we must use the block form.
- module ContainsAssignment
- def self.call(parent)
- queue = [parent]
-
- while node = queue.shift
- return true if [Assign, MAssign, OpAssign].include?(node.class)
- queue += node.child_nodes
- end
-
- false
- end
- end
-
- # Formats an If or Unless node.
- class ConditionalFormatter
- # [String] the keyword associated with this conditional
- attr_reader :keyword
-
- # [If | Unless] the node that is being formatted
- attr_reader :node
-
- def initialize(keyword, node)
- @keyword = keyword
- @node = node
- end
-
- def format(q)
- # If the predicate of the conditional contains an assignment (in which
- # case we can't know for certain that that assignment doesn't impact the
- # statements inside the conditional) then we can't use the modifier form
- # and we must use the block form.
- if ContainsAssignment.call(node.predicate)
- format_break(q, force: true)
- return
- end
-
- if node.consequent || node.statements.empty?
- q.group { format_break(q, force: true) }
- else
- q.group do
- q.if_break { format_break(q, force: false) }.if_flat do
- Parentheses.flat(q) do
- q.format(node.statements)
- q.text(" #{keyword} ")
- q.format(node.predicate)
- end
- end
- end
- end
- end
-
- private
-
- def format_break(q, force:)
- q.text("#{keyword} ")
- q.nest(keyword.length + 1) { q.format(node.predicate) }
-
- unless node.statements.empty?
- q.indent do
- q.breakable(force: force)
- q.format(node.statements)
- end
- end
-
- if node.consequent
- q.breakable(force: force)
- q.format(node.consequent)
- end
-
- q.breakable(force: force)
- q.text("end")
- end
- 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(
- predicate:,
- statements:,
- consequent:,
- location:,
- comments: []
- )
- @predicate = predicate
- @statements = statements
- @consequent = consequent
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [predicate, statements, consequent]
- end
-
- def format(q)
- ConditionalFormatter.new("if", self).format(q)
- 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
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :if,
- pred: predicate,
- stmts: statements,
- cons: consequent,
- loc: location,
- cmts: comments
- }.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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(predicate:, truthy:, falsy:, location:, comments: [])
- @predicate = predicate
- @truthy = truthy
- @falsy = falsy
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [predicate, truthy, falsy]
- end
-
- def format(q)
- force_flat = [
- Alias, Assign, Break, Command, CommandCall, Heredoc, If, IfMod, IfOp,
- Lambda, MAssign, Next, OpAssign, RescueMod, Return, Return0, Super,
- Undef, Unless, UnlessMod, UntilMod, VarAlias, VoidStmt, WhileMod, Yield,
- Yield0, ZSuper
- ]
-
- if force_flat.include?(truthy.class) || force_flat.include?(falsy.class)
- q.group { format_flat(q) }
- return
- end
-
- q.group { q.if_break { format_break(q) }.if_flat { format_flat(q) } }
- end
-
- def 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)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :ifop,
- pred: predicate,
- tthy: truthy,
- flsy: falsy,
- loc: location,
- cmts: comments
- }.to_json(*opts)
- end
-
- private
-
- def format_break(q)
- Parentheses.break(q) do
- q.text("if ")
- q.nest("if ".length) { q.format(predicate) }
-
- q.indent do
- q.breakable
- q.format(truthy)
- end
-
- q.breakable
- q.text("else")
-
- q.indent do
- q.breakable
- q.format(falsy)
- end
-
- q.breakable
- q.text("end")
- end
- end
-
- def format_flat(q)
- q.format(predicate)
- q.text(" ?")
-
- q.breakable
- q.format(truthy)
- q.text(" :")
-
- q.breakable
- q.format(falsy)
- 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
-
- # Formats an IfMod or UnlessMod node.
- class ConditionalModFormatter
- # [String] the keyword associated with this conditional
- attr_reader :keyword
-
- # [IfMod | UnlessMod] the node that is being formatted
- attr_reader :node
-
- def initialize(keyword, node)
- @keyword = keyword
- @node = node
- end
-
- def format(q)
- if ContainsAssignment.call(node.statement)
- q.group { format_flat(q) }
- else
- q.group { q.if_break { format_break(q) }.if_flat { format_flat(q) } }
- end
- end
-
- private
-
- def format_break(q)
- q.text("#{keyword} ")
- q.nest(keyword.length + 1) { q.format(node.predicate) }
- q.indent do
- q.breakable
- q.format(node.statement)
- end
- q.breakable
- q.text("end")
- end
-
- def format_flat(q)
- Parentheses.flat(q) do
- q.format(node.statement)
- q.text(" #{keyword} ")
- q.format(node.predicate)
- end
- end
- 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(statement:, predicate:, location:, comments: [])
- @statement = statement
- @predicate = predicate
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [statement, predicate]
- end
-
- def format(q)
- ConditionalModFormatter.new("if", self).format(q)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("if_mod")
-
- q.breakable
- q.pp(statement)
-
- q.breakable
- q.pp(predicate)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :if_mod,
- stmt: statement,
- pred: predicate,
- loc: location,
- cmts: comments
- }.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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:, comments: [])
- @value = value
- @location = location
- @comments = comments
- end
-
- def child_nodes
- []
- end
-
- def format(q)
- q.text(value)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("imaginary")
-
- q.breakable
- q.pp(value)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :imaginary, value: value, loc: location, cmts: comments }.to_json(
- *opts
- )
- end
- end
-
- # :call-seq:
- # on_imaginary: (String value) -> Imaginary
- def on_imaginary(value)
- Imaginary.new(
- value: value,
- location: Location.token(line: lineno, char: char_pos, size: value.size)
- )
- 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(pattern:, statements:, consequent:, location:, comments: [])
- @pattern = pattern
- @statements = statements
- @consequent = consequent
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [pattern, statements, consequent]
- end
-
- def format(q)
- keyword = "in "
-
- q.group do
- q.text(keyword)
- q.nest(keyword.length) { q.format(pattern) }
-
- unless statements.empty?
- q.indent do
- q.breakable(force: true)
- q.format(statements)
- end
- end
-
- if consequent
- q.breakable(force: true)
- q.format(consequent)
- end
- end
- 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
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :in,
- pattern: pattern,
- stmts: statements,
- cons: consequent,
- loc: location,
- cmts: comments
- }.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_start = pattern
- if token = find_token(Kw, "then", consume: false)
- tokens.delete(token)
- statements_start = token
- end
-
- statements.bind(
- find_next_statement_start(statements_start.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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:, comments: [])
- @value = value
- @location = location
- @comments = comments
- end
-
- def child_nodes
- []
- end
-
- def format(q)
- if !value.start_with?(/\+?0/) && value.length >= 5 && !value.include?("_")
- # If it's a plain integer and it doesn't have any underscores separating
- # the values, then we're going to insert them every 3 characters
- # starting from the right.
- index = (value.length + 2) % 3
- q.text(" #{value}"[index..-1].scan(/.../).join("_").strip)
- else
- q.text(value)
- end
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("int")
-
- q.breakable
- q.pp(value)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :int, value: value, loc: location, cmts: comments }.to_json(*opts)
- end
- end
-
- # :call-seq:
- # on_int: (String value) -> Int
- def on_int(value)
- Int.new(
- value: value,
- location: Location.token(line: lineno, char: char_pos, size: value.size)
- )
- 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:, comments: [])
- @value = value
- @location = location
- @comments = comments
- end
-
- def child_nodes
- []
- end
-
- def format(q)
- q.text(value)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("ivar")
-
- q.breakable
- q.pp(value)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :ivar, value: value, loc: location, cmts: comments }.to_json(
- *opts
- )
- end
- end
-
- # :call-seq:
- # on_ivar: (String value) -> IVar
- def on_ivar(value)
- IVar.new(
- value: value,
- location: Location.token(line: lineno, char: char_pos, size: value.size)
- )
- 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:, comments: [])
- @value = value
- @location = location
- @comments = comments
- end
-
- def child_nodes
- []
- end
-
- def format(q)
- q.text(value)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("kw")
-
- q.breakable
- q.pp(value)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :kw, value: value, loc: location, cmts: comments }.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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(name:, location:, comments: [])
- @name = name
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [name]
- end
-
- def format(q)
- q.text("**")
- q.format(name) if name
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("kwrest_param")
-
- q.breakable
- q.pp(name)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :kwrest_param,
- name: name,
- loc: location,
- cmts: comments
- }.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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:, comments: [])
- @value = value
- @location = location
- @comments = comments
- end
-
- def child_nodes
- []
- end
-
- def format(q)
- q.text(value)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("label")
-
- q.breakable
- q.text(":")
- q.text(value[0...-1])
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :label, value: value, loc: location, cmts: comments }.to_json(
- *opts
- )
- end
- end
-
- # :call-seq:
- # on_label: (String value) -> Label
- def on_label(value)
- Label.new(
- value: value,
- location: Location.token(line: lineno, char: char_pos, size: value.size)
- )
- 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(params:, statements:, location:, comments: [])
- @params = params
- @statements = statements
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [params, statements]
- end
-
- def format(q)
- q.group(0, "->") do
- if params.is_a?(Paren)
- q.format(params) unless params.contents.empty?
- elsif !params.empty?
- q.group do
- q.text("(")
- q.format(params)
- q.text(")")
- end
- end
-
- q.text(" ")
- q.if_break do
- force_parens =
- q.parents.any? do |node|
- node.is_a?(Command) || node.is_a?(CommandCall)
- end
-
- q.text(force_parens ? "{" : "do")
- q.indent do
- q.breakable
- q.format(statements)
- end
-
- q.breakable
- q.text(force_parens ? "}" : "end")
- end.if_flat do
- q.text("{ ")
- q.format(statements)
- q.text(" }")
- end
- end
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("lambda")
-
- q.breakable
- q.pp(params)
-
- q.breakable
- q.pp(statements)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :lambda,
- params: params,
- stmts: statements,
- loc: location,
- cmts: comments
- }.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 tokens.any? { |token|
- token.is_a?(TLamBeg) &&
- token.location.start_char > beginning.location.start_char
- }
- opening = find_token(TLamBeg)
- 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:, comments: [])
- @value = value
- @location = location
- @comments = comments
- end
-
- def child_nodes
- []
- end
-
- def format(q)
- q.text(value)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("lbrace")
-
- q.breakable
- q.pp(value)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :lbrace, value: value, loc: location, cmts: comments }.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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:, comments: [])
- @value = value
- @location = location
- @comments = comments
- end
-
- def child_nodes
- []
- end
-
- def format(q)
- q.text(value)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("lbracket")
-
- q.breakable
- q.pp(value)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :lbracket, value: value, loc: location, cmts: comments }.to_json(
- *opts
- )
- 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:, comments: [])
- @value = value
- @location = location
- @comments = comments
- end
-
- def child_nodes
- []
- end
-
- def format(q)
- q.text(value)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("lparen")
-
- q.breakable
- q.pp(value)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :lparen, value: value, loc: location, cmts: comments }.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 | 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(target:, value:, location:, comments: [])
- @target = target
- @value = value
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [target, value]
- end
-
- def format(q)
- q.group do
- q.group { q.format(target) }
- q.text(" =")
- q.indent do
- q.breakable
- q.format(value)
- end
- end
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("massign")
-
- q.breakable
- q.pp(target)
-
- q.breakable
- q.pp(value)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :massign,
- target: target,
- value: value,
- loc: location,
- cmts: comments
- }.to_json(*opts)
- end
- end
-
- # :call-seq:
- # on_massign: ((MLHS | 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
-
- # :call-seq:
- # on_method_add_arg: (
- # (Call | FCall) call,
- # (ArgParen | Args) arguments
- # ) -> Call | FCall
- def on_method_add_arg(call, arguments)
- location = call.location
- location = location.to(arguments.location) if arguments.is_a?(ArgParen)
-
- if call.is_a?(FCall)
- FCall.new(value: call.value, arguments: arguments, location: location)
- else
- Call.new(
- receiver: call.receiver,
- operator: call.operator,
- message: call.message,
- arguments: arguments,
- location: location
- )
- end
- end
-
- # MethodAddBlock represents a method call with a block argument.
- #
- # method {}
- #
- class MethodAddBlock
- # [Call | Command | CommandCall | FCall] 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(call:, block:, location:, comments: [])
- @call = call
- @block = block
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [call, block]
- end
-
- def format(q)
- q.format(call)
- q.format(block)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("method_add_block")
-
- q.breakable
- q.pp(call)
-
- q.breakable
- q.pp(block)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :method_add_block,
- call: call,
- block: block,
- loc: location,
- cmts: comments
- }.to_json(*opts)
- end
- end
-
- # :call-seq:
- # on_method_add_block: (
- # (Call | Command | CommandCall | FCall) 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 | ArgStar | 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(parts:, comma: false, location:, comments: [])
- @parts = parts
- @comma = comma
- @location = location
- @comments = comments
- end
-
- def child_nodes
- parts
- end
-
- def format(q)
- q.seplist(parts) { |part| q.format(part) }
- q.text(",") if comma
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("mlhs")
-
- q.breakable
- q.group(2, "(", ")") { q.seplist(parts) { |part| q.pp(part) } }
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :mlhs,
- parts: parts,
- comma: comma,
- loc: location,
- cmts: comments
- }.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)
- location =
- mlhs.parts.empty? ? part.location : mlhs.location.to(part.location)
-
- MLHS.new(parts: mlhs.parts << part, location: location)
- end
-
- # :call-seq:
- # on_mlhs_add_post: (MLHS left, MLHS right) -> MLHS
- def on_mlhs_add_post(left, right)
- MLHS.new(
- parts: left.parts + right.parts,
- location: left.location.to(right.location)
- )
- end
-
- # :call-seq:
- # on_mlhs_add_star: (
- # MLHS mlhs,
- # (nil | ARefField | Field | Ident | VarField) part
- # ) -> MLHS
- def on_mlhs_add_star(mlhs, part)
- beginning = find_token(Op, "*")
- ending = part || beginning
-
- location = beginning.location.to(ending.location)
- arg_star = ArgStar.new(value: part, location: location)
-
- location = mlhs.location.to(location) unless mlhs.parts.empty?
- MLHS.new(parts: mlhs.parts << arg_star, location: 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 | MLHSParen] the contents inside of the parentheses
- attr_reader :contents
-
- # [Location] the location of this node
- attr_reader :location
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(contents:, location:, comments: [])
- @contents = contents
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [contents]
- end
-
- def format(q)
- parent = q.parent
-
- if parent.is_a?(MAssign) || parent.is_a?(MLHSParen)
- q.format(contents)
- else
- q.group(0, "(", ")") do
- q.indent do
- q.breakable("")
- q.format(contents)
- end
-
- q.breakable("")
- end
- end
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("mlhs_paren")
-
- q.breakable
- q.pp(contents)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :mlhs_paren,
- cnts: contents,
- loc: location,
- cmts: comments
- }.to_json(*opts)
- end
- end
-
- # :call-seq:
- # on_mlhs_paren: ((MLHS | 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(constant:, bodystmt:, location:, comments: [])
- @constant = constant
- @bodystmt = bodystmt
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [constant, bodystmt]
- end
-
- def format(q)
- declaration = -> do
- q.group do
- q.text("module ")
- q.format(constant)
- end
- end
-
- if bodystmt.empty?
- q.group do
- declaration.call
- q.breakable(force: true)
- q.text("end")
- end
- else
- q.group do
- declaration.call
-
- q.indent do
- q.breakable(force: true)
- q.format(bodystmt)
- end
-
- q.breakable(force: true)
- q.text("end")
- end
- end
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("module")
-
- q.breakable
- q.pp(constant)
-
- q.breakable
- q.pp(bodystmt)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :module,
- constant: constant,
- bodystmt: bodystmt,
- loc: location,
- cmts: comments
- }.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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(parts:, location:, comments: [])
- @parts = parts
- @location = location
- @comments = comments
- end
-
- def child_nodes
- parts
- end
-
- def format(q)
- q.seplist(parts) { |part| q.format(part) }
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("mrhs")
-
- q.breakable
- q.group(2, "(", ")") { q.seplist(parts) { |part| q.pp(part) } }
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :mrhs, parts: parts, loc: location, cmts: comments }.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)
- location =
- if mrhs.parts.empty?
- mrhs.location
- else
- mrhs.location.to(part.location)
- end
-
- MRHS.new(parts: mrhs.parts << part, location: location)
- end
-
- # :call-seq:
- # on_mrhs_add_star: (MRHS mrhs, untyped value) -> MRHS
- def on_mrhs_add_star(mrhs, value)
- beginning = find_token(Op, "*")
- ending = value || beginning
-
- arg_star =
- ArgStar.new(
- value: value,
- location: beginning.location.to(ending.location)
- )
-
- location =
- if mrhs.parts.empty?
- arg_star.location
- else
- mrhs.location.to(arg_star.location)
- end
-
- MRHS.new(parts: mrhs.parts << arg_star, location: location)
- end
-
- # :call-seq:
- # on_mrhs_new_from_args: (Args arguments) -> MRHS
- def on_mrhs_new_from_args(arguments)
- MRHS.new(parts: arguments.parts, 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] the arguments passed to the next keyword
- attr_reader :arguments
-
- # [Location] the location of this node
- attr_reader :location
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(arguments:, location:, comments: [])
- @arguments = arguments
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [arguments]
- end
-
- def format(q)
- FlowControlFormatter.new("next", self).format(q)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("next")
-
- q.breakable
- q.pp(arguments)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :next, args: arguments, loc: location, cmts: comments }.to_json(
- *opts
- )
- end
- end
-
- # :call-seq:
- # on_next: (Args arguments) -> Next
- def on_next(arguments)
- keyword = find_token(Kw, "next")
-
- location = keyword.location
- location = location.to(arguments.location) if arguments.parts.any?
-
- 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:, comments: [])
- @value = value
- @location = location
- @comments = comments
- end
-
- def child_nodes
- []
- end
-
- def format(q)
- q.text(value)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("op")
-
- q.breakable
- q.pp(value)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :op, value: value, loc: location, cmts: comments }.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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(target:, operator:, value:, location:, comments: [])
- @target = target
- @operator = operator
- @value = value
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [target, operator, value]
- end
-
- def format(q)
- q.group do
- q.format(target)
- q.text(" ")
- q.format(operator)
- q.indent do
- q.breakable
- q.format(value)
- end
- end
- 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)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :opassign,
- target: target,
- op: operator,
- value: value,
- loc: location,
- cmts: comments
- }.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
-
- # If you have a modifier statement (for instance a modifier if statement or a
- # modifier while loop) there are times when you need to wrap the entire
- # statement in parentheses. This occurs when you have something like:
- #
- # foo[:foo] =
- # if bar?
- # baz
- # end
- #
- # Normally we would shorten this to an inline version, which would result in:
- #
- # foo[:foo] = baz if bar?
- #
- # but this actually has different semantic meaning. The first example will
- # result in a nil being inserted into the hash for the :foo key, whereas the
- # second example will result in an empty hash because the if statement applies
- # to the entire assignment.
- #
- # We can fix this in a couple of ways. We can use the then keyword, as in:
- #
- # foo[:foo] = if bar? then baz end
- #
- # But this isn't used very often. We can also just leave it as is with the
- # multi-line version, but for a short predicate and short value it looks
- # verbose. The last option and the one used here is to add parentheses on
- # both sides of the expression, as in:
- #
- # foo[:foo] = (baz if bar?)
- #
- # This approach maintains the nice conciseness of the inline version, while
- # keeping the correct semantic meaning.
- module Parentheses
- NODES = [Args, Assign, Assoc, Binary, Call, Defined, MAssign, OpAssign]
-
- def self.flat(q)
- return yield unless NODES.include?(q.parent.class)
-
- q.text("(")
- yield
- q.text(")")
- end
-
- def self.break(q)
- return yield unless NODES.include?(q.parent.class)
-
- q.text("(")
- q.indent do
- q.breakable("")
- yield
- end
- q.breakable("")
- q.text(")")
- end
- end
-
- # def on_operator_ambiguous(value)
- # value
- # end
-
- # Params represents defining parameters on a method or lambda.
- #
- # def method(param) end
- #
- class Params
- class OptionalFormatter
- # [Ident] the name of the parameter
- attr_reader :name
-
- # [untyped] the value of the parameter
- attr_reader :value
-
- def initialize(name, value)
- @name = name
- @value = value
- end
-
- def comments
- []
- end
-
- def format(q)
- q.format(name)
- q.text(" = ")
- q.format(value)
- end
- end
-
- class KeywordFormatter
- # [Ident] the name of the parameter
- attr_reader :name
-
- # [nil | untyped] the value of the parameter
- attr_reader :value
-
- def initialize(name, value)
- @name = name
- @value = value
- end
-
- def comments
- []
- end
-
- def format(q)
- q.format(name)
-
- if value
- q.text(" ")
- q.format(value)
- end
- end
- end
-
- class KeywordRestFormatter
- # [:nil | ArgsForward | KwRestParam] the value of the parameter
- attr_reader :value
-
- def initialize(value)
- @value = value
- end
-
- def comments
- []
- end
-
- def format(q)
- if value == :nil
- q.text("**nil")
- else
- q.format(value)
- end
- end
- end
-
- # [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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(
- requireds: [],
- optionals: [],
- rest: nil,
- posts: [],
- keywords: [],
- keyword_rest: nil,
- block: nil,
- location:,
- comments: []
- )
- @requireds = requireds
- @optionals = optionals
- @rest = rest
- @posts = posts
- @keywords = keywords
- @keyword_rest = keyword_rest
- @block = block
- @location = location
- @comments = comments
- end
-
- # Params nodes are the most complicated in the tree. Occasionally you want
- # to know if they are "empty", which means not having any parameters
- # declared. This logic accesses every kind of parameter and determines if
- # it's missing.
- def empty?
- requireds.empty? && optionals.empty? && !rest && posts.empty? &&
- keywords.empty? && !keyword_rest && !block
- end
-
- def child_nodes
- [
- *requireds,
- *optionals.flatten(1),
- rest,
- *posts,
- *keywords.flatten(1),
- (keyword_rest if keyword_rest != :nil),
- block
- ]
- end
-
- def format(q)
- parts = [
- *requireds,
- *optionals.map { |(name, value)| OptionalFormatter.new(name, value) }
- ]
-
- parts << rest if rest && !rest.is_a?(ExcessedComma)
- parts +=
- [
- *posts,
- *keywords.map { |(name, value)| KeywordFormatter.new(name, value) }
- ]
-
- parts << KeywordRestFormatter.new(keyword_rest) if keyword_rest
- parts << block if block
-
- contents = -> do
- q.seplist(parts) { |part| q.format(part) }
- q.format(rest) if rest && rest.is_a?(ExcessedComma)
- end
-
- if [Def, Defs, DefEndless].include?(q.parent.class)
- q.group(0, "(", ")") do
- q.indent do
- q.breakable("")
- contents.call
- end
- q.breakable("")
- end
- else
- q.nest(0, &contents)
- end
- 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
-
- q.pp(Comment::List.new(comments))
- 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,
- cmts: comments
- }.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 | ArgsForward | 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 if 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 if 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
-
- # [nil | untyped] the expression inside the parentheses
- attr_reader :contents
-
- # [Location] the location of this node
- attr_reader :location
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(lparen:, contents:, location:, comments: [])
- @lparen = lparen
- @contents = contents
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [lparen, contents]
- end
-
- def format(q)
- q.group do
- q.format(lparen)
-
- if contents && (!contents.is_a?(Params) || !contents.empty?)
- q.indent do
- q.breakable("")
- q.format(contents)
- end
- end
-
- q.breakable("")
- q.text(")")
- end
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("paren")
-
- q.breakable
- q.pp(contents)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :paren,
- lparen: lparen,
- cnts: contents,
- loc: location,
- cmts: comments
- }.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 || nil,
- 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:, comments: [])
- @value = value
- @location = location
- @comments = comments
- end
-
- def child_nodes
- []
- end
-
- def format(q)
- q.text(value)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("period")
-
- q.breakable
- q.pp(value)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :period, value: value, loc: location, cmts: comments }.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
-
- # [Location] the location of this node
- attr_reader :location
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(statements:, location:, comments: [])
- @statements = statements
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [statements]
- end
-
- def format(q)
- q.format(statements)
-
- # We're going to put a newline on the end so that it always has one unless
- # it ends with the special __END__ syntax. In that case we want to
- # replicate the text exactly so we will just let it be.
- q.breakable(force: true) unless statements.body.last.is_a?(EndContent)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("program")
-
- q.breakable
- q.pp(statements)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :program,
- stmts: statements,
- comments: comments,
- loc: location,
- cmts: comments
- }.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 = Program.new(statements: statements, location: location)
- attach_comments(program, @comments)
-
- program
- end
-
- # Attaches comments to the nodes in the tree that most closely correspond to
- # the location of the comments.
- def attach_comments(program, comments)
- comments.each do |comment|
- preceding, enclosing, following = nearest_nodes(program, comment)
-
- if comment.inline?
- if preceding
- preceding.comments << comment
- comment.trailing!
- elsif following
- following.comments << comment
- comment.leading!
- elsif enclosing
- enclosing.comments << comment
- else
- program.comments << comment
- end
- else
- # If a comment exists on its own line, prefer a leading comment.
- if following
- following.comments << comment
- comment.leading!
- elsif preceding
- preceding.comments << comment
- comment.trailing!
- elsif enclosing
- enclosing.comments << comment
- else
- program.comments << comment
- end
- end
- end
- end
-
- # Responsible for finding the nearest nodes to the given comment within the
- # context of the given encapsulating node.
- def nearest_nodes(node, comment)
- comment_start = comment.location.start_char
- comment_end = comment.location.end_char
-
- child_nodes = node.child_nodes.compact
- preceding = nil
- following = nil
-
- left = 0
- right = child_nodes.length
-
- # This is a custom binary search that finds the nearest nodes to the given
- # comment. When it finds a node that completely encapsulates the comment, it
- # recursed downward into the tree.
- while left < right
- middle = (left + right) / 2
- child = child_nodes[middle]
-
- node_start = child.location.start_char
- node_end = child.location.end_char
-
- if node_start <= comment_start && comment_end <= node_end
- # The comment is completely contained by this child node. Abandon the
- # binary search at this level.
- return nearest_nodes(child, comment)
- end
-
- if node_end <= comment_start
- # This child node falls completely before the comment. Because we will
- # never consider this node or any nodes before it again, this node must
- # be the closest preceding node we have encountered so far.
- preceding = child
- left = middle + 1
- next
- end
-
- if comment_end <= node_start
- # This child node falls completely after the comment. Because we will
- # never consider this node or any nodes after it again, this node must
- # be the closest following node we have encountered so far.
- following = child
- right = middle
- next
- end
-
- # This should only happen if there is a bug in this parser.
- raise "Comment location overlaps with node location"
- end
-
- [preceding, node, following]
- end
-
- # QSymbols represents a symbol literal array without interpolation.
- #
- # %i[one two three]
- #
- class QSymbols
- # [QSymbolsBeg] the token that opens this array literal
- attr_reader :beginning
-
- # [Array[ TStringContent ]] the elements of the array
- attr_reader :elements
-
- # [Location] the location of this node
- attr_reader :location
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(beginning:, elements:, location:, comments: [])
- @beginning = beginning
- @elements = elements
- @location = location
- @comments = comments
- end
-
- def child_nodes
- []
- end
-
- def format(q)
- opening, closing = "%i[", "]"
-
- if elements.any? { |element| element.match?(/[\[\]]/) }
- opening = beginning.value
- closing = Quotes.matching(opening[2])
- end
-
- q.group(0, opening, closing) do
- q.indent do
- q.breakable("")
- q.seplist(elements, -> { q.breakable }) do |element|
- q.format(element)
- end
- end
- q.breakable("")
- end
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("qsymbols")
-
- q.breakable
- q.group(2, "(", ")") { q.seplist(elements) { |element| q.pp(element) } }
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :qsymbols,
- elems: elements,
- loc: location,
- cmts: comments
- }.to_json(*opts)
- end
- end
-
- # :call-seq:
- # on_qsymbols_add: (QSymbols qsymbols, TStringContent element) -> QSymbols
- def on_qsymbols_add(qsymbols, element)
- QSymbols.new(
- beginning: qsymbols.beginning,
- 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
- beginning = find_token(QSymbolsBeg)
-
- QSymbols.new(
- beginning: beginning,
- elements: [],
- location: beginning.location
- )
- end
-
- # QWords represents a string literal array without interpolation.
- #
- # %w[one two three]
- #
- class QWords
- # [QWordsBeg] the token that opens this array literal
- attr_reader :beginning
-
- # [Array[ TStringContent ]] the elements of the array
- attr_reader :elements
-
- # [Location] the location of this node
- attr_reader :location
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(beginning:, elements:, location:, comments: [])
- @beginning = beginning
- @elements = elements
- @location = location
- @comments = comments
- end
-
- def child_nodes
- []
- end
-
- def format(q)
- opening, closing = "%w[", "]"
-
- if elements.any? { |element| element.match?(/[\[\]]/) }
- opening = beginning.value
- closing = Quotes.matching(opening[2])
- end
-
- q.group(0, opening, closing) do
- q.indent do
- q.breakable("")
- q.seplist(elements, -> { q.breakable }) do |element|
- q.format(element)
- end
- end
- q.breakable("")
- end
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("qwords")
-
- q.breakable
- q.group(2, "(", ")") { q.seplist(elements) { |element| q.pp(element) } }
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :qwords, elems: elements, loc: location, cmts: comments }.to_json(
- *opts
- )
- end
- end
-
- # :call-seq:
- # on_qwords_add: (QWords qwords, TStringContent element) -> QWords
- def on_qwords_add(qwords, element)
- QWords.new(
- beginning: qwords.beginning,
- 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
- beginning = find_token(QWordsBeg)
-
- QWords.new(beginning: beginning, elements: [], location: beginning.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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:, comments: [])
- @value = value
- @location = location
- @comments = comments
- end
-
- def child_nodes
- []
- end
-
- def format(q)
- q.text(value)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("rational")
-
- q.breakable
- q.pp(value)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :rational, value: value, loc: location, cmts: comments }.to_json(
- *opts
- )
- end
- end
-
- # :call-seq:
- # on_rational: (String value) -> RationalLiteral
- def on_rational(value)
- RationalLiteral.new(
- value: value,
- location: Location.token(line: lineno, char: char_pos, size: value.size)
- )
- 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:, comments: [])
- @value = value
- @location = location
- @comments = comments
- end
-
- def child_nodes
- []
- end
-
- def format(q)
- q.text(value)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("redo")
-
- q.breakable
- q.pp(value)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :redo, value: value, loc: location, cmts: comments }.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
-
- # [Location] the location of this node
- attr_reader :location
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(beginning:, ending:, parts:, location:, comments: [])
- @beginning = beginning
- @ending = ending
- @parts = parts
- @location = location
- @comments = comments
- end
-
- def child_nodes
- parts
- end
-
- def format(q)
- braces = ambiguous?(q) || include?(%r{\/})
-
- if braces && include?(/[{}]/)
- q.group do
- q.text(beginning)
- q.format_each(parts)
- q.text(ending)
- end
- elsif braces
- q.group do
- q.text("%r{")
-
- if beginning == "/"
- # If we're changing from a forward slash to a %r{, then we can
- # replace any escaped forward slashes with regular forward slashes.
- parts.each do |part|
- if part.is_a?(TStringContent)
- q.text(part.value.gsub("\\/", "/"))
- else
- q.format(part)
- end
- end
- else
- q.format_each(parts)
- end
-
- q.text("}")
- q.text(ending[1..-1])
- end
- else
- q.group do
- q.text("/")
- q.format_each(parts)
- q.text("/")
- q.text(ending[1..-1])
- end
- end
- 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) } }
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :regexp_literal,
- beging: beginning,
- ending: ending,
- parts: parts,
- loc: location,
- cmts: comments
- }.to_json(*opts)
- end
-
- private
-
- def include?(pattern)
- parts.any? do |part|
- part.is_a?(TStringContent) && part.value.match?(pattern)
- end
- end
-
- # If the first part of this regex is plain string content, we have a space
- # or an =, and we're contained within a command or command_call node, then
- # we want to use braces because otherwise we could end up with an ambiguous
- # operator, e.g. foo / bar/ or foo /=bar/
- def ambiguous?(q)
- return false if parts.empty?
- part = parts.first
-
- part.is_a?(TStringContent) && part.value.start_with?(" ", "=") &&
- q.parents.any? { |node| node.is_a?(Command) || node.is_a?(CommandCall) }
- end
- end
-
- # :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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(exceptions:, variable:, location:, comments: [])
- @exceptions = exceptions
- @variable = variable
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [*exceptions, variable]
- end
-
- def format(q)
- q.group do
- if exceptions
- q.text(" ")
- q.format(exceptions)
- end
-
- if variable
- q.text(" => ")
- q.format(variable)
- end
- end
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("rescue_ex")
-
- q.breakable
- q.pp(exceptions)
-
- q.breakable
- q.pp(variable)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :rescue_ex,
- extns: exceptions,
- var: variable,
- loc: location,
- cmts: comments
- }.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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(
- exception:,
- statements:,
- consequent:,
- location:,
- comments: []
- )
- @exception = exception
- @statements = statements
- @consequent = consequent
- @location = location
- @comments = comments
- 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 child_nodes
- [exception, statements, consequent]
- end
-
- def format(q)
- q.group do
- q.text("rescue")
-
- if exception
- q.nest("rescue ".length) { q.format(exception) }
- else
- q.text(" StandardError")
- end
-
- unless statements.empty?
- q.indent do
- q.breakable(force: true)
- q.format(statements)
- end
- end
-
- if consequent
- q.breakable(force: true)
- q.format(consequent)
- end
- 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
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :rescue,
- extn: exception,
- stmts: statements,
- cons: consequent,
- loc: location,
- cmts: comments
- }.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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(statement:, value:, location:, comments: [])
- @statement = statement
- @value = value
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [statement, value]
- end
-
- def format(q)
- q.group(0, "begin", "end") do
- q.indent do
- q.breakable(force: true)
- q.format(statement)
- end
- q.breakable(force: true)
- q.text("rescue StandardError")
- q.indent do
- q.breakable(force: true)
- q.format(value)
- end
- q.breakable(force: true)
- end
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("rescue_mod")
-
- q.breakable
- q.pp(statement)
-
- q.breakable
- q.pp(value)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :rescue_mod,
- stmt: statement,
- value: value,
- loc: location,
- cmts: comments
- }.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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(name:, location:, comments: [])
- @name = name
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [name]
- end
-
- def format(q)
- q.text("*")
- q.format(name) if name
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("rest_param")
-
- q.breakable
- q.pp(name)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :rest_param, name: name, loc: location, cmts: comments }.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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:, comments: [])
- @value = value
- @location = location
- @comments = comments
- end
-
- def child_nodes
- []
- end
-
- def format(q)
- q.text(value)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("retry")
-
- q.breakable
- q.pp(value)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :retry, value: value, loc: location, cmts: comments }.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] the arguments being passed to the keyword
- attr_reader :arguments
-
- # [Location] the location of this node
- attr_reader :location
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(arguments:, location:, comments: [])
- @arguments = arguments
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [arguments]
- end
-
- def format(q)
- FlowControlFormatter.new("return", self).format(q)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("return")
-
- q.breakable
- q.pp(arguments)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :return, args: arguments, loc: location, cmts: comments }.to_json(
- *opts
- )
- end
- end
-
- # :call-seq:
- # on_return: (Args 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:, comments: [])
- @value = value
- @location = location
- @comments = comments
- end
-
- def child_nodes
- []
- end
-
- def format(q)
- q.text(value)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("return0")
-
- q.breakable
- q.pp(value)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :return0, value: value, loc: location, cmts: comments }.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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(target:, bodystmt:, location:, comments: [])
- @target = target
- @bodystmt = bodystmt
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [target, bodystmt]
- end
-
- def format(q)
- q.group(0, "class << ", "end") do
- q.format(target)
- q.indent do
- q.breakable(force: true)
- q.format(bodystmt)
- end
- q.breakable(force: true)
- end
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("sclass")
-
- q.breakable
- q.pp(target)
-
- q.breakable
- q.pp(bodystmt)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :sclass,
- target: target,
- bodystmt: bodystmt,
- loc: location,
- cmts: comments
- }.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
-
- # 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 is generating 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(parser, body:, location:, comments: [])
- @parser = parser
- @body = body
- @location = location
- @comments = comments
- 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 empty?
- body.all? do |statement|
- statement.is_a?(VoidStmt) && statement.comments.empty?
- end
- end
-
- def child_nodes
- body
- end
-
- def format(q)
- line = nil
-
- # This handles a special case where you've got a block of statements where
- # the only value is a comment. In that case a lot of nodes like
- # brace_block will attempt to format as a single line, but since that
- # wouldn't work with a comment, we intentionally break the parent group.
- if body.length == 2
- void_stmt, comment = body
-
- if void_stmt.is_a?(VoidStmt) && comment.is_a?(Comment)
- q.format(comment)
- q.break_parent
- return
- end
- end
-
- access_controls =
- Hash.new do |hash, node|
- hash[node] = node.is_a?(VCall) &&
- %w[private protected public].include?(node.value.value)
- end
-
- body.each_with_index do |statement, index|
- next if statement.is_a?(VoidStmt)
-
- if line.nil?
- q.format(statement)
- elsif (statement.location.start_line - line) > 1
- q.breakable(force: true)
- q.breakable(force: true)
- q.format(statement)
- elsif access_controls[statement] || access_controls[body[index - 1]]
- q.breakable(force: true)
- q.breakable(force: true)
- q.format(statement)
- elsif statement.location.start_line != line
- q.breakable(force: true)
- q.format(statement)
- elsif !q.parent.is_a?(StringEmbExpr)
- q.breakable(force: true)
- q.format(statement)
- else
- q.text("; ")
- q.format(statement)
- end
-
- line = statement.location.end_line
- end
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("statements")
-
- q.breakable
- q.seplist(body) { |statement| q.pp(statement) }
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :statements, body: body, loc: location, cmts: comments }.to_json(
- *opts
- )
- end
-
- private
-
- # As efficiently as possible, gather up all of the comments that have been
- # found while this statements list was being parsed and add them into the
- # body.
- def attach_comments(start_char, end_char)
- parser_comments = parser.comments
-
- comment_index = 0
- body_index = 0
-
- while comment_index < parser_comments.size
- comment = parser_comments[comment_index]
- location = comment.location
-
- if !comment.inline? && (start_char <= location.start_char) &&
- (end_char >= location.end_char) && !comment.ignore?
- parser_comments.delete_at(comment_index)
-
- while (node = body[body_index]) &&
- (
- node.is_a?(VoidStmt) ||
- node.location.start_char < location.start_char
- )
- body_index += 1
- end
-
- body.insert(body_index, comment)
- else
- comment_index += 1
- end
- end
- end
- 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)
- location =
- if statements.body.empty?
- statement.location
- else
- statements.location.to(statement.location)
- end
-
- Statements.new(self, body: statements.body << statement, location: location)
- end
-
- # :call-seq:
- # on_stmts_new: () -> Statements
- def on_stmts_new
- Statements.new(
- 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(left:, right:, location:, comments: [])
- @left = left
- @right = right
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [left, right]
- end
-
- def format(q)
- q.group do
- q.format(left)
- q.text(' \\')
- q.indent do
- q.breakable(force: true)
- q.format(right)
- end
- end
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("string_concat")
-
- q.breakable
- q.pp(left)
-
- q.breakable
- q.pp(right)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :string_concat,
- left: left,
- right: right,
- loc: location,
- cmts: comments
- }.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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(variable:, location:, comments: [])
- @variable = variable
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [variable]
- end
-
- def format(q)
- q.text('#{')
- q.format(variable)
- q.text("}")
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("string_dvar")
-
- q.breakable
- q.pp(variable)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :string_dvar,
- var: variable,
- loc: location,
- cmts: comments
- }.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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(statements:, location:, comments: [])
- @statements = statements
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [statements]
- end
-
- def format(q)
- if location.start_line == location.end_line
- # If the contents of this embedded expression were originally on the
- # same line in the source, then we're going to leave them in place and
- # assume that's the way the developer wanted this expression
- # represented.
- doc = q.group(0, '#{', "}") { q.format(statements) }
- RemoveBreaks.call(doc)
- else
- q.group do
- q.text('#{')
- q.indent do
- q.breakable("")
- q.format(statements)
- end
- q.breakable("")
- q.text("}")
- end
- end
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("string_embexpr")
-
- q.breakable
- q.pp(statements)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :string_embexpr,
- stmts: statements,
- loc: location,
- cmts: comments
- }.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
- )
-
- location =
- Location.new(
- start_line: embexpr_beg.location.start_line,
- start_char: embexpr_beg.location.start_char,
- end_line: [
- embexpr_end.location.end_line,
- statements.location.end_line
- ].max,
- end_char: embexpr_end.location.end_char
- )
-
- StringEmbExpr.new(statements: statements, location: 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(parts:, quote:, location:, comments: [])
- @parts = parts
- @quote = quote
- @location = location
- @comments = comments
- end
-
- def child_nodes
- parts
- end
-
- def format(q)
- if parts.empty?
- q.text("#{q.quote}#{q.quote}")
- return
- end
-
- opening_quote, closing_quote =
- if !Quotes.locked?(self)
- [q.quote, q.quote]
- elsif quote.start_with?("%")
- [quote, Quotes.matching(quote[/%[qQ]?(.)/, 1])]
- else
- [quote, quote]
- end
-
- q.group(0, opening_quote, closing_quote) do
- parts.each do |part|
- if part.is_a?(TStringContent)
- value = Quotes.normalize(part.value, closing_quote)
- separator = -> { q.breakable(force: true, indent: false) }
- q.seplist(value.split(/\r?\n/, -1), separator) do |text|
- q.text(text)
- end
- else
- q.format(part)
- end
- end
- end
- 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) } }
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :string_literal,
- parts: parts,
- quote: quote,
- loc: location,
- cmts: comments
- }.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, location: tstring_beg.location)
-
- location =
- Location.new(
- start_line: tstring_beg.location.start_line,
- start_char: tstring_beg.location.start_char,
- end_line: [
- tstring_end.location.end_line,
- string.location.end_line
- ].max,
- end_char: tstring_end.location.end_char
- )
-
- StringLiteral.new(
- parts: string.parts,
- quote: tstring_beg.value,
- location: location
- )
- end
- end
-
- # Super represents using the +super+ keyword with arguments. It can optionally
- # use parentheses.
- #
- # super(value)
- #
- class Super
- # [ArgParen | Args] the arguments to the keyword
- attr_reader :arguments
-
- # [Location] the location of this node
- attr_reader :location
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(arguments:, location:, comments: [])
- @arguments = arguments
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [arguments]
- end
-
- def format(q)
- q.group do
- q.text("super")
-
- if arguments.is_a?(ArgParen)
- q.format(arguments)
- else
- q.text(" ")
- q.nest("super ".length) { q.format(arguments) }
- end
- end
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("super")
-
- q.breakable
- q.pp(arguments)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :super, args: arguments, loc: location, cmts: comments }.to_json(
- *opts
- )
- end
- end
-
- # :call-seq:
- # on_super: ((ArgParen | Args) 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.delete(value)
-
- 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:, comments: [])
- @value = value
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [value]
- end
-
- def format(q)
- q.text(":")
- q.format(value)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("symbol_literal")
-
- q.breakable
- q.pp(value)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :symbol_literal,
- value: value,
- loc: location,
- cmts: comments
- }.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 value.is_a?(SymbolContent)
- symbeg = find_token(SymBeg)
-
- SymbolLiteral.new(
- value: value.value,
- location: symbeg.location.to(value.location)
- )
- else
- tokens.delete(value)
- SymbolLiteral.new(value: value, location: value.location)
- end
- end
-
- # Symbols represents a symbol array literal with interpolation.
- #
- # %I[one two three]
- #
- class Symbols
- # [SymbolsBeg] the token that opens this array literal
- attr_reader :beginning
-
- # [Array[ Word ]] the words in the symbol array literal
- attr_reader :elements
-
- # [Location] the location of this node
- attr_reader :location
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(beginning:, elements:, location:, comments: [])
- @beginning = beginning
- @elements = elements
- @location = location
- @comments = comments
- end
-
- def child_nodes
- []
- end
-
- def format(q)
- opening, closing = "%I[", "]"
-
- if elements.any? { |element| element.match?(/[\[\]]/) }
- opening = beginning.value
- closing = Quotes.matching(opening[2])
- end
-
- q.group(0, opening, closing) do
- q.indent do
- q.breakable("")
- q.seplist(elements, -> { q.breakable }) do |element|
- q.format(element)
- end
- end
- q.breakable("")
- end
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("symbols")
-
- q.breakable
- q.group(2, "(", ")") { q.seplist(elements) { |element| q.pp(element) } }
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :symbols,
- elems: elements,
- loc: location,
- cmts: comments
- }.to_json(*opts)
- end
- end
-
- # :call-seq:
- # on_symbols_add: (Symbols symbols, Word word) -> Symbols
- def on_symbols_add(symbols, word)
- Symbols.new(
- beginning: symbols.beginning,
- 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
- beginning = find_token(SymbolsBeg)
-
- Symbols.new(
- beginning: beginning,
- elements: [],
- location: beginning.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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(constant:, location:, comments: [])
- @constant = constant
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [constant]
- end
-
- def format(q)
- q.text("::")
- q.format(constant)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("top_const_field")
-
- q.breakable
- q.pp(constant)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :top_const_field,
- constant: constant,
- loc: location,
- cmts: comments
- }.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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(constant:, location:, comments: [])
- @constant = constant
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [constant]
- end
-
- def format(q)
- q.text("::")
- q.format(constant)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("top_const_ref")
-
- q.breakable
- q.pp(constant)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :top_const_ref,
- constant: constant,
- loc: location,
- cmts: comments
- }.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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:, comments: [])
- @value = value
- @location = location
- @comments = comments
- end
-
- def match?(pattern)
- value.match?(pattern)
- end
-
- def child_nodes
- []
- end
-
- def format(q)
- q.text(value)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("tstring_content")
-
- q.breakable
- q.pp(value)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :tstring_content,
- value: value.force_encoding("UTF-8"),
- loc: location,
- cmts: comments
- }.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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(statement:, parentheses:, location:, comments: [])
- @statement = statement
- @parentheses = parentheses
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [statement]
- end
-
- def format(q)
- q.text(parentheses ? "not(" : "not ")
- q.format(statement)
- q.text(")") if parentheses
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("not")
-
- q.breakable
- q.pp(statement)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :not,
- value: statement,
- paren: parentheses,
- loc: location,
- cmts: comments
- }.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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(operator:, statement:, location:, comments: [])
- @operator = operator
- @statement = statement
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [statement]
- end
-
- def format(q)
- q.text(operator)
- q.format(statement)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("unary")
-
- q.breakable
- q.pp(operator)
-
- q.breakable
- q.pp(statement)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :unary,
- op: operator,
- value: statement,
- loc: location,
- cmts: comments
- }.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
- class UndefArgumentFormatter
- # [DynaSymbol | SymbolLiteral] the symbol to undefine
- attr_reader :node
-
- def initialize(node)
- @node = node
- end
-
- def comments
- if node.is_a?(SymbolLiteral)
- node.comments + node.value.comments
- else
- node.comments
- end
- end
-
- def format(q)
- node.is_a?(SymbolLiteral) ? q.format(node.value) : q.format(node)
- end
- end
-
- # [Array[ DynaSymbol | SymbolLiteral ]] the symbols to undefine
- attr_reader :symbols
-
- # [Location] the location of this node
- attr_reader :location
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(symbols:, location:, comments: [])
- @symbols = symbols
- @location = location
- @comments = comments
- end
-
- def child_nodes
- symbols
- end
-
- def format(q)
- keyword = "undef "
- formatters = symbols.map { |symbol| UndefArgumentFormatter.new(symbol) }
-
- q.group do
- q.text(keyword)
- q.nest(keyword.length) do
- q.seplist(formatters) { |formatter| q.format(formatter) }
- end
- end
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("undef")
-
- q.breakable
- q.group(2, "(", ")") { q.seplist(symbols) { |symbol| q.pp(symbol) } }
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :undef, syms: symbols, loc: location, cmts: comments }.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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(
- predicate:,
- statements:,
- consequent:,
- location:,
- comments: []
- )
- @predicate = predicate
- @statements = statements
- @consequent = consequent
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [predicate, statements, consequent]
- end
-
- def format(q)
- ConditionalFormatter.new("unless", self).format(q)
- 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
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :unless,
- pred: predicate,
- stmts: statements,
- cons: consequent,
- loc: location,
- cmts: comments
- }.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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(statement:, predicate:, location:, comments: [])
- @statement = statement
- @predicate = predicate
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [statement, predicate]
- end
-
- def format(q)
- ConditionalModFormatter.new("unless", self).format(q)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("unless_mod")
-
- q.breakable
- q.pp(statement)
-
- q.breakable
- q.pp(predicate)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :unless_mod,
- stmt: statement,
- pred: predicate,
- loc: location,
- cmts: comments
- }.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
-
- # Formats an Until, UntilMod, While, or WhileMod node.
- class LoopFormatter
- # [String] the name of the keyword used for this loop
- attr_reader :keyword
-
- # [Until | UntilMod | While | WhileMod] the node that is being formatted
- attr_reader :node
-
- # [untyped] the statements associated with the node
- attr_reader :statements
-
- def initialize(keyword, node, statements)
- @keyword = keyword
- @node = node
- @statements = statements
- end
-
- def format(q)
- if ContainsAssignment.call(node.predicate)
- format_break(q)
- q.break_parent
- return
- end
-
- q.group do
- q.if_break { format_break(q) }.if_flat do
- Parentheses.flat(q) do
- q.format(statements)
- q.text(" #{keyword} ")
- q.format(node.predicate)
- end
- end
- end
- end
-
- private
-
- def format_break(q)
- q.text("#{keyword} ")
- q.nest(keyword.length + 1) { q.format(node.predicate) }
- q.indent do
- q.breakable("")
- q.format(statements)
- end
- q.breakable("")
- q.text("end")
- end
- 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(predicate:, statements:, location:, comments: [])
- @predicate = predicate
- @statements = statements
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [predicate, statements]
- end
-
- def format(q)
- if statements.empty?
- keyword = "until "
-
- q.group do
- q.text(keyword)
- q.nest(keyword.length) { q.format(predicate) }
- q.breakable(force: true)
- q.text("end")
- end
- else
- LoopFormatter.new("until", self, statements).format(q)
- end
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("until")
-
- q.breakable
- q.pp(predicate)
-
- q.breakable
- q.pp(statements)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :until,
- pred: predicate,
- stmts: statements,
- loc: location,
- cmts: comments
- }.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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(statement:, predicate:, location:, comments: [])
- @statement = statement
- @predicate = predicate
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [statement, predicate]
- end
-
- def format(q)
- # If we're in the modifier form and we're modifying a `begin`, then this
- # is a special case where we need to explicitly use the modifier form
- # because otherwise the semantic meaning changes. This looks like:
- #
- # begin
- # foo
- # end until bar
- #
- # Also, if the statement of the modifier includes an assignment, then we
- # can't know for certain that it won't impact the predicate, so we need to
- # force it to stay as it is. This looks like:
- #
- # foo = bar until foo
- #
- if statement.is_a?(Begin) || ContainsAssignment.call(statement)
- q.format(statement)
- q.text(" until ")
- q.format(predicate)
- else
- LoopFormatter.new("until", self, statement).format(q)
- end
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("until_mod")
-
- q.breakable
- q.pp(statement)
-
- q.breakable
- q.pp(predicate)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :until_mod,
- stmt: statement,
- pred: predicate,
- loc: location,
- cmts: comments
- }.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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(left:, right:, location:, comments: [])
- @left = left
- @right = right
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [left, right]
- end
-
- def format(q)
- keyword = "alias "
-
- q.text(keyword)
- q.format(left)
- q.text(" ")
- q.format(right)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("var_alias")
-
- q.breakable
- q.pp(left)
-
- q.breakable
- q.pp(right)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :var_alias,
- left: left,
- right: right,
- loc: location,
- cmts: comments
- }.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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:, comments: [])
- @value = value
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [value]
- end
-
- def format(q)
- q.format(value) if value
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("var_field")
-
- q.breakable
- q.pp(value)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :var_field, value: value, loc: location, cmts: comments }.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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:, comments: [])
- @value = value
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [value]
- end
-
- def format(q)
- q.format(value)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("var_ref")
-
- q.breakable
- q.pp(value)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :var_ref, value: value, loc: location, cmts: comments }.to_json(
- *opts
- )
- end
- end
-
- # PinnedVarRef represents a pinned variable reference within a pattern
- # matching pattern.
- #
- # case value
- # in ^variable
- # end
- #
- # This can be a plain local variable like the example above. It can also be a
- # a class variable, a global variable, or an instance variable.
- class PinnedVarRef
- # [VarRef] the value of this node
- attr_reader :value
-
- # [Location] the location of this node
- attr_reader :location
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:, comments: [])
- @value = value
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [value]
- end
-
- def format(q)
- q.group do
- q.text("^")
- q.format(value)
- end
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("pinned_var_ref")
-
- q.breakable
- q.pp(value)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :pinned_var_ref, value: value, loc: location, cmts: comments }
- .to_json(*opts)
- end
- end
-
- # :call-seq:
- # on_var_ref: ((Const | CVar | GVar | Ident | IVar | Kw) value) -> VarRef
- def on_var_ref(value)
- pin = find_token(Op, "^", consume: false)
-
- if pin && pin.location.start_char == value.location.start_char - 1
- tokens.delete(pin)
- PinnedVarRef.new(value: value, location: pin.location.to(value.location))
- else
- VarRef.new(value: value, location: value.location)
- 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:, comments: [])
- @value = value
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [value]
- end
-
- def format(q)
- q.format(value)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("vcall")
-
- q.breakable
- q.pp(value)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :vcall, value: value, loc: location, cmts: comments }.to_json(
- *opts
- )
- end
- end
-
- # :call-seq:
- # on_vcall: (Ident ident) -> VCall
- def on_vcall(ident)
- VCall.new(value: ident, location: ident.location)
- end
-
- # VoidStmt represents an empty lexical block of code.
- #
- # ;;
- #
- class VoidStmt
- # [Location] the location of this node
- attr_reader :location
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(location:, comments: [])
- @location = location
- @comments = comments
- end
-
- def child_nodes
- []
- end
-
- def format(q)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("void_stmt")
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :void_stmt, loc: location, cmts: comments }.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
- # [Args] the arguments to the when clause
- attr_reader :arguments
-
- # [Statements] the expressions to be executed
- attr_reader :statements
-
- # [nil | Else | When] the next clause in the chain
- attr_reader :consequent
-
- # [Location] the location of this node
- attr_reader :location
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(
- arguments:,
- statements:,
- consequent:,
- location:,
- comments: []
- )
- @arguments = arguments
- @statements = statements
- @consequent = consequent
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [arguments, statements, consequent]
- end
-
- def format(q)
- keyword = "when "
-
- q.group do
- q.group do
- q.text(keyword)
- q.nest(keyword.length) do
- if arguments.comments.any?
- q.format(arguments)
- else
- separator = -> { q.group { q.comma_breakable } }
- q.seplist(arguments.parts, separator) { |part| q.format(part) }
- end
-
- # Very special case here. If you're inside of a when clause and the
- # last argument to the predicate is and endless range, then you are
- # forced to use the "then" keyword to make it parse properly.
- last = arguments.parts.last
- if (last.is_a?(Dot2) || last.is_a?(Dot3)) && !last.right
- q.text(" then")
- end
- end
- end
-
- unless statements.empty?
- q.indent do
- q.breakable(force: true)
- q.format(statements)
- end
- end
-
- if consequent
- q.breakable(force: true)
- q.format(consequent)
- end
- end
- 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
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :when,
- args: arguments,
- stmts: statements,
- cons: consequent,
- loc: location,
- cmts: comments
- }.to_json(*opts)
- end
- end
-
- # :call-seq:
- # on_when: (
- # Args 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_start = arguments
- if token = find_token(Kw, "then", consume: false)
- tokens.delete(token)
- statements_start = token
- end
-
- statements.bind(
- find_next_statement_start(statements_start.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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(predicate:, statements:, location:, comments: [])
- @predicate = predicate
- @statements = statements
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [predicate, statements]
- end
-
- def format(q)
- if statements.empty?
- keyword = "while "
-
- q.group do
- q.text(keyword)
- q.nest(keyword.length) { q.format(predicate) }
- q.breakable(force: true)
- q.text("end")
- end
- else
- LoopFormatter.new("while", self, statements).format(q)
- end
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("while")
-
- q.breakable
- q.pp(predicate)
-
- q.breakable
- q.pp(statements)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :while,
- pred: predicate,
- stmts: statements,
- loc: location,
- cmts: comments
- }.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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(statement:, predicate:, location:, comments: [])
- @statement = statement
- @predicate = predicate
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [statement, predicate]
- end
-
- def format(q)
- # If we're in the modifier form and we're modifying a `begin`, then this
- # is a special case where we need to explicitly use the modifier form
- # because otherwise the semantic meaning changes. This looks like:
- #
- # begin
- # foo
- # end while bar
- #
- # Also, if the statement of the modifier includes an assignment, then we
- # can't know for certain that it won't impact the predicate, so we need to
- # force it to stay as it is. This looks like:
- #
- # foo = bar while foo
- #
- if statement.is_a?(Begin) || ContainsAssignment.call(statement)
- q.format(statement)
- q.text(" while ")
- q.format(predicate)
- else
- LoopFormatter.new("while", self, statement).format(q)
- end
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("while_mod")
-
- q.breakable
- q.pp(statement)
-
- q.breakable
- q.pp(predicate)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :while_mod,
- stmt: statement,
- pred: predicate,
- loc: location,
- cmts: comments
- }.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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(parts:, location:, comments: [])
- @parts = parts
- @location = location
- @comments = comments
- end
-
- def match?(pattern)
- parts.any? { |part| part.is_a?(TStringContent) && part.match?(pattern) }
- end
-
- def child_nodes
- parts
- end
-
- def format(q)
- q.format_each(parts)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("word")
-
- q.breakable
- q.group(2, "(", ")") { q.seplist(parts) { |part| q.pp(part) } }
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :word, parts: parts, loc: location, cmts: comments }.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
- # [WordsBeg] the token that opens this array literal
- attr_reader :beginning
-
- # [Array[ Word ]] the elements of this array
- attr_reader :elements
-
- # [Location] the location of this node
- attr_reader :location
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(beginning:, elements:, location:, comments: [])
- @beginning = beginning
- @elements = elements
- @location = location
- @comments = comments
- end
-
- def child_nodes
- []
- end
-
- def format(q)
- opening, closing = "%W[", "]"
-
- if elements.any? { |element| element.match?(/[\[\]]/) }
- opening = beginning.value
- closing = Quotes.matching(opening[2])
- end
-
- q.group(0, opening, closing) do
- q.indent do
- q.breakable("")
- q.seplist(elements, -> { q.breakable }) do |element|
- q.format(element)
- end
- end
- q.breakable("")
- end
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("words")
-
- q.breakable
- q.group(2, "(", ")") { q.seplist(elements) { |element| q.pp(element) } }
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :words, elems: elements, loc: location, cmts: comments }.to_json(
- *opts
- )
- end
- end
-
- # :call-seq:
- # on_words_add: (Words words, Word word) -> Words
- def on_words_add(words, word)
- Words.new(
- beginning: words.beginning,
- 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
- beginning = find_token(WordsBeg)
-
- Words.new(beginning: beginning, elements: [], location: beginning.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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(parts:, location:, comments: [])
- @parts = parts
- @location = location
- @comments = comments
- end
-
- def child_nodes
- parts
- end
-
- def format(q)
- q.text("`")
- q.format_each(parts)
- q.text("`")
- 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) } }
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- {
- type: :xstring_literal,
- parts: parts,
- loc: location,
- cmts: comments
- }.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, location: xstring.location)
-
- 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
- # [Args | Paren] the arguments passed to the yield
- attr_reader :arguments
-
- # [Location] the location of this node
- attr_reader :location
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(arguments:, location:, comments: [])
- @arguments = arguments
- @location = location
- @comments = comments
- end
-
- def child_nodes
- [arguments]
- end
-
- def format(q)
- q.group do
- q.text("yield")
-
- if arguments.is_a?(Paren)
- q.format(arguments)
- else
- q.if_break { q.text("(") }.if_flat { q.text(" ") }
- q.indent do
- q.breakable("")
- q.format(arguments)
- end
- q.breakable("")
- q.if_break { q.text(")") }
- end
- end
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("yield")
-
- q.breakable
- q.pp(arguments)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :yield, args: arguments, loc: location, cmts: comments }.to_json(
- *opts
- )
- end
- end
-
- # :call-seq:
- # on_yield: ((Args | 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
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:, comments: [])
- @value = value
- @location = location
- @comments = comments
- end
-
- def child_nodes
- []
- end
-
- def format(q)
- q.text(value)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("yield0")
-
- q.breakable
- q.pp(value)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :yield0, value: value, loc: location, cmts: comments }.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 this node
- attr_reader :location
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:, comments: [])
- @value = value
- @location = location
- @comments = comments
- end
-
- def child_nodes
- []
- end
-
- def format(q)
- q.text(value)
- end
-
- def pretty_print(q)
- q.group(2, "(", ")") do
- q.text("zsuper")
-
- q.breakable
- q.pp(value)
-
- q.pp(Comment::List.new(comments))
- end
- end
-
- def to_json(*opts)
- { type: :zsuper, value: value, loc: location, cmts: comments }.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
diff --git a/lib/syntax_tree/cli.rb b/lib/syntax_tree/cli.rb
index 82545df7..1f6f6468 100644
--- a/lib/syntax_tree/cli.rb
+++ b/lib/syntax_tree/cli.rb
@@ -1,7 +1,18 @@
# frozen_string_literal: true
-class SyntaxTree
+module SyntaxTree
module CLI
+ # This holds references to objects that respond to both #parse and #format
+ # so that we can use them in the CLI.
+ HANDLERS = {}
+ HANDLERS.default = SyntaxTree
+
+ # This is a hook provided so that plugins can register themselves as the
+ # handler for a particular file type.
+ def self.register_handler(extension, handler)
+ HANDLERS[extension] = handler
+ end
+
# A utility wrapper around colored strings in the output.
class Color
attr_reader :value, :code
@@ -15,6 +26,10 @@ def to_s
"\033[#{code}m#{value}\033[0m"
end
+ def self.bold(value)
+ new(value, "1")
+ end
+
def self.gray(value)
new(value, "38;5;102")
end
@@ -30,7 +45,7 @@ def self.yellow(value)
# The parent action class for the CLI that implements the basics.
class Action
- def run(filepath, source)
+ def run(handler, filepath, source)
end
def success
@@ -42,8 +57,8 @@ def failure
# An action of the CLI that prints out the AST for the given source.
class AST < Action
- def run(filepath, source)
- pp SyntaxTree.parse(source)
+ def run(handler, filepath, source)
+ pp handler.parse(source)
end
end
@@ -53,8 +68,8 @@ class Check < Action
class UnformattedError < StandardError
end
- def run(filepath, source)
- raise UnformattedError if source != SyntaxTree.format(source)
+ def run(handler, filepath, source)
+ raise UnformattedError if source != handler.format(source)
rescue StandardError
warn("[#{Color.yellow("warn")}] #{filepath}")
raise
@@ -75,11 +90,11 @@ class Debug < Action
class NonIdempotentFormatError < StandardError
end
- def run(filepath, source)
+ def run(handler, filepath, source)
warning = "[#{Color.yellow("warn")}] #{filepath}"
- formatted = SyntaxTree.format(source)
+ formatted = handler.format(source)
- if formatted != SyntaxTree.format(formatted)
+ if formatted != handler.format(formatted)
raise NonIdempotentFormatError
end
rescue StandardError
@@ -98,28 +113,28 @@ def failure
# An action of the CLI that prints out the doc tree IR for the given source.
class Doc < Action
- def run(filepath, source)
+ def run(handler, filepath, source)
formatter = Formatter.new([])
- SyntaxTree.parse(source).format(formatter)
+ handler.parse(source).format(formatter)
pp formatter.groups.first
end
end
# An action of the CLI that formats the input source and prints it out.
class Format < Action
- def run(filepath, source)
- puts SyntaxTree.format(source)
+ def run(handler, filepath, source)
+ puts handler.format(source)
end
end
# An action of the CLI that formats the input source and writes the
# formatted output back to the file.
class Write < Action
- def run(filepath, source)
+ def run(handler, filepath, source)
print filepath
start = Time.now
- formatted = SyntaxTree.format(source)
+ formatted = handler.format(source)
File.write(filepath, formatted)
color = source == formatted ? Color.gray(filepath) : filepath
@@ -135,24 +150,65 @@ def run(filepath, source)
# The help message displayed if the input arguments are not correctly
# ordered or formatted.
HELP = <<~HELP
- stree MODE FILE
+ #{Color.bold("stree ast [OPTIONS] [FILE]")}
+ Print out the AST corresponding to the given files
+
+ #{Color.bold("stree check [OPTIONS] [FILE]")}
+ Check that the given files are formatted as syntax tree would format them
+
+ #{Color.bold("stree debug [OPTIONS] [FILE]")}
+ Check that the given files can be formatted idempotently
+
+ #{Color.bold("stree doc [OPTIONS] [FILE]")}
+ Print out the doc tree that would be used to format the given files
+
+ #{Color.bold("stree format [OPTIONS] [FILE]")}
+ Print out the formatted version of the given files
+
+ #{Color.bold("stree help")}
+ Display this help message
- MODE: ast | check | debug | doc | format | write
- FILE: one or more paths to files to parse
+ #{Color.bold("stree lsp")}
+ Run syntax tree in language server mode
+
+ #{Color.bold("stree version")}
+ Output the current version of syntax tree
+
+ #{Color.bold("stree write [OPTIONS] [FILE]")}
+ Read, format, and write back the source of the given files
+
+ [OPTIONS]
+
+ --plugins=...
+ A comma-separated list of plugins to load.
HELP
class << self
# Run the CLI over the given array of strings that make up the arguments
# passed to the invocation.
def run(argv)
- if argv.length < 2
+ name, *arguments = argv
+
+ case name
+ when "help"
+ puts HELP
+ return 0
+ when "lsp"
+ require "syntax_tree/language_server"
+ LanguageServer.new.run
+ return 0
+ when "version"
+ puts SyntaxTree::VERSION
+ return 0
+ end
+
+ if arguments.empty?
warn(HELP)
return 1
end
- arg, *patterns = argv
action =
- case arg
+ case name
when "a", "ast"
AST.new
when "c", "check"
@@ -170,15 +226,31 @@ def run(argv)
return 1
end
+ # If there are any plugins specified on the command line, then load them
+ # by requiring them here. We do this by transforming something like
+ #
+ # stree format --plugins=haml template.haml
+ #
+ # into
+ #
+ # require "syntax_tree/haml"
+ #
+ if arguments.first.start_with?("--plugins=")
+ plugins = arguments.shift[/^--plugins=(.*)$/, 1]
+ plugins.split(",").each { |plugin| require "syntax_tree/#{plugin}" }
+ end
+
errored = false
- patterns.each do |pattern|
+ arguments.each do |pattern|
Dir.glob(pattern).each do |filepath|
next unless File.file?(filepath)
- source = SyntaxTree.read(filepath)
+
+ handler = HANDLERS[File.extname(filepath)]
+ source = handler.read(filepath)
begin
- action.run(filepath, source)
- rescue ParseError => error
+ action.run(handler, filepath, source)
+ rescue Parser::ParseError => error
warn("Error: #{error.message}")
if error.lineno
diff --git a/lib/syntax_tree/formatter.rb b/lib/syntax_tree/formatter.rb
new file mode 100644
index 00000000..643030ac
--- /dev/null
+++ b/lib/syntax_tree/formatter.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+module SyntaxTree
+ # A slightly enhanced PP that knows how to format recursively including
+ # comments.
+ class Formatter < PP
+ COMMENT_PRIORITY = 1
+ HEREDOC_PRIORITY = 2
+
+ attr_reader :source, :stack, :quote
+
+ def initialize(source, ...)
+ super(...)
+
+ @source = source
+ @stack = []
+ @quote = "\""
+ end
+
+ def format(node, stackable: true)
+ stack << node if stackable
+ doc = nil
+
+ # If there are comments, then we're going to format them around the node
+ # so that they get printed properly.
+ if node.comments.any?
+ leading, trailing = node.comments.partition(&:leading?)
+
+ # Print all comments that were found before the node.
+ leading.each do |comment|
+ comment.format(self)
+ breakable(force: true)
+ end
+
+ # If the node has a stree-ignore comment right before it, then we're
+ # going to just print out the node as it was seen in the source.
+ if leading.last&.ignore?
+ doc = text(source[node.location.start_char...node.location.end_char])
+ else
+ doc = node.format(self)
+ end
+
+ # Print all comments that were found after the node.
+ trailing.each do |comment|
+ line_suffix(priority: COMMENT_PRIORITY) do
+ text(" ")
+ comment.format(self)
+ break_parent
+ end
+ end
+ else
+ doc = node.format(self)
+ end
+
+ stack.pop if stackable
+ doc
+ end
+
+ def format_each(nodes)
+ nodes.each { |node| format(node) }
+ end
+
+ def parent
+ stack[-2]
+ end
+
+ def parents
+ stack[0...-1].reverse_each
+ end
+ end
+end
diff --git a/lib/syntax_tree/language_server.rb b/lib/syntax_tree/language_server.rb
new file mode 100644
index 00000000..177a39a1
--- /dev/null
+++ b/lib/syntax_tree/language_server.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+require "cgi"
+require "json"
+require "uri"
+
+require_relative "language_server/inlay_hints"
+
+module SyntaxTree
+ class LanguageServer
+ attr_reader :input, :output
+
+ def initialize(input: STDIN, output: STDOUT)
+ @input = input.binmode
+ @output = output.binmode
+ end
+
+ def run
+ store =
+ Hash.new do |hash, uri|
+ hash[uri] = File.binread(CGI.unescape(URI.parse(uri).path))
+ end
+
+ while headers = input.gets("\r\n\r\n")
+ source = input.read(headers[/Content-Length: (\d+)/i, 1].to_i)
+ request = JSON.parse(source, symbolize_names: true)
+
+ case request
+ in { method: "initialize", id: }
+ store.clear
+ write(id: id, result: { capabilities: capabilities })
+ in { method: "initialized" }
+ # ignored
+ in { method: "shutdown" }
+ store.clear
+ return
+ in { method: "textDocument/didChange", params: { textDocument: { uri: }, contentChanges: [{ text: }, *] } }
+ store[uri] = text
+ in { method: "textDocument/didOpen", params: { textDocument: { uri:, text: } } }
+ store[uri] = text
+ in { method: "textDocument/didClose", params: { textDocument: { uri: } } }
+ store.delete(uri)
+ in { method: "textDocument/formatting", id:, params: { textDocument: { uri: } } }
+ write(id: id, result: [format(store[uri])])
+ in { method: "textDocument/inlayHints", id:, params: { textDocument: { uri: } } }
+ write(id: id, result: inlay_hints(store[uri]))
+ in { method: "syntaxTree/visualizing", id:, params: { textDocument: { uri: } } }
+ output = []
+ PP.pp(SyntaxTree.parse(store[uri]), output)
+ write(id: id, result: output.join)
+ in { method: %r{\$/.+} }
+ # ignored
+ else
+ raise "Unhandled: #{request}"
+ end
+ end
+ end
+
+ private
+
+ def capabilities
+ {
+ documentFormattingProvider: true,
+ textDocumentSync: { change: 1, openClose: true }
+ }
+ end
+
+ def format(source)
+ {
+ range: {
+ start: { line: 0, character: 0 },
+ end: { line: source.lines.size + 1, character: 0 }
+ },
+ newText: SyntaxTree.format(source)
+ }
+ end
+
+ def log(message)
+ write(method: "window/logMessage", params: { type: 4, message: message })
+ end
+
+ def inlay_hints(source)
+ inlay_hints = InlayHints.find(SyntaxTree.parse(source))
+ serialize = ->(position, text) { { position: position, text: text } }
+
+ {
+ before: inlay_hints.before.map(&serialize),
+ after: inlay_hints.after.map(&serialize)
+ }
+ rescue Parser::ParseError
+ end
+
+ def write(value)
+ response = value.merge(jsonrpc: "2.0").to_json
+ output.print("Content-Length: #{response.bytesize}\r\n\r\n#{response}")
+ output.flush
+ end
+ end
+end
diff --git a/lib/syntax_tree/language_server/inlay_hints.rb b/lib/syntax_tree/language_server/inlay_hints.rb
new file mode 100644
index 00000000..5e43439c
--- /dev/null
+++ b/lib/syntax_tree/language_server/inlay_hints.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+module SyntaxTree
+ class LanguageServer
+ class InlayHints
+ attr_reader :before, :after
+
+ def initialize
+ @before = Hash.new { |hash, key| hash[key] = +"" }
+ @after = Hash.new { |hash, key| hash[key] = +"" }
+ end
+
+ # Adds the implicitly rescued StandardError into a bare rescue clause. For
+ # example,
+ #
+ # begin
+ # rescue
+ # end
+ #
+ # becomes
+ #
+ # begin
+ # rescue StandardError
+ # end
+ #
+ def bare_rescue(location)
+ after[location.start_char + "rescue".length] << " StandardError"
+ end
+
+ # Adds the implicitly referenced value (local variable or method call)
+ # that is added into a hash when the value of a key-value pair is omitted.
+ # For example,
+ #
+ # { value: }
+ #
+ # becomes
+ #
+ # { value: value }
+ #
+ def missing_hash_value(key, location)
+ after[location.end_char] << " #{key}"
+ end
+
+ # Adds implicit parentheses around certain expressions to make it clear
+ # which subexpression will be evaluated first. For example,
+ #
+ # a + b * c
+ #
+ # becomes
+ #
+ # a + ₍b * c₎
+ #
+ def precedence_parentheses(location)
+ before[location.start_char] << "₍"
+ after[location.end_char] << "₎"
+ end
+
+ def self.find(program)
+ inlay_hints = new
+ queue = [[nil, program]]
+
+ until queue.empty?
+ parent_node, child_node = queue.shift
+
+ child_node.child_nodes.each do |grand_child_node|
+ queue << [child_node, grand_child_node] if grand_child_node
+ end
+
+ case [parent_node, child_node]
+ in _, Rescue[exception: nil, location:]
+ inlay_hints.bare_rescue(location)
+ in _, Assoc[key: Label[value: key], value: nil, location:]
+ inlay_hints.missing_hash_value(key[0...-1], location)
+ in Assign | Binary | IfOp | OpAssign, IfOp[location:]
+ inlay_hints.precedence_parentheses(location)
+ in Assign | OpAssign, Binary[location:]
+ inlay_hints.precedence_parentheses(location)
+ in Binary[operator: parent_oper], Binary[operator: child_oper, location:] if parent_oper != child_oper
+ inlay_hints.precedence_parentheses(location)
+ in Binary, Unary[operator: "-", location:]
+ inlay_hints.precedence_parentheses(location)
+ in Params, Assign[location:]
+ inlay_hints.precedence_parentheses(location)
+ else
+ # do nothing
+ end
+ end
+
+ inlay_hints
+ end
+ end
+ end
+end
diff --git a/lib/syntax_tree/node.rb b/lib/syntax_tree/node.rb
new file mode 100644
index 00000000..ee7ecfac
--- /dev/null
+++ b/lib/syntax_tree/node.rb
@@ -0,0 +1,11979 @@
+# frozen_string_literal: true
+
+module SyntaxTree
+ # Represents the location of a node in the tree from the source code.
+ class Location
+ attr_reader :start_line, :start_char, :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 lines
+ start_line..end_line
+ end
+
+ def ==(other)
+ other.is_a?(Location) && start_line == other.start_line &&
+ start_char == other.start_char && end_line == other.end_line &&
+ end_char == other.end_char
+ end
+
+ def to(other)
+ Location.new(
+ start_line: start_line,
+ start_char: start_char,
+ end_line: [end_line, other.end_line].max,
+ 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
+
+ # This is the parent node of all of the syntax tree nodes. It's pretty much
+ # exclusively here to make it easier to operate with the tree in cases where
+ # you're trying to monkey-patch or strictly type.
+ class Node
+ def child_nodes
+ raise NotImplementedError
+ end
+
+ def deconstruct
+ raise NotImplementedError
+ end
+
+ def deconstruct_keys(keys)
+ raise NotImplementedError
+ end
+
+ def format(q)
+ raise NotImplementedError
+ end
+
+ def pretty_print(q)
+ raise NotImplementedError
+ end
+
+ def to_json(*opts)
+ raise NotImplementedError
+ end
+ end
+
+ # BEGINBlock represents the use of the +BEGIN+ keyword, which hooks into the
+ # lifecycle of the interpreter. Whatever is inside the block will get executed
+ # when the program starts.
+ #
+ # BEGIN {
+ # }
+ #
+ # Interestingly, the BEGIN keyword doesn't allow the do and end keywords for
+ # the block. Only braces are permitted.
+ class BEGINBlock < Node
+ # [LBrace] the left brace that is seen after the keyword
+ attr_reader :lbrace
+
+ # [Statements] the expressions to be executed
+ attr_reader :statements
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(lbrace:, statements:, location:, comments: [])
+ @lbrace = lbrace
+ @statements = statements
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [lbrace, statements]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ lbrace: lbrace,
+ statements: statements,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ q.group do
+ q.text("BEGIN ")
+ q.format(lbrace)
+ q.indent do
+ q.breakable
+ q.format(statements)
+ end
+ q.breakable
+ q.text("}")
+ end
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("BEGIN")
+
+ q.breakable
+ q.pp(statements)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :BEGIN,
+ lbrace: lbrace,
+ stmts: statements,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # CHAR irepresents a single codepoint in the script encoding.
+ #
+ # ?a
+ #
+ # In the example above, the CHAR node represents the string literal "a". You
+ # can use control characters with this as well, as in ?\C-a.
+ class CHAR < Node
+ # [String] the value of the character literal
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(value:, location:, comments: [])
+ @value = value
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ []
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { value: value, location: location, comments: comments }
+ end
+
+ def format(q)
+ if value.length != 2
+ q.text(value)
+ else
+ q.text(q.quote)
+ q.text(value[1] == "\"" ? "\\\"" : value[1])
+ q.text(q.quote)
+ end
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("CHAR")
+
+ q.breakable
+ q.pp(value)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :CHAR, value: value, loc: location, cmts: comments }.to_json(
+ *opts
+ )
+ end
+ end
+
+ # ENDBlock represents the use of the +END+ keyword, which hooks into the
+ # lifecycle of the interpreter. Whatever is inside the block will get executed
+ # when the program ends.
+ #
+ # END {
+ # }
+ #
+ # Interestingly, the END keyword doesn't allow the do and end keywords for the
+ # block. Only braces are permitted.
+ class ENDBlock < Node
+ # [LBrace] the left brace that is seen after the keyword
+ attr_reader :lbrace
+
+ # [Statements] the expressions to be executed
+ attr_reader :statements
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(lbrace:, statements:, location:, comments: [])
+ @lbrace = lbrace
+ @statements = statements
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [lbrace, statements]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ lbrace: lbrace,
+ statements: statements,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ q.group do
+ q.text("END ")
+ q.format(lbrace)
+ q.indent do
+ q.breakable
+ q.format(statements)
+ end
+ q.breakable
+ q.text("}")
+ end
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("END")
+
+ q.breakable
+ q.pp(statements)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :END,
+ lbrace: lbrace,
+ stmts: statements,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # EndContent represents the use of __END__ syntax, which allows individual
+ # scripts to keep content after the main ruby code that can be read through
+ # the DATA constant.
+ #
+ # puts DATA.read
+ #
+ # __END__
+ # some other content that is not executed by the program
+ #
+ class EndContent < Node
+ # [String] the content after the script
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(value:, location:, comments: [])
+ @value = value
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ []
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { value: value, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.text("__END__")
+ q.breakable(force: true)
+
+ separator = -> { q.breakable(indent: false, force: true) }
+ q.seplist(value.split(/\r?\n/, -1), separator) { |line| q.text(line) }
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("__end__")
+
+ q.breakable
+ q.pp(value)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :__end__, value: value, loc: location, cmts: comments }.to_json(
+ *opts
+ )
+ end
+ end
+
+ # Alias represents the use of the +alias+ keyword with regular arguments (not
+ # global variables). The +alias+ keyword is used to make a method respond to
+ # another name as well as the current one.
+ #
+ # alias aliased_name name
+ #
+ # For the example above, in the current context you can now call aliased_name
+ # and it will execute the name method. When you're aliasing two methods, you
+ # can either provide bare words (like the example above) or you can provide
+ # symbols (note that this includes dynamic symbols like
+ # :"left-#{middle}-right").
+ class Alias < Node
+ class AliasArgumentFormatter
+ # [DynaSymbol | SymbolLiteral] the argument being passed to alias
+ attr_reader :argument
+
+ def initialize(argument)
+ @argument = argument
+ end
+
+ def comments
+ if argument.is_a?(SymbolLiteral)
+ argument.comments + argument.value.comments
+ else
+ argument.comments
+ end
+ end
+
+ def format(q)
+ if argument.is_a?(SymbolLiteral)
+ q.format(argument.value)
+ else
+ q.format(argument)
+ end
+ end
+ end
+
+ # [DynaSymbol | 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
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(left:, right:, location:, comments: [])
+ @left = left
+ @right = right
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [left, right]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { left: left, right: right, location: location, comments: comments }
+ end
+
+ def format(q)
+ keyword = "alias "
+ left_argument = AliasArgumentFormatter.new(left)
+
+ q.group do
+ q.text(keyword)
+ q.format(left_argument, stackable: false)
+ q.group do
+ q.nest(keyword.length) do
+ q.breakable(force: left_argument.comments.any?)
+ q.format(AliasArgumentFormatter.new(right), stackable: false)
+ end
+ end
+ end
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("alias")
+
+ q.breakable
+ q.pp(left)
+
+ q.breakable
+ q.pp(right)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :alias,
+ left: left,
+ right: right,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # ARef represents when you're pulling a value out of a collection at a
+ # specific index. Put another way, it's any time you're calling the method
+ # #[].
+ #
+ # collection[index]
+ #
+ # The nodes usually contains two children, the collection and the index. In
+ # some cases, you don't necessarily have the second child node, because you
+ # can call procs with a pretty esoteric syntax. In the following example, you
+ # wouldn't have a second child node:
+ #
+ # collection[]
+ #
+ class ARef < Node
+ # [untyped] the value being indexed
+ attr_reader :collection
+
+ # [nil | Args] the value being passed within the brackets
+ attr_reader :index
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(collection:, index:, location:, comments: [])
+ @collection = collection
+ @index = index
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [collection, index]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ collection: collection,
+ index: index,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ q.group do
+ q.format(collection)
+ q.text("[")
+
+ if index
+ q.indent do
+ q.breakable("")
+ q.format(index)
+ end
+ q.breakable("")
+ end
+
+ q.text("]")
+ end
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("aref")
+
+ q.breakable
+ q.pp(collection)
+
+ q.breakable
+ q.pp(index)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :aref,
+ collection: collection,
+ index: index,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # ARefField represents assigning values into collections at specific indices.
+ # Put another way, it's any time you're calling the method #[]=. The
+ # ARefField node itself is just the left side of the assignment, and they're
+ # always wrapped in assign nodes.
+ #
+ # collection[index] = value
+ #
+ class ARefField < Node
+ # [untyped] the value being indexed
+ attr_reader :collection
+
+ # [nil | Args] the value being passed within the brackets
+ attr_reader :index
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(collection:, index:, location:, comments: [])
+ @collection = collection
+ @index = index
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [collection, index]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ collection: collection,
+ index: index,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ q.group do
+ q.format(collection)
+ q.text("[")
+
+ if index
+ q.indent do
+ q.breakable("")
+ q.format(index)
+ end
+ q.breakable("")
+ end
+
+ q.text("]")
+ end
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("aref_field")
+
+ q.breakable
+ q.pp(collection)
+
+ q.breakable
+ q.pp(index)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :aref_field,
+ collection: collection,
+ index: index,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # ArgParen represents wrapping arguments to a method inside a set of
+ # parentheses.
+ #
+ # method(argument)
+ #
+ # In the example above, there would be an ArgParen node around the Args node
+ # that represents the set of arguments being sent to the method method. The
+ # argument child node can be +nil+ if no arguments were passed, as in:
+ #
+ # method()
+ #
+ class ArgParen < Node
+ # [nil | Args | ArgsForward] the arguments inside the
+ # parentheses
+ attr_reader :arguments
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(arguments:, location:, comments: [])
+ @arguments = arguments
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [arguments]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { arguments: arguments, location: location, comments: comments }
+ end
+
+ def format(q)
+ unless arguments
+ q.text("()")
+ return
+ end
+
+ q.group(0, "(", ")") do
+ q.indent do
+ q.breakable("")
+ q.format(arguments)
+ end
+ q.breakable("")
+ end
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("arg_paren")
+
+ q.breakable
+ q.pp(arguments)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :arg_paren,
+ args: arguments,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # Args represents a list of arguments being passed to a method call or array
+ # literal.
+ #
+ # method(first, second, third)
+ #
+ class Args < Node
+ # [Array[ untyped ]] the arguments that this node wraps
+ attr_reader :parts
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(parts:, location:, comments: [])
+ @parts = parts
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ parts
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { parts: parts, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.seplist(parts) { |part| q.format(part) }
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("args")
+
+ q.breakable
+ q.group(2, "(", ")") { q.seplist(parts) { |part| q.pp(part) } }
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :args, parts: parts, loc: location, cmts: comments }.to_json(
+ *opts
+ )
+ end
+ end
+
+ # ArgBlock represents using a block operator on an expression.
+ #
+ # method(&expression)
+ #
+ class ArgBlock < Node
+ # [nil | untyped] the expression being turned into a block
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(value:, location:, comments: [])
+ @value = value
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [value]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { value: value, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.text("&")
+ q.format(value) if value
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("arg_block")
+
+ if value
+ q.breakable
+ q.pp(value)
+ end
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :arg_block, value: value, loc: location, cmts: comments }.to_json(
+ *opts
+ )
+ end
+ end
+
+ # Star represents using a splat operator on an expression.
+ #
+ # method(*arguments)
+ #
+ class ArgStar < Node
+ # [nil | untyped] the expression being splatted
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(value:, location:, comments: [])
+ @value = value
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [value]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { value: value, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.text("*")
+ q.format(value) if value
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("arg_star")
+
+ q.breakable
+ q.pp(value)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :arg_star, value: value, loc: location, cmts: comments }.to_json(
+ *opts
+ )
+ end
+ end
+
+ # ArgsForward represents forwarding all kinds of arguments onto another method
+ # call.
+ #
+ # def request(method, path, **headers, &block); end
+ #
+ # def get(...)
+ # request(:GET, ...)
+ # end
+ #
+ # def post(...)
+ # request(:POST, ...)
+ # end
+ #
+ # In the example above, both the get and post methods are forwarding all of
+ # their arguments (positional, keyword, and block) on to the request method.
+ # The ArgsForward node appears in both the caller (the request method calls)
+ # and the callee (the get and post definitions).
+ class ArgsForward < Node
+ # [String] the value of the operator
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(value:, location:, comments: [])
+ @value = value
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ []
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { value: value, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.text(value)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("args_forward")
+
+ q.breakable
+ q.pp(value)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :args_forward,
+ value: value,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # ArrayLiteral represents an array literal, which can optionally contain
+ # elements.
+ #
+ # []
+ # [one, two, three]
+ #
+ class ArrayLiteral < Node
+ class QWordsFormatter
+ # [Args] the contents of the array
+ attr_reader :contents
+
+ def initialize(contents)
+ @contents = contents
+ end
+
+ def format(q)
+ q.group(0, "%w[", "]") do
+ q.indent do
+ q.breakable("")
+ q.seplist(contents.parts, -> { q.breakable }) do |part|
+ if part.is_a?(StringLiteral)
+ q.format(part.parts.first)
+ else
+ q.text(part.value[1..-1])
+ end
+ end
+ end
+ q.breakable("")
+ end
+ end
+ end
+
+ class QSymbolsFormatter
+ # [Args] the contents of the array
+ attr_reader :contents
+
+ def initialize(contents)
+ @contents = contents
+ end
+
+ def format(q)
+ q.group(0, "%i[", "]") do
+ q.indent do
+ q.breakable("")
+ q.seplist(contents.parts, -> { q.breakable }) do |part|
+ q.format(part.value)
+ end
+ end
+ q.breakable("")
+ end
+ end
+ end
+
+ class VarRefsFormatter
+ # [Args] the contents of the array
+ attr_reader :contents
+
+ def initialize(contents)
+ @contents = contents
+ end
+
+ def format(q)
+ q.group(0, "[", "]") do
+ q.indent do
+ q.breakable("")
+
+ separator = -> do
+ q.text(",")
+ q.fill_breakable
+ end
+
+ q.seplist(contents.parts, separator) { |part| q.format(part) }
+ end
+ q.breakable("")
+ end
+ end
+ end
+
+ # [LBracket] the bracket that opens this array
+ attr_reader :lbracket
+
+ # [nil | Args] the contents of the array
+ attr_reader :contents
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(lbracket:, contents:, location:, comments: [])
+ @lbracket = lbracket
+ @contents = contents
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [lbracket, contents]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ lbracket: lbracket,
+ contents: contents,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ if qwords?
+ QWordsFormatter.new(contents).format(q)
+ return
+ end
+
+ if qsymbols?
+ QSymbolsFormatter.new(contents).format(q)
+ return
+ end
+
+ if var_refs?(q)
+ VarRefsFormatter.new(contents).format(q)
+ return
+ end
+
+ q.group do
+ q.format(lbracket)
+
+ if contents
+ q.indent do
+ q.breakable("")
+ q.format(contents)
+ end
+ end
+
+ q.breakable("")
+ q.text("]")
+ end
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("array")
+
+ q.breakable
+ q.pp(contents)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :array, cnts: contents, loc: location, cmts: comments }.to_json(
+ *opts
+ )
+ end
+
+ private
+
+ def qwords?
+ lbracket.comments.empty? && contents && contents.comments.empty? &&
+ contents.parts.length > 1 &&
+ contents.parts.all? do |part|
+ case part
+ when StringLiteral
+ part.comments.empty? && part.parts.length == 1 &&
+ part.parts.first.is_a?(TStringContent) &&
+ !part.parts.first.value.match?(/[\s\[\]\\]/)
+ when CHAR
+ !part.value.match?(/[\[\]\\]/)
+ else
+ false
+ end
+ end
+ end
+
+ def qsymbols?
+ lbracket.comments.empty? && contents && contents.comments.empty? &&
+ contents.parts.length > 1 &&
+ contents.parts.all? do |part|
+ part.is_a?(SymbolLiteral) && part.comments.empty?
+ end
+ end
+
+ def var_refs?(q)
+ lbracket.comments.empty? && contents && contents.comments.empty? &&
+ contents.parts.all? do |part|
+ part.is_a?(VarRef) && part.comments.empty?
+ end &&
+ (
+ contents.parts.sum { |part| part.value.value.length + 2 } >
+ q.maxwidth * 2
+ )
+ end
+ end
+
+ # AryPtn represents matching against an array pattern using the Ruby 2.7+
+ # pattern matching syntax. It’s one of the more complicated nodes, because
+ # the four parameters that it accepts can almost all be nil.
+ #
+ # case [1, 2, 3]
+ # in [Integer, Integer]
+ # "matched"
+ # in Container[Integer, Integer]
+ # "matched"
+ # in [Integer, *, Integer]
+ # "matched"
+ # end
+ #
+ # An AryPtn node is created with four parameters: an optional constant
+ # wrapper, an array of positional matches, an optional splat with identifier,
+ # and an optional array of positional matches that occur after the splat.
+ # All of the in clauses above would create an AryPtn node.
+ class AryPtn < Node
+ class RestFormatter
+ # [VarField] the identifier that represents the remaining positionals
+ attr_reader :value
+
+ def initialize(value)
+ @value = value
+ end
+
+ def comments
+ value.comments
+ end
+
+ def format(q)
+ q.text("*")
+ q.format(value)
+ end
+ end
+
+ # [nil | VarRef] 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
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(
+ constant:,
+ requireds:,
+ rest:,
+ posts:,
+ location:,
+ comments: []
+ )
+ @constant = constant
+ @requireds = requireds
+ @rest = rest
+ @posts = posts
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [constant, *requireds, rest, *posts]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ constant: constant,
+ requireds: requireds,
+ rest: rest,
+ posts: posts,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ parts = [*requireds]
+ parts << RestFormatter.new(rest) if rest
+ parts += posts
+
+ if constant
+ q.group do
+ q.format(constant)
+ q.text("[")
+ q.seplist(parts) { |part| q.format(part) }
+ q.text("]")
+ end
+
+ return
+ end
+
+ parent = q.parent
+ if parts.length == 1 || PATTERNS.include?(parent.class)
+ q.text("[")
+ q.seplist(parts) { |part| q.format(part) }
+ q.text("]")
+ else
+ q.group { q.seplist(parts) { |part| q.format(part) } }
+ end
+ 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
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :aryptn,
+ constant: constant,
+ reqs: requireds,
+ rest: rest,
+ posts: posts,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # Determins if the following value should be indented or not.
+ module AssignFormatting
+ def self.skip_indent?(value)
+ (value.is_a?(Call) && skip_indent?(value.receiver)) ||
+ [
+ ArrayLiteral,
+ HashLiteral,
+ Heredoc,
+ Lambda,
+ QSymbols,
+ QWords,
+ Symbols,
+ Words
+ ].include?(value.class)
+ end
+ end
+
+ # Assign represents assigning something to a variable or constant. Generally,
+ # the left side of the assignment is going to be any node that ends with the
+ # name "Field".
+ #
+ # variable = value
+ #
+ class Assign < Node
+ # [ARefField | ConstPathField | Field | TopConstField | VarField] the target
+ # to assign the result of the expression to
+ attr_reader :target
+
+ # [untyped] the expression to be assigned
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(target:, value:, location:, comments: [])
+ @target = target
+ @value = value
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [target, value]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { target: target, value: value, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.group do
+ q.format(target)
+ q.text(" =")
+
+ if skip_indent?
+ q.text(" ")
+ q.format(value)
+ else
+ q.indent do
+ q.breakable
+ q.format(value)
+ end
+ end
+ end
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("assign")
+
+ q.breakable
+ q.pp(target)
+
+ q.breakable
+ q.pp(value)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :assign,
+ target: target,
+ value: value,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+
+ private
+
+ def skip_indent?
+ target.comments.empty? &&
+ (target.is_a?(ARefField) || AssignFormatting.skip_indent?(value))
+ end
+ end
+
+ # Assoc represents a key-value pair within a hash. It is a child node of
+ # either an AssocListFromArgs or a BareAssocHash.
+ #
+ # { key1: value1, key2: value2 }
+ #
+ # In the above example, the would be two AssocNew nodes.
+ class Assoc < Node
+ # [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
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(key:, value:, location:, comments: [])
+ @key = key
+ @value = value
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [key, value]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { key: key, value: value, location: location, comments: comments }
+ end
+
+ def format(q)
+ if value&.is_a?(HashLiteral)
+ format_contents(q)
+ else
+ q.group { format_contents(q) }
+ end
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("assoc")
+
+ q.breakable
+ q.pp(key)
+
+ if value
+ q.breakable
+ q.pp(value)
+ end
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :assoc,
+ key: key,
+ value: value,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+
+ private
+
+ def format_contents(q)
+ q.parent.format_key(q, key)
+ return unless value
+
+ if key.comments.empty? && AssignFormatting.skip_indent?(value)
+ q.text(" ")
+ q.format(value)
+ else
+ q.indent do
+ q.breakable
+ q.format(value)
+ end
+ end
+ end
+ end
+
+ # AssocSplat represents double-splatting a value into a hash (either a hash
+ # literal or a bare hash in a method call).
+ #
+ # { **pairs }
+ #
+ class AssocSplat < Node
+ # [untyped] the expression that is being splatted
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(value:, location:, comments: [])
+ @value = value
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [value]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { value: value, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.text("**")
+ q.format(value)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("assoc_splat")
+
+ q.breakable
+ q.pp(value)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :assoc_splat,
+ value: value,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # Backref represents a global variable referencing a matched value. It comes
+ # in the form of a $ followed by a positive integer.
+ #
+ # $1
+ #
+ class Backref < Node
+ # [String] the name of the global backreference variable
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(value:, location:, comments: [])
+ @value = value
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ []
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { value: value, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.text(value)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("backref")
+
+ q.breakable
+ q.pp(value)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :backref, value: value, loc: location, cmts: comments }.to_json(
+ *opts
+ )
+ end
+ end
+
+ # Backtick represents the use of the ` operator. It's usually found being used
+ # for an XStringLiteral, but could also be found as the name of a method being
+ # defined.
+ class Backtick < Node
+ # [String] the backtick in the string
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(value:, location:, comments: [])
+ @value = value
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ []
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { value: value, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.text(value)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("backtick")
+
+ q.breakable
+ q.pp(value)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :backtick, value: value, loc: location, cmts: comments }.to_json(
+ *opts
+ )
+ end
+ end
+
+ # This module is responsible for formatting the assocs contained within a
+ # hash or bare hash. It first determines if every key in the hash can use
+ # labels. If it can, it uses labels. Otherwise it uses hash rockets.
+ module HashKeyFormatter
+ class Labels
+ def format_key(q, key)
+ case key
+ when Label
+ q.format(key)
+ when SymbolLiteral
+ q.format(key.value)
+ q.text(":")
+ when DynaSymbol
+ q.format(key)
+ q.text(":")
+ end
+ end
+ end
+
+ class Rockets
+ def format_key(q, key)
+ case key
+ when Label
+ q.text(":")
+ q.text(key.value.chomp(":"))
+ when DynaSymbol
+ q.text(":")
+ q.format(key)
+ else
+ q.format(key)
+ end
+
+ q.text(" =>")
+ end
+ end
+
+ def self.for(container)
+ labels =
+ container.assocs.all? do |assoc|
+ next true if assoc.is_a?(AssocSplat)
+
+ case assoc.key
+ when Label
+ true
+ when SymbolLiteral
+ # When attempting to convert a hash rocket into a hash label,
+ # you need to take care because only certain patterns are
+ # allowed. Ruby source says that they have to match keyword
+ # arguments to methods, but don't specify what that is. After
+ # some experimentation, it looks like it's:
+ value = assoc.key.value.value
+ value.match?(/^[_A-Za-z]/) && !value.end_with?("=")
+ when DynaSymbol
+ true
+ else
+ false
+ end
+ end
+
+ (labels ? Labels : Rockets).new
+ end
+ end
+
+ # BareAssocHash represents a hash of contents being passed as a method
+ # argument (and therefore has omitted braces). It's very similar to an
+ # AssocListFromArgs node.
+ #
+ # method(key1: value1, key2: value2)
+ #
+ class BareAssocHash < Node
+ # [Array[ AssocNew | AssocSplat ]]
+ attr_reader :assocs
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(assocs:, location:, comments: [])
+ @assocs = assocs
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ assocs
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { assocs: assocs, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.seplist(assocs) { |assoc| q.format(assoc) }
+ end
+
+ def format_key(q, key)
+ (@key_formatter ||= HashKeyFormatter.for(self)).format_key(q, key)
+ 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) } }
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :bare_assoc_hash,
+ assocs: assocs,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # Begin represents a begin..end chain.
+ #
+ # begin
+ # value
+ # end
+ #
+ class Begin < Node
+ # [BodyStmt] the bodystmt that contains the contents of this begin block
+ attr_reader :bodystmt
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(bodystmt:, location:, comments: [])
+ @bodystmt = bodystmt
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [bodystmt]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { bodystmt: bodystmt, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.text("begin")
+
+ unless bodystmt.empty?
+ q.indent do
+ q.breakable(force: true) unless bodystmt.statements.empty?
+ q.format(bodystmt)
+ end
+ end
+
+ q.breakable(force: true)
+ q.text("end")
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("begin")
+
+ q.breakable
+ q.pp(bodystmt)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :begin,
+ bodystmt: bodystmt,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # PinnedBegin represents a pinning a nested statement within pattern matching.
+ #
+ # case value
+ # in ^(statement)
+ # end
+ #
+ class PinnedBegin < Node
+ # [untyped] the expression being pinned
+ attr_reader :statement
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(statement:, location:, comments: [])
+ @statement = statement
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [statement]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { statement: statement, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.group do
+ q.text("^(")
+ q.nest(1) do
+ q.indent do
+ q.breakable("")
+ q.format(statement)
+ end
+ q.breakable("")
+ q.text(")")
+ end
+ end
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("pinned_begin")
+
+ q.breakable
+ q.pp(statement)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :pinned_begin,
+ stmt: statement,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+
+ # Binary represents any expression that involves two sub-expressions with an
+ # operator in between. This can be something that looks like a mathematical
+ # operation:
+ #
+ # 1 + 1
+ #
+ # but can also be something like pushing a value onto an array:
+ #
+ # array << value
+ #
+ class Binary < Node
+ # [untyped] the left-hand side of the expression
+ attr_reader :left
+
+ # [Symbol] 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
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(left:, operator:, right:, location:, comments: [])
+ @left = left
+ @operator = operator
+ @right = right
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [left, right]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ left: left,
+ operator: operator,
+ right: right,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ power = operator == :**
+
+ q.group do
+ q.group { q.format(left) }
+ q.text(" ") unless power
+
+ q.group do
+ q.text(operator)
+
+ q.indent do
+ q.breakable(power ? "" : " ")
+ q.format(right)
+ end
+ end
+ end
+ 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)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :binary,
+ left: left,
+ op: operator,
+ right: right,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # This module will remove any breakables from the list of contents so that no
+ # newlines are present in the output.
+ module RemoveBreaks
+ class << self
+ def call(doc)
+ marker = Object.new
+ stack = [doc]
+
+ while stack.any?
+ doc = stack.pop
+
+ if doc == marker
+ stack.pop
+ next
+ end
+
+ stack += [doc, marker]
+
+ case doc
+ when PrettyPrint::Align, PrettyPrint::Indent, PrettyPrint::Group
+ doc.contents.map! { |child| remove_breaks(child) }
+ stack += doc.contents.reverse
+ when PrettyPrint::IfBreak
+ doc.flat_contents.map! { |child| remove_breaks(child) }
+ stack += doc.flat_contents.reverse
+ end
+ end
+ end
+
+ private
+
+ def remove_breaks(doc)
+ case doc
+ when PrettyPrint::Breakable
+ text = PrettyPrint::Text.new
+ text.add(object: doc.force? ? "; " : doc.separator, width: doc.width)
+ text
+ when PrettyPrint::IfBreak
+ PrettyPrint::Align.new(indent: 0, contents: doc.flat_contents)
+ else
+ doc
+ end
+ end
+ end
+ end
+
+ # BlockVar represents the parameters being declared for a block. Effectively
+ # this node is everything contained within the pipes. This includes all of the
+ # various parameter types, as well as block-local variable declarations.
+ #
+ # method do |positional, optional = value, keyword:, █ local|
+ # end
+ #
+ class BlockVar < Node
+ # [Params] the parameters being declared with the block
+ attr_reader :params
+
+ # [Array[ Ident ]] the list of block-local variable declarations
+ attr_reader :locals
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(params:, locals:, location:, comments: [])
+ @params = params
+ @locals = locals
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [params, *locals]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { params: params, locals: locals, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.group(0, "|", "|") do
+ doc = q.format(params)
+ RemoveBreaks.call(doc)
+
+ if locals.any?
+ q.text("; ")
+ q.seplist(locals, -> { q.text(", ") }) { |local| q.format(local) }
+ end
+ end
+ 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
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :block_var,
+ params: params,
+ locals: locals,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # BlockArg represents declaring a block parameter on a method definition.
+ #
+ # def method(&block); end
+ #
+ class BlockArg < Node
+ # [nil | Ident] the name of the block argument
+ attr_reader :name
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(name:, location:, comments: [])
+ @name = name
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [name]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { name: name, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.text("&")
+ q.format(name) if name
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("blockarg")
+
+ if name
+ q.breakable
+ q.pp(name)
+ end
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :blockarg, name: name, loc: location, cmts: comments }.to_json(
+ *opts
+ )
+ end
+ end
+
+ # bodystmt can't actually determine its bounds appropriately because it
+ # doesn't necessarily know where it started. So the parent node needs to
+ # report back down into this one where it goes.
+ class BodyStmt < Node
+ # [Statements] the list of statements inside the begin clause
+ attr_reader :statements
+
+ # [nil | Rescue] the optional rescue chain attached to the begin clause
+ attr_reader :rescue_clause
+
+ # [nil | 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
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(
+ statements:,
+ rescue_clause:,
+ else_clause:,
+ ensure_clause:,
+ location:,
+ comments: []
+ )
+ @statements = statements
+ @rescue_clause = rescue_clause
+ @else_clause = else_clause
+ @ensure_clause = ensure_clause
+ @location = location
+ @comments = comments
+ 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 empty?
+ statements.empty? && !rescue_clause && !else_clause && !ensure_clause
+ end
+
+ def child_nodes
+ [statements, rescue_clause, else_clause, ensure_clause]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ statements: statements,
+ rescue_clause: rescue_clause,
+ else_clause: else_clause,
+ ensure_clause: ensure_clause,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ q.group do
+ q.format(statements) unless statements.empty?
+
+ if rescue_clause
+ q.nest(-2) do
+ q.breakable(force: true)
+ q.format(rescue_clause)
+ end
+ end
+
+ if else_clause
+ q.nest(-2) do
+ q.breakable(force: true)
+ q.text("else")
+ end
+ q.breakable(force: true)
+ q.format(else_clause)
+ end
+
+ if ensure_clause
+ q.nest(-2) do
+ q.breakable(force: true)
+ q.format(ensure_clause)
+ end
+ end
+ 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
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :bodystmt,
+ stmts: statements,
+ rsc: rescue_clause,
+ els: else_clause,
+ ens: ensure_clause,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # Responsible for formatting either a BraceBlock or a DoBlock.
+ class BlockFormatter
+ class BlockOpenFormatter
+ # [String] the actual output that should be printed
+ attr_reader :text
+
+ # [LBrace | Keyword] the node that is being represented
+ attr_reader :node
+
+ def initialize(text, node)
+ @text = text
+ @node = node
+ end
+
+ def comments
+ node.comments
+ end
+
+ def format(q)
+ q.text(text)
+ end
+ end
+
+ # [BraceBlock | DoBlock] the block node to be formatted
+ attr_reader :node
+
+ # [LBrace | Keyword] the node that opens the block
+ attr_reader :block_open
+
+ # [String] the string that closes the block
+ attr_reader :block_close
+
+ # [BodyStmt | Statements] the statements inside the block
+ attr_reader :statements
+
+ def initialize(node, block_open, block_close, statements)
+ @node = node
+ @block_open = block_open
+ @block_close = block_close
+ @statements = statements
+ end
+
+ def format(q)
+ # If this is nested anywhere inside of a Command or CommandCall node, then
+ # we can't change which operators we're using for the bounds of the block.
+ break_opening, break_closing, flat_opening, flat_closing =
+ if unchangeable_bounds?(q)
+ [block_open.value, block_close, block_open.value, block_close]
+ elsif forced_do_end_bounds?(q)
+ %w[do end do end]
+ elsif forced_brace_bounds?(q)
+ %w[{ } { }]
+ else
+ %w[do end { }]
+ end
+
+ # If the receiver of this block a Command or CommandCall node, then there
+ # are no parentheses around the arguments to that command, so we need to
+ # break the block.
+ receiver = q.parent.call
+ if receiver.is_a?(Command) || receiver.is_a?(CommandCall)
+ q.break_parent
+ format_break(q, break_opening, break_closing)
+ return
+ end
+
+ q.group do
+ q.if_break { format_break(q, break_opening, break_closing) }.if_flat do
+ format_flat(q, flat_opening, flat_closing)
+ end
+ end
+ end
+
+ private
+
+ # If this is nested anywhere inside certain nodes, then we can't change
+ # which operators/keywords we're using for the bounds of the block.
+ def unchangeable_bounds?(q)
+ q.parents.any? do |parent|
+ # If we hit a statements, then we're safe to use whatever since we
+ # know for certain we're going to get split over multiple lines
+ # anyway.
+ break false if parent.is_a?(Statements)
+
+ [Command, CommandCall].include?(parent.class)
+ end
+ end
+
+ # If we're a sibling of a control-flow keyword, then we're going to have to
+ # use the do..end bounds.
+ def forced_do_end_bounds?(q)
+ [Break, Next, Return, Super].include?(q.parent.call.class)
+ end
+
+ # If we're the predicate of a loop or conditional, then we're going to have
+ # to go with the {..} bounds.
+ def forced_brace_bounds?(q)
+ parents = q.parents.to_a
+ parents.each_with_index.any? do |parent, index|
+ # If we hit certain breakpoints then we know we're safe.
+ break false if [Paren, Statements].include?(parent.class)
+
+ [
+ If,
+ IfMod,
+ IfOp,
+ Unless,
+ UnlessMod,
+ While,
+ WhileMod,
+ Until,
+ UntilMod
+ ].include?(parent.class) && parent.predicate == parents[index - 1]
+ end
+ end
+
+ def format_break(q, opening, closing)
+ q.text(" ")
+ q.format(BlockOpenFormatter.new(opening, block_open), stackable: false)
+
+ if node.block_var
+ q.text(" ")
+ q.format(node.block_var)
+ end
+
+ unless statements.empty?
+ q.indent do
+ q.breakable
+ q.format(statements)
+ end
+ end
+
+ q.breakable
+ q.text(closing)
+ end
+
+ def format_flat(q, opening, closing)
+ q.text(" ")
+ q.format(BlockOpenFormatter.new(opening, block_open), stackable: false)
+
+ if node.block_var
+ q.breakable
+ q.format(node.block_var)
+ q.breakable
+ end
+
+ if statements.empty?
+ q.text(" ") if opening == "do"
+ else
+ q.breakable unless node.block_var
+ q.format(statements)
+ q.breakable
+ end
+
+ q.text(closing)
+ end
+ end
+
+ # BraceBlock represents passing a block to a method call using the { }
+ # operators.
+ #
+ # method { |variable| variable + 1 }
+ #
+ class BraceBlock < Node
+ # [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
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(lbrace:, block_var:, statements:, location:, comments: [])
+ @lbrace = lbrace
+ @block_var = block_var
+ @statements = statements
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [lbrace, block_var, statements]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ lbrace: lbrace,
+ block_var: block_var,
+ statements: statements,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ BlockFormatter.new(self, lbrace, "}", statements).format(q)
+ 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)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :brace_block,
+ lbrace: lbrace,
+ block_var: block_var,
+ stmts: statements,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # Formats either a Break or Next node.
+ class FlowControlFormatter
+ # [String] the keyword to print
+ attr_reader :keyword
+
+ # [Break | Next] the node being formatted
+ attr_reader :node
+
+ def initialize(keyword, node)
+ @keyword = keyword
+ @node = node
+ end
+
+ def format(q)
+ arguments = node.arguments
+
+ q.group do
+ q.text(keyword)
+
+ if arguments.parts.any?
+ if arguments.parts.length == 1
+ part = arguments.parts.first
+
+ if part.is_a?(Paren)
+ q.format(arguments)
+ elsif part.is_a?(ArrayLiteral)
+ q.text(" ")
+ q.format(arguments)
+ else
+ format_arguments(q, "(", ")")
+ end
+ else
+ format_arguments(q, " [", "]")
+ end
+ end
+ end
+ end
+
+ private
+
+ def format_arguments(q, opening, closing)
+ q.if_break { q.text(opening) }
+ q.indent do
+ q.breakable(" ")
+ q.format(node.arguments)
+ end
+ q.breakable("")
+ q.if_break { q.text(closing) }
+ end
+ end
+
+ # Break represents using the +break+ keyword.
+ #
+ # break
+ #
+ # It can also optionally accept arguments, as in:
+ #
+ # break 1
+ #
+ class Break < Node
+ # [Args] the arguments being sent to the keyword
+ attr_reader :arguments
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(arguments:, location:, comments: [])
+ @arguments = arguments
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [arguments]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { arguments: arguments, location: location, comments: comments }
+ end
+
+ def format(q)
+ FlowControlFormatter.new("break", self).format(q)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("break")
+
+ q.breakable
+ q.pp(arguments)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :break, args: arguments, loc: location, cmts: comments }.to_json(
+ *opts
+ )
+ end
+ end
+
+ # Wraps a call operator (which can be a string literal :: or an Op node or a
+ # Period node) and formats it when called.
+ class CallOperatorFormatter
+ # [:"::" | Op | Period] the operator being formatted
+ attr_reader :operator
+
+ def initialize(operator)
+ @operator = operator
+ end
+
+ def comments
+ operator == :"::" ? [] : operator.comments
+ end
+
+ def format(q)
+ if operator == :"::" || (operator.is_a?(Op) && operator.value == "::")
+ q.text(".")
+ else
+ operator.format(q)
+ end
+ end
+ end
+
+ # Call represents a method call.
+ #
+ # receiver.message
+ #
+ class Call < Node
+ # [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
+
+ # [nil | ArgParen | Args] the arguments to the method call
+ attr_reader :arguments
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(
+ receiver:,
+ operator:,
+ message:,
+ arguments:,
+ location:,
+ comments: []
+ )
+ @receiver = receiver
+ @operator = operator
+ @message = message
+ @arguments = arguments
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [
+ receiver,
+ (operator if operator != :"::"),
+ (message if message != :call),
+ arguments
+ ]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ receiver: receiver,
+ operator: operator,
+ message: message,
+ arguments: arguments,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ call_operator = CallOperatorFormatter.new(operator)
+
+ q.group do
+ q.format(receiver)
+
+ # If there are trailing comments on the call operator, then we need to
+ # use the trailing form as opposed to the leading form.
+ q.format(call_operator) if call_operator.comments.any?
+
+ q.group do
+ q.indent do
+ if receiver.comments.any? || call_operator.comments.any?
+ q.breakable(force: true)
+ end
+
+ if call_operator.comments.empty?
+ q.format(call_operator, stackable: false)
+ end
+
+ q.format(message) if message != :call
+ end
+
+ q.format(arguments) if arguments
+ end
+ end
+ 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)
+
+ if arguments
+ q.breakable
+ q.pp(arguments)
+ end
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :call,
+ receiver: receiver,
+ op: operator,
+ message: message,
+ args: arguments,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # Case represents the beginning of a case chain.
+ #
+ # case value
+ # when 1
+ # "one"
+ # when 2
+ # "two"
+ # else
+ # "number"
+ # end
+ #
+ class Case < Node
+ # [Kw] the keyword that opens this expression
+ attr_reader :keyword
+
+ # [nil | 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
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(keyword:, value:, consequent:, location:, comments: [])
+ @keyword = keyword
+ @value = value
+ @consequent = consequent
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [keyword, value, consequent]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ keyword: keyword,
+ value: value,
+ consequent: consequent,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ q.group do
+ q.format(keyword)
+
+ if value
+ q.text(" ")
+ q.format(value)
+ end
+
+ q.breakable(force: true)
+ q.format(consequent)
+ q.breakable(force: true)
+
+ q.text("end")
+ end
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("case")
+
+ q.breakable
+ q.pp(keyword)
+
+ if value
+ q.breakable
+ q.pp(value)
+ end
+
+ q.breakable
+ q.pp(consequent)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :case,
+ value: value,
+ cons: consequent,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # RAssign represents a single-line pattern match.
+ #
+ # value in pattern
+ # value => pattern
+ #
+ class RAssign < Node
+ # [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
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(value:, operator:, pattern:, location:, comments: [])
+ @value = value
+ @operator = operator
+ @pattern = pattern
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [value, operator, pattern]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ value: value,
+ operator: operator,
+ pattern: pattern,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ q.group do
+ q.format(value)
+ q.text(" ")
+ q.format(operator)
+ q.group do
+ q.indent do
+ q.breakable
+ q.format(pattern)
+ end
+ end
+ end
+ 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)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :rassign,
+ value: value,
+ op: operator,
+ pattern: pattern,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # Class represents defining a class using the +class+ keyword.
+ #
+ # class Container
+ # end
+ #
+ # Classes can have path names as their class name in case it's being nested
+ # under a namespace, as in:
+ #
+ # class Namespace::Container
+ # end
+ #
+ # Classes can also be defined as a top-level path, in the case that it's
+ # already in a namespace but you want to define it at the top-level instead,
+ # as in:
+ #
+ # module OtherNamespace
+ # class ::Namespace::Container
+ # end
+ # end
+ #
+ # All of these declarations can also have an optional superclass reference, as
+ # in:
+ #
+ # class Child < Parent
+ # end
+ #
+ # That superclass can actually be any Ruby expression, it doesn't necessarily
+ # need to be a constant, as in:
+ #
+ # class Child < method
+ # end
+ #
+ class ClassDeclaration < Node
+ # [ConstPathRef | ConstRef | TopConstRef] the name of the class being
+ # defined
+ attr_reader :constant
+
+ # [nil | 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
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(constant:, superclass:, bodystmt:, location:, comments: [])
+ @constant = constant
+ @superclass = superclass
+ @bodystmt = bodystmt
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [constant, superclass, bodystmt]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ constant: constant,
+ superclass: superclass,
+ bodystmt: bodystmt,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ declaration = -> do
+ q.group do
+ q.text("class ")
+ q.format(constant)
+
+ if superclass
+ q.text(" < ")
+ q.format(superclass)
+ end
+ end
+ end
+
+ if bodystmt.empty?
+ q.group do
+ declaration.call
+ q.breakable(force: true)
+ q.text("end")
+ end
+ else
+ q.group do
+ declaration.call
+
+ q.indent do
+ q.breakable(force: true)
+ q.format(bodystmt)
+ end
+
+ q.breakable(force: true)
+ q.text("end")
+ end
+ end
+ 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)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :class,
+ constant: constant,
+ superclass: superclass,
+ bodystmt: bodystmt,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # Comma represents the use of the , operator.
+ class Comma < Node
+ # [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
+
+ # Command represents a method call with arguments and no parentheses. Note
+ # that Command nodes only happen when there is no explicit receiver for this
+ # method.
+ #
+ # method argument
+ #
+ class Command < Node
+ # [Const | Ident] the message being sent to the implicit receiver
+ attr_reader :message
+
+ # [Args] the arguments being sent with the message
+ attr_reader :arguments
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(message:, arguments:, location:, comments: [])
+ @message = message
+ @arguments = arguments
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [message, arguments]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ message: message,
+ arguments: arguments,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ q.group do
+ q.format(message)
+ q.text(" ")
+ q.nest(message.value.length + 1) { q.format(arguments) }
+ end
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("command")
+
+ q.breakable
+ q.pp(message)
+
+ q.breakable
+ q.pp(arguments)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :command,
+ message: message,
+ args: arguments,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # CommandCall represents a method call on an object with arguments and no
+ # parentheses.
+ #
+ # object.method argument
+ #
+ class CommandCall < Node
+ # [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
+
+ # [nil | Args] the arguments going along with the message
+ attr_reader :arguments
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(
+ receiver:,
+ operator:,
+ message:,
+ arguments:,
+ location:,
+ comments: []
+ )
+ @receiver = receiver
+ @operator = operator
+ @message = message
+ @arguments = arguments
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [receiver, message, arguments]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ receiver: receiver,
+ operator: operator,
+ message: message,
+ arguments: arguments,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ q.group do
+ doc =
+ q.nest(0) do
+ q.format(receiver)
+ q.format(CallOperatorFormatter.new(operator), stackable: false)
+ q.format(message)
+ end
+
+ if arguments
+ q.text(" ")
+ q.nest(argument_alignment(q, doc)) { q.format(arguments) }
+ end
+ end
+ 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)
+
+ if arguments
+ q.breakable
+ q.pp(arguments)
+ end
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :command_call,
+ receiver: receiver,
+ op: operator,
+ message: message,
+ args: arguments,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+
+ private
+
+ # This is a somewhat naive method that is attempting to sum up the width of
+ # the doc nodes that make up the given doc node. This is used to align
+ # content.
+ def doc_width(parent)
+ queue = [parent]
+ width = 0
+
+ until queue.empty?
+ doc = queue.shift
+
+ case doc
+ when PrettyPrint::Text
+ width += doc.width
+ when PrettyPrint::Indent, PrettyPrint::Align, PrettyPrint::Group
+ queue = doc.contents + queue
+ when PrettyPrint::IfBreak
+ queue = doc.break_contents + queue
+ when PrettyPrint::Breakable
+ width = 0
+ end
+ end
+
+ width
+ end
+
+ def argument_alignment(q, doc)
+ # Very special handling case for rspec matchers. In general with rspec
+ # matchers you expect to see something like:
+ #
+ # expect(foo).to receive(:bar).with(
+ # 'one',
+ # 'two',
+ # 'three',
+ # 'four',
+ # 'five'
+ # )
+ #
+ # In this case the arguments are aligned to the left side as opposed to
+ # being aligned with the `receive` call.
+ if %w[to not_to to_not].include?(message.value)
+ 0
+ else
+ width = doc_width(doc) + 1
+ width > (q.maxwidth / 2) ? 0 : width
+ end
+ end
+ end
+
+ # Comment represents a comment in the source.
+ #
+ # # comment
+ #
+ class Comment < Node
+ class List
+ # [Array[ Comment ]] the list of comments this list represents
+ attr_reader :comments
+
+ def initialize(comments)
+ @comments = comments
+ end
+
+ def pretty_print(q)
+ return if comments.empty?
+
+ q.breakable
+ q.group(2, "(", ")") { q.seplist(comments) { |comment| q.pp(comment) } }
+ end
+ end
+
+ # [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
+
+ @leading = false
+ @trailing = false
+ end
+
+ def leading!
+ @leading = true
+ end
+
+ def leading?
+ @leading
+ end
+
+ def trailing!
+ @trailing = true
+ end
+
+ def trailing?
+ @trailing
+ end
+
+ def ignore?
+ value[1..-1].strip == "stree-ignore"
+ end
+
+ def comments
+ []
+ end
+
+ def child_nodes
+ []
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { value: value, inline: inline, location: location }
+ end
+
+ def format(q)
+ q.text(value)
+ 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
+
+ # Const represents a literal value that _looks_ like a constant. This could
+ # actually be a reference to a constant:
+ #
+ # Constant
+ #
+ # It could also be something that looks like a constant in another context, as
+ # in a method call to a capitalized method:
+ #
+ # object.Constant
+ #
+ # or a symbol that starts with a capital letter:
+ #
+ # :Constant
+ #
+ class Const < Node
+ # [String] the name of the constant
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(value:, location:, comments: [])
+ @value = value
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ []
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { value: value, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.text(value)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("const")
+
+ q.breakable
+ q.pp(value)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :const, value: value, loc: location, cmts: comments }.to_json(
+ *opts
+ )
+ end
+ end
+
+ # ConstPathField represents the child node of some kind of assignment. It
+ # represents when you're assigning to a constant that is being referenced as
+ # a child of another variable.
+ #
+ # object::Const = value
+ #
+ class ConstPathField < Node
+ # [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
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(parent:, constant:, location:, comments: [])
+ @parent = parent
+ @constant = constant
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [parent, constant]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ parent: parent,
+ constant: constant,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ q.format(parent)
+ q.text("::")
+ q.format(constant)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("const_path_field")
+
+ q.breakable
+ q.pp(parent)
+
+ q.breakable
+ q.pp(constant)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :const_path_field,
+ parent: parent,
+ constant: constant,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # ConstPathRef represents referencing a constant by a path.
+ #
+ # object::Const
+ #
+ class ConstPathRef < Node
+ # [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
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(parent:, constant:, location:, comments: [])
+ @parent = parent
+ @constant = constant
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [parent, constant]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ parent: parent,
+ constant: constant,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ q.format(parent)
+ q.text("::")
+ q.format(constant)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("const_path_ref")
+
+ q.breakable
+ q.pp(parent)
+
+ q.breakable
+ q.pp(constant)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :const_path_ref,
+ parent: parent,
+ constant: constant,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # ConstRef represents the name of the constant being used in a class or module
+ # declaration.
+ #
+ # class Container
+ # end
+ #
+ class ConstRef < Node
+ # [Const] the constant itself
+ attr_reader :constant
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(constant:, location:, comments: [])
+ @constant = constant
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [constant]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { constant: constant, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.format(constant)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("const_ref")
+
+ q.breakable
+ q.pp(constant)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :const_ref,
+ constant: constant,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # CVar represents the use of a class variable.
+ #
+ # @@variable
+ #
+ class CVar < Node
+ # [String] the name of the class variable
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(value:, location:, comments: [])
+ @value = value
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ []
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { value: value, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.text(value)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("cvar")
+
+ q.breakable
+ q.pp(value)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :cvar, value: value, loc: location, cmts: comments }.to_json(
+ *opts
+ )
+ end
+ end
+
+ # Def represents defining a regular method on the current self object.
+ #
+ # def method(param) result end
+ #
+ class Def < Node
+ # [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
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(name:, params:, bodystmt:, location:, comments: [])
+ @name = name
+ @params = params
+ @bodystmt = bodystmt
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [name, params, bodystmt]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ name: name,
+ params: params,
+ bodystmt: bodystmt,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ q.group do
+ q.group do
+ q.text("def ")
+ q.format(name)
+ q.format(params) if !params.is_a?(Params) || !params.empty?
+ end
+
+ unless bodystmt.empty?
+ q.indent do
+ q.breakable(force: true)
+ q.format(bodystmt)
+ end
+ end
+
+ q.breakable(force: true)
+ q.text("end")
+ end
+ 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)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :def,
+ name: name,
+ params: params,
+ bodystmt: bodystmt,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # DefEndless represents defining a single-line method since Ruby 3.0+.
+ #
+ # def method = result
+ #
+ class DefEndless < Node
+ # [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
+
+ # [nil | Params | 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
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(
+ target:,
+ operator:,
+ name:,
+ paren:,
+ statement:,
+ location:,
+ comments: []
+ )
+ @target = target
+ @operator = operator
+ @name = name
+ @paren = paren
+ @statement = statement
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [target, operator, name, paren, statement]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ target: target,
+ operator: operator,
+ name: name,
+ paren: paren,
+ statement: statement,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ q.group do
+ q.text("def ")
+
+ if target
+ q.format(target)
+ q.format(CallOperatorFormatter.new(operator), stackable: false)
+ end
+
+ q.format(name)
+
+ if paren
+ params = paren
+ params = params.contents if params.is_a?(Paren)
+ q.format(paren) unless params.empty?
+ end
+
+ q.text(" =")
+ q.group do
+ q.indent do
+ q.breakable
+ q.format(statement)
+ end
+ end
+ end
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("def_endless")
+
+ if target
+ q.breakable
+ q.pp(target)
+
+ q.breakable
+ q.pp(operator)
+ end
+
+ q.breakable
+ q.pp(name)
+
+ if paren
+ q.breakable
+ q.pp(paren)
+ end
+
+ q.breakable
+ q.pp(statement)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :def_endless,
+ name: name,
+ paren: paren,
+ stmt: statement,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # Defined represents the use of the +defined?+ operator. It can be used with
+ # and without parentheses.
+ #
+ # defined?(variable)
+ #
+ class Defined < Node
+ # [untyped] the value being sent to the keyword
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(value:, location:, comments: [])
+ @value = value
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [value]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { value: value, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.group(0, "defined?(", ")") do
+ q.indent do
+ q.breakable("")
+ q.format(value)
+ end
+ q.breakable("")
+ end
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("defined")
+
+ q.breakable
+ q.pp(value)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :defined, value: value, loc: location, cmts: comments }.to_json(
+ *opts
+ )
+ end
+ end
+
+ # Defs represents defining a singleton method on an object.
+ #
+ # def object.method(param) result end
+ #
+ class Defs < Node
+ # [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
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(
+ target:,
+ operator:,
+ name:,
+ params:,
+ bodystmt:,
+ location:,
+ comments: []
+ )
+ @target = target
+ @operator = operator
+ @name = name
+ @params = params
+ @bodystmt = bodystmt
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [target, operator, name, params, bodystmt]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ target: target,
+ operator: operator,
+ name: name,
+ params: params,
+ bodystmt: bodystmt,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ q.group do
+ q.group do
+ q.text("def ")
+ q.format(target)
+ q.format(CallOperatorFormatter.new(operator), stackable: false)
+ q.format(name)
+ q.format(params) if !params.is_a?(Params) || !params.empty?
+ end
+
+ unless bodystmt.empty?
+ q.indent do
+ q.breakable(force: true)
+ q.format(bodystmt)
+ end
+ end
+
+ q.breakable(force: true)
+ q.text("end")
+ end
+ 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)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :defs,
+ target: target,
+ op: operator,
+ name: name,
+ params: params,
+ bodystmt: bodystmt,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # DoBlock represents passing a block to a method call using the +do+ and +end+
+ # keywords.
+ #
+ # method do |value|
+ # end
+ #
+ class DoBlock < Node
+ # [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
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(keyword:, block_var:, bodystmt:, location:, comments: [])
+ @keyword = keyword
+ @block_var = block_var
+ @bodystmt = bodystmt
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [keyword, block_var, bodystmt]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ keyword: keyword,
+ block_var: block_var,
+ bodystmt: bodystmt,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ BlockFormatter.new(self, keyword, "end", bodystmt).format(q)
+ 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)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :do_block,
+ keyword: keyword,
+ block_var: block_var,
+ bodystmt: bodystmt,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # Responsible for formatting Dot2 and Dot3 nodes.
+ class DotFormatter
+ # [String] the operator to display
+ attr_reader :operator
+
+ # [Dot2 | Dot3] the node that is being formatter
+ attr_reader :node
+
+ def initialize(operator, node)
+ @operator = operator
+ @node = node
+ end
+
+ def format(q)
+ space = [If, IfMod, Unless, UnlessMod].include?(q.parent.class)
+
+ left = node.left
+ right = node.right
+
+ q.format(left) if left
+ q.text(" ") if space
+ q.text(operator)
+ q.text(" ") if space
+ q.format(right) if right
+ end
+ 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 < Node
+ # [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
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(left:, right:, location:, comments: [])
+ @left = left
+ @right = right
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [left, right]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { left: left, right: right, location: location, comments: comments }
+ end
+
+ def format(q)
+ DotFormatter.new("..", self).format(q)
+ 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
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :dot2,
+ left: left,
+ right: right,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ 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 < Node
+ # [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
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(left:, right:, location:, comments: [])
+ @left = left
+ @right = right
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [left, right]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { left: left, right: right, location: location, comments: comments }
+ end
+
+ def format(q)
+ DotFormatter.new("...", self).format(q)
+ 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
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :dot3,
+ left: left,
+ right: right,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # Responsible for providing information about quotes to be used for strings
+ # and dynamic symbols.
+ module Quotes
+ # The matching pairs of quotes that can be used with % literals.
+ PAIRS = { "(" => ")", "[" => "]", "{" => "}", "<" => ">" }.freeze
+
+ # If there is some part of this string that matches an escape sequence or
+ # that contains the interpolation pattern ("#{"), then we are locked into
+ # whichever quote the user chose. (If they chose single quotes, then double
+ # quoting would activate the escape sequence, and if they chose double
+ # quotes, then single quotes would deactivate it.)
+ def self.locked?(node)
+ node.parts.any? do |part|
+ part.is_a?(TStringContent) && part.value.match?(/#[@${]|[\\]/)
+ end
+ end
+
+ # Find the matching closing quote for the given opening quote.
+ def self.matching(quote)
+ PAIRS.fetch(quote) { quote }
+ end
+
+ # Escape and unescape single and double quotes as needed to be able to
+ # enclose +content+ with +enclosing+.
+ def self.normalize(content, enclosing)
+ return content if enclosing != "\"" && enclosing != "'"
+
+ content.gsub(/\\([\s\S])|(['"])/) do
+ _match, escaped, quote = Regexp.last_match.to_a
+
+ if quote == enclosing
+ "\\#{quote}"
+ elsif quote
+ quote
+ else
+ "\\#{escaped}"
+ end
+ end
+ end
+ end
+
+ # DynaSymbol represents a symbol literal that uses quotes to dynamically
+ # define its value.
+ #
+ # :"#{variable}"
+ #
+ # They can also be used as a special kind of dynamic hash key, as in:
+ #
+ # { "#{key}": value }
+ #
+ class DynaSymbol < Node
+ # [Array[ StringDVar | StringEmbExpr | TStringContent ]] the parts of the
+ # dynamic symbol
+ attr_reader :parts
+
+ # [String] the quote used to delimit the dynamic symbol
+ attr_reader :quote
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(parts:, quote:, location:, comments: [])
+ @parts = parts
+ @quote = quote
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ parts
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { parts: parts, quote: quote, location: location, comments: comments }
+ end
+
+ def format(q)
+ opening_quote, closing_quote = quotes(q)
+
+ q.group(0, opening_quote, closing_quote) do
+ parts.each do |part|
+ if part.is_a?(TStringContent)
+ value = Quotes.normalize(part.value, closing_quote)
+ separator = -> { q.breakable(force: true, indent: false) }
+ q.seplist(value.split(/\r?\n/, -1), separator) do |text|
+ q.text(text)
+ end
+ else
+ q.format(part)
+ end
+ end
+ end
+ 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) } }
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :dyna_symbol,
+ parts: parts,
+ quote: quote,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+
+ private
+
+ # Here we determine the quotes to use for a dynamic symbol. It's bound by a
+ # lot of rules because it could be in many different contexts with many
+ # different kinds of escaping.
+ def quotes(q)
+ # If we're inside of an assoc node as the key, then it will handle
+ # printing the : on its own since it could change sides.
+ parent = q.parent
+ hash_key = parent.is_a?(Assoc) && parent.key == self
+
+ if quote.start_with?("%s")
+ # Here we're going to check if there is a closing character, a new line,
+ # or a quote in the content of the dyna symbol. If there is, then
+ # quoting could get weird, so just bail out and stick to the original
+ # quotes in the source.
+ matching = Quotes.matching(quote[2])
+ pattern = /[\n#{Regexp.escape(matching)}'"]/
+
+ if parts.any? { |part|
+ part.is_a?(TStringContent) && part.value.match?(pattern)
+ }
+ [quote, matching]
+ elsif Quotes.locked?(self)
+ ["#{":" unless hash_key}'", "'"]
+ else
+ ["#{":" unless hash_key}#{q.quote}", q.quote]
+ end
+ elsif Quotes.locked?(self)
+ if quote.start_with?(":")
+ [hash_key ? quote[1..-1] : quote, quote[1..-1]]
+ else
+ [hash_key ? quote : ":#{quote}", quote]
+ end
+ else
+ [hash_key ? q.quote : ":#{q.quote}", q.quote]
+ end
+ end
+ end
+
+ # Else represents the end of an +if+, +unless+, or +case+ chain.
+ #
+ # if variable
+ # else
+ # end
+ #
+ class Else < Node
+ # [Statements] the expressions to be executed
+ attr_reader :statements
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(statements:, location:, comments: [])
+ @statements = statements
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [statements]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { statements: statements, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.group do
+ q.text("else")
+
+ unless statements.empty?
+ q.indent do
+ q.breakable(force: true)
+ q.format(statements)
+ end
+ end
+ end
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("else")
+
+ q.breakable
+ q.pp(statements)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :else, stmts: statements, loc: location, cmts: comments }.to_json(
+ *opts
+ )
+ end
+ end
+
+ # Elsif represents another clause in an +if+ or +unless+ chain.
+ #
+ # if variable
+ # elsif other_variable
+ # end
+ #
+ class Elsif < Node
+ # [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
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(
+ predicate:,
+ statements:,
+ consequent:,
+ location:,
+ comments: []
+ )
+ @predicate = predicate
+ @statements = statements
+ @consequent = consequent
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [predicate, statements, consequent]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ predicate: predicate,
+ statements: statements,
+ consequent: consequent,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ q.group do
+ q.group do
+ q.text("elsif ")
+ q.nest("elsif".length - 1) { q.format(predicate) }
+ end
+
+ unless statements.empty?
+ q.indent do
+ q.breakable(force: true)
+ q.format(statements)
+ end
+ end
+
+ if consequent
+ q.group do
+ q.breakable(force: true)
+ q.format(consequent)
+ end
+ end
+ end
+ 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
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :elsif,
+ pred: predicate,
+ stmts: statements,
+ cons: consequent,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # EmbDoc represents a multi-line comment.
+ #
+ # =begin
+ # first line
+ # second line
+ # =end
+ #
+ class EmbDoc < Node
+ # [String] the contents of the comment
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ def initialize(value:, location:)
+ @value = value
+ @location = location
+ end
+
+ def inline?
+ false
+ end
+
+ def ignore?
+ false
+ end
+
+ def comments
+ []
+ end
+
+ def child_nodes
+ []
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { value: value, location: location }
+ end
+
+ def format(q)
+ q.trim
+ q.text(value)
+ 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
+
+ # EmbExprBeg represents the beginning token for using interpolation inside of
+ # a parent node that accepts string content (like a string or regular
+ # expression).
+ #
+ # "Hello, #{person}!"
+ #
+ class EmbExprBeg < Node
+ # [String] the #{ used in the string
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ def initialize(value:, location:)
+ @value = value
+ @location = location
+ end
+ end
+
+ # EmbExprEnd represents the ending token for using interpolation inside of a
+ # parent node that accepts string content (like a string or regular
+ # expression).
+ #
+ # "Hello, #{person}!"
+ #
+ class EmbExprEnd < Node
+ # [String] the } used in the string
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ def initialize(value:, location:)
+ @value = value
+ @location = location
+ end
+ end
+
+ # EmbVar represents the use of shorthand interpolation for an instance, class,
+ # or global variable into a parent node that accepts string content (like a
+ # string or regular expression).
+ #
+ # "#@variable"
+ #
+ # In the example above, an EmbVar node represents the # because it forces
+ # @variable to be interpolated.
+ class EmbVar < Node
+ # [String] the # used in the string
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ def initialize(value:, location:)
+ @value = value
+ @location = location
+ end
+ end
+
+ # Ensure represents the use of the +ensure+ keyword and its subsequent
+ # statements.
+ #
+ # begin
+ # ensure
+ # end
+ #
+ class Ensure < Node
+ # [Kw] the ensure keyword that began this node
+ attr_reader :keyword
+
+ # [Statements] the expressions to be executed
+ attr_reader :statements
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(keyword:, statements:, location:, comments: [])
+ @keyword = keyword
+ @statements = statements
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [keyword, statements]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ keyword: keyword,
+ statements: statements,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ q.format(keyword)
+
+ unless statements.empty?
+ q.indent do
+ q.breakable(force: true)
+ q.format(statements)
+ end
+ end
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("ensure")
+
+ q.breakable
+ q.pp(statements)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :ensure,
+ keyword: keyword,
+ stmts: statements,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # ExcessedComma represents a trailing comma in a list of block parameters. It
+ # changes the block parameters such that they will destructure.
+ #
+ # [[1, 2, 3], [2, 3, 4]].each do |first, second,|
+ # end
+ #
+ # In the above example, an ExcessedComma node would appear in the third
+ # position of the Params node that is used to declare that block. The third
+ # position typically represents a rest-type parameter, but in this case is
+ # used to indicate that a trailing comma was used.
+ class ExcessedComma < Node
+ # [String] the comma
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(value:, location:, comments: [])
+ @value = value
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ []
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { value: value, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.text(value)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("excessed_comma")
+
+ q.breakable
+ q.pp(value)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :excessed_comma,
+ value: value,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ 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 < Node
+ # [Const | Ident] the name of the method
+ attr_reader :value
+
+ # [nil | ArgParen | Args] the arguments to the method call
+ attr_reader :arguments
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(value:, arguments:, location:, comments: [])
+ @value = value
+ @arguments = arguments
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [value, arguments]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ value: value,
+ arguments: arguments,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ q.format(value)
+ q.format(arguments)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("fcall")
+
+ q.breakable
+ q.pp(value)
+
+ if arguments
+ q.breakable
+ q.pp(arguments)
+ end
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :fcall,
+ value: value,
+ args: arguments,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # Field is always the child of an assignment. It represents assigning to a
+ # “field” on an object.
+ #
+ # object.variable = value
+ #
+ class Field < Node
+ # [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
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(parent:, operator:, name:, location:, comments: [])
+ @parent = parent
+ @operator = operator
+ @name = name
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [parent, (operator if operator != :"::"), name]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ parent: parent,
+ operator: operator,
+ name: name,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ q.group do
+ q.format(parent)
+ q.format(CallOperatorFormatter.new(operator), stackable: false)
+ q.format(name)
+ end
+ end
+
+ def 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)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :field,
+ parent: parent,
+ op: operator,
+ name: name,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # FloatLiteral represents a floating point number literal.
+ #
+ # 1.0
+ #
+ class FloatLiteral < Node
+ # [String] the value of the floating point number literal
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(value:, location:, comments: [])
+ @value = value
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ []
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { value: value, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.text(value)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("float")
+
+ q.breakable
+ q.pp(value)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :float, value: value, loc: location, cmts: comments }.to_json(
+ *opts
+ )
+ end
+ end
+
+ # FndPtn represents matching against a pattern where you find a pattern in an
+ # array using the Ruby 3.0+ pattern matching syntax.
+ #
+ # case value
+ # in [*, 7, *]
+ # end
+ #
+ class FndPtn < Node
+ # [nil | 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
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(constant:, left:, values:, right:, location:, comments: [])
+ @constant = constant
+ @left = left
+ @values = values
+ @right = right
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [constant, left, *values, right]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ constant: constant,
+ left: left,
+ values: values,
+ right: right,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ q.format(constant) if constant
+ q.group(0, "[", "]") do
+ q.text("*")
+ q.format(left)
+ q.comma_breakable
+
+ q.seplist(values) { |value| q.format(value) }
+ q.comma_breakable
+
+ q.text("*")
+ q.format(right)
+ end
+ 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)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :fndptn,
+ constant: constant,
+ left: left,
+ values: values,
+ right: right,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # For represents using a +for+ loop.
+ #
+ # for value in list do
+ # end
+ #
+ class For < Node
+ # [MLHS | VarField] the variable declaration being used to
+ # pull values out of the object being enumerated
+ attr_reader :index
+
+ # [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
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(index:, collection:, statements:, location:, comments: [])
+ @index = index
+ @collection = collection
+ @statements = statements
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [index, collection, statements]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ index: index,
+ collection: collection,
+ statements: statements,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ q.group do
+ q.text("for ")
+ q.group { q.format(index) }
+ q.text(" in ")
+ q.format(collection)
+
+ unless statements.empty?
+ q.indent do
+ q.breakable(force: true)
+ q.format(statements)
+ end
+ end
+
+ q.breakable(force: true)
+ q.text("end")
+ end
+ 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)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :for,
+ index: index,
+ collection: collection,
+ stmts: statements,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # GVar represents a global variable literal.
+ #
+ # $variable
+ #
+ class GVar < Node
+ # [String] the name of the global variable
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(value:, location:, comments: [])
+ @value = value
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ []
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { value: value, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.text(value)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("gvar")
+
+ q.breakable
+ q.pp(value)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :gvar, value: value, loc: location, cmts: comments }.to_json(
+ *opts
+ )
+ end
+ end
+
+ # HashLiteral represents a hash literal.
+ #
+ # { key => value }
+ #
+ class HashLiteral < Node
+ # [LBrace] the left brace that opens this hash
+ attr_reader :lbrace
+
+ # [Array[ AssocNew | AssocSplat ]] the optional contents of the hash
+ attr_reader :assocs
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(lbrace:, assocs:, location:, comments: [])
+ @lbrace = lbrace
+ @assocs = assocs
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [lbrace] + assocs
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { lbrace: lbrace, assocs: assocs, location: location, comments: comments }
+ end
+
+ def format(q)
+ if q.parent.is_a?(Assoc)
+ format_contents(q)
+ else
+ q.group { format_contents(q) }
+ end
+ end
+
+ def format_key(q, key)
+ (@key_formatter ||= HashKeyFormatter.for(self)).format_key(q, key)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("hash")
+
+ if assocs.any?
+ q.breakable
+ q.group(2, "(", ")") { q.seplist(assocs) { |assoc| q.pp(assoc) } }
+ end
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :hash, assocs: assocs, loc: location, cmts: comments }.to_json(
+ *opts
+ )
+ end
+
+ private
+
+ def format_contents(q)
+ q.format(lbrace)
+
+ if assocs.empty?
+ q.breakable("")
+ else
+ q.indent do
+ q.breakable
+ q.seplist(assocs) { |assoc| q.format(assoc) }
+ end
+ q.breakable
+ end
+
+ q.text("}")
+ end
+ end
+
+ # Heredoc represents a heredoc string literal.
+ #
+ # <<~DOC
+ # contents
+ # DOC
+ #
+ class Heredoc < Node
+ # [HeredocBeg] the opening of the heredoc
+ attr_reader :beginning
+
+ # [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
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(beginning:, ending: nil, parts: [], location:, comments: [])
+ @beginning = beginning
+ @ending = ending
+ @parts = parts
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [beginning, *parts]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ beginning: beginning,
+ location: location,
+ ending: ending,
+ parts: parts,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ # This is a very specific behavior that should probably be included in the
+ # prettyprint module. It's when you want to force a newline, but don't
+ # want to force the break parent.
+ breakable = -> do
+ q.target <<
+ PrettyPrint::Breakable.new(" ", 1, indent: false, force: true)
+ end
+
+ q.group do
+ q.format(beginning)
+
+ q.line_suffix(priority: Formatter::HEREDOC_PRIORITY) do
+ q.group do
+ breakable.call
+
+ parts.each do |part|
+ if part.is_a?(TStringContent)
+ texts = part.value.split(/\r?\n/, -1)
+ q.seplist(texts, breakable) { |text| q.text(text) }
+ else
+ q.format(part)
+ end
+ end
+
+ q.text(ending)
+ end
+ end
+ end
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("heredoc")
+
+ q.breakable
+ q.group(2, "(", ")") { q.seplist(parts) { |part| q.pp(part) } }
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :heredoc,
+ beging: beginning,
+ ending: ending,
+ parts: parts,
+ loc: location,
+ cmts: comments
+ }.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 < Node
+ # [String] the opening declaration of the heredoc
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(value:, location:, comments: [])
+ @value = value
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ []
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { value: value, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.text(value)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("heredoc_beg")
+
+ q.breakable
+ q.pp(value)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :heredoc_beg,
+ value: value,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # HshPtn represents matching against a hash pattern using the Ruby 2.7+
+ # pattern matching syntax.
+ #
+ # case value
+ # in { key: }
+ # end
+ #
+ class HshPtn < Node
+ class KeywordFormatter
+ # [Label] the keyword being used
+ attr_reader :key
+
+ # [untyped] the optional value for the keyword
+ attr_reader :value
+
+ def initialize(key, value)
+ @key = key
+ @value = value
+ end
+
+ def comments
+ []
+ end
+
+ def format(q)
+ q.format(key)
+
+ if value
+ q.text(" ")
+ q.format(value)
+ end
+ end
+ end
+
+ class KeywordRestFormatter
+ # [VarField] the parameter that matches the remaining keywords
+ attr_reader :keyword_rest
+
+ def initialize(keyword_rest)
+ @keyword_rest = keyword_rest
+ end
+
+ def comments
+ []
+ end
+
+ def format(q)
+ q.text("**")
+ q.format(keyword_rest)
+ end
+ end
+
+ # [nil | 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
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(constant:, keywords:, keyword_rest:, location:, comments: [])
+ @constant = constant
+ @keywords = keywords
+ @keyword_rest = keyword_rest
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [constant, *keywords.flatten(1), keyword_rest]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ constant: constant,
+ keywords: keywords,
+ keyword_rest: keyword_rest,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ parts = keywords.map { |(key, value)| KeywordFormatter.new(key, value) }
+ parts << KeywordRestFormatter.new(keyword_rest) if keyword_rest
+ contents = -> do
+ q.seplist(parts) { |part| q.format(part, stackable: false) }
+ end
+
+ if constant
+ q.format(constant)
+ q.group(0, "[", "]", &contents)
+ return
+ end
+
+ parent = q.parent
+ if PATTERNS.include?(parent.class)
+ q.text("{ ")
+ contents.call
+ q.text(" }")
+ else
+ contents.call
+ end
+ 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) do |(key, value)|
+ q.group(2, "(", ")") do
+ q.pp(key)
+
+ if value
+ q.breakable
+ q.pp(value)
+ end
+ end
+ end
+ end
+ end
+
+ if keyword_rest
+ q.breakable
+ q.pp(keyword_rest)
+ end
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :hshptn,
+ constant: constant,
+ keywords: keywords,
+ kwrest: keyword_rest,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # The list of nodes that represent patterns inside of pattern matching so that
+ # when a pattern is being printed it knows if it's nested.
+ PATTERNS = [AryPtn, Binary, FndPtn, HshPtn, RAssign]
+
+ # Ident represents an identifier anywhere in code. It can represent a very
+ # large number of things, depending on where it is in the syntax tree.
+ #
+ # value
+ #
+ class Ident < Node
+ # [String] the value of the identifier
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(value:, location:, comments: [])
+ @value = value
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ []
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { value: value, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.text(value)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("ident")
+
+ q.breakable
+ q.pp(value)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :ident,
+ value: value.force_encoding("UTF-8"),
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # If the predicate of a conditional or loop contains an assignment (in which
+ # case we can't know for certain that that assignment doesn't impact the
+ # statements inside the conditional) then we can't use the modifier form
+ # and we must use the block form.
+ module ContainsAssignment
+ def self.call(parent)
+ queue = [parent]
+
+ while node = queue.shift
+ return true if [Assign, MAssign, OpAssign].include?(node.class)
+ queue += node.child_nodes
+ end
+
+ false
+ end
+ end
+
+ # Formats an If or Unless node.
+ class ConditionalFormatter
+ # [String] the keyword associated with this conditional
+ attr_reader :keyword
+
+ # [If | Unless] the node that is being formatted
+ attr_reader :node
+
+ def initialize(keyword, node)
+ @keyword = keyword
+ @node = node
+ end
+
+ def format(q)
+ # If the predicate of the conditional contains an assignment (in which
+ # case we can't know for certain that that assignment doesn't impact the
+ # statements inside the conditional) then we can't use the modifier form
+ # and we must use the block form.
+ if ContainsAssignment.call(node.predicate)
+ format_break(q, force: true)
+ return
+ end
+
+ if node.consequent || node.statements.empty?
+ q.group { format_break(q, force: true) }
+ else
+ q.group do
+ q.if_break { format_break(q, force: false) }.if_flat do
+ Parentheses.flat(q) do
+ q.format(node.statements)
+ q.text(" #{keyword} ")
+ q.format(node.predicate)
+ end
+ end
+ end
+ end
+ end
+
+ private
+
+ def format_break(q, force:)
+ q.text("#{keyword} ")
+ q.nest(keyword.length + 1) { q.format(node.predicate) }
+
+ unless node.statements.empty?
+ q.indent do
+ q.breakable(force: force)
+ q.format(node.statements)
+ end
+ end
+
+ if node.consequent
+ q.breakable(force: force)
+ q.format(node.consequent)
+ end
+
+ q.breakable(force: force)
+ q.text("end")
+ end
+ end
+
+ # If represents the first clause in an +if+ chain.
+ #
+ # if predicate
+ # end
+ #
+ class If < Node
+ # [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
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(
+ predicate:,
+ statements:,
+ consequent:,
+ location:,
+ comments: []
+ )
+ @predicate = predicate
+ @statements = statements
+ @consequent = consequent
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [predicate, statements, consequent]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ predicate: predicate,
+ statements: statements,
+ consequent: consequent,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ ConditionalFormatter.new("if", self).format(q)
+ end
+
+ def 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
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :if,
+ pred: predicate,
+ stmts: statements,
+ cons: consequent,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # IfOp represents a ternary clause.
+ #
+ # predicate ? truthy : falsy
+ #
+ class IfOp < Node
+ # [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
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(predicate:, truthy:, falsy:, location:, comments: [])
+ @predicate = predicate
+ @truthy = truthy
+ @falsy = falsy
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [predicate, truthy, falsy]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ predicate: predicate,
+ truthy: truthy,
+ falsy: falsy,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ force_flat = [
+ Alias, Assign, Break, Command, CommandCall, Heredoc, If, IfMod, IfOp,
+ Lambda, MAssign, Next, OpAssign, RescueMod, Return, Return0, Super,
+ Undef, Unless, UnlessMod, UntilMod, VarAlias, VoidStmt, WhileMod, Yield,
+ Yield0, ZSuper
+ ]
+
+ if force_flat.include?(truthy.class) || force_flat.include?(falsy.class)
+ q.group { format_flat(q) }
+ return
+ end
+
+ q.group { q.if_break { format_break(q) }.if_flat { format_flat(q) } }
+ end
+
+ def 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)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :ifop,
+ pred: predicate,
+ tthy: truthy,
+ flsy: falsy,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+
+ private
+
+ def format_break(q)
+ Parentheses.break(q) do
+ q.text("if ")
+ q.nest("if ".length) { q.format(predicate) }
+
+ q.indent do
+ q.breakable
+ q.format(truthy)
+ end
+
+ q.breakable
+ q.text("else")
+
+ q.indent do
+ q.breakable
+ q.format(falsy)
+ end
+
+ q.breakable
+ q.text("end")
+ end
+ end
+
+ def format_flat(q)
+ q.format(predicate)
+ q.text(" ?")
+
+ q.breakable
+ q.format(truthy)
+ q.text(" :")
+
+ q.breakable
+ q.format(falsy)
+ end
+ end
+
+ # Formats an IfMod or UnlessMod node.
+ class ConditionalModFormatter
+ # [String] the keyword associated with this conditional
+ attr_reader :keyword
+
+ # [IfMod | UnlessMod] the node that is being formatted
+ attr_reader :node
+
+ def initialize(keyword, node)
+ @keyword = keyword
+ @node = node
+ end
+
+ def format(q)
+ if ContainsAssignment.call(node.statement) || q.parent.is_a?(In)
+ q.group { format_flat(q) }
+ else
+ q.group { q.if_break { format_break(q) }.if_flat { format_flat(q) } }
+ end
+ end
+
+ private
+
+ def format_break(q)
+ q.text("#{keyword} ")
+ q.nest(keyword.length + 1) { q.format(node.predicate) }
+ q.indent do
+ q.breakable
+ q.format(node.statement)
+ end
+ q.breakable
+ q.text("end")
+ end
+
+ def format_flat(q)
+ Parentheses.flat(q) do
+ q.format(node.statement)
+ q.text(" #{keyword} ")
+ q.format(node.predicate)
+ end
+ end
+ end
+
+ # IfMod represents the modifier form of an +if+ statement.
+ #
+ # expression if predicate
+ #
+ class IfMod < Node
+ # [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
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(statement:, predicate:, location:, comments: [])
+ @statement = statement
+ @predicate = predicate
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [statement, predicate]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ statement: statement,
+ predicate: predicate,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ ConditionalModFormatter.new("if", self).format(q)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("if_mod")
+
+ q.breakable
+ q.pp(statement)
+
+ q.breakable
+ q.pp(predicate)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :if_mod,
+ stmt: statement,
+ pred: predicate,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # Imaginary represents an imaginary number literal.
+ #
+ # 1i
+ #
+ class Imaginary < Node
+ # [String] the value of the imaginary number literal
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(value:, location:, comments: [])
+ @value = value
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ []
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { value: value, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.text(value)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("imaginary")
+
+ q.breakable
+ q.pp(value)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :imaginary, value: value, loc: location, cmts: comments }.to_json(
+ *opts
+ )
+ end
+ end
+
+ # In represents using the +in+ keyword within the Ruby 2.7+ pattern matching
+ # syntax.
+ #
+ # case value
+ # in pattern
+ # end
+ #
+ class In < Node
+ # [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
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(pattern:, statements:, consequent:, location:, comments: [])
+ @pattern = pattern
+ @statements = statements
+ @consequent = consequent
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [pattern, statements, consequent]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ pattern: pattern,
+ statements: statements,
+ consequent: consequent,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ keyword = "in "
+
+ q.group do
+ q.text(keyword)
+ q.nest(keyword.length) { q.format(pattern) }
+
+ unless statements.empty?
+ q.indent do
+ q.breakable(force: true)
+ q.format(statements)
+ end
+ end
+
+ if consequent
+ q.breakable(force: true)
+ q.format(consequent)
+ end
+ end
+ 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
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :in,
+ pattern: pattern,
+ stmts: statements,
+ cons: consequent,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # Int represents an integer number literal.
+ #
+ # 1
+ #
+ class Int < Node
+ # [String] the value of the integer
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(value:, location:, comments: [])
+ @value = value
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ []
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { value: value, location: location, comments: comments }
+ end
+
+ def format(q)
+ if !value.start_with?(/\+?0/) && value.length >= 5 && !value.include?("_")
+ # If it's a plain integer and it doesn't have any underscores separating
+ # the values, then we're going to insert them every 3 characters
+ # starting from the right.
+ index = (value.length + 2) % 3
+ q.text(" #{value}"[index..-1].scan(/.../).join("_").strip)
+ else
+ q.text(value)
+ end
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("int")
+
+ q.breakable
+ q.pp(value)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :int, value: value, loc: location, cmts: comments }.to_json(*opts)
+ end
+ end
+
+ # IVar represents an instance variable literal.
+ #
+ # @variable
+ #
+ class IVar < Node
+ # [String] the name of the instance variable
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(value:, location:, comments: [])
+ @value = value
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ []
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { value: value, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.text(value)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("ivar")
+
+ q.breakable
+ q.pp(value)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :ivar, value: value, loc: location, cmts: comments }.to_json(
+ *opts
+ )
+ end
+ end
+
+ # Kw represents the use of a keyword. It can be almost anywhere in the syntax
+ # tree, so you end up seeing it quite a lot.
+ #
+ # if value
+ # end
+ #
+ # In the above example, there would be two Kw nodes: one for the if and one
+ # for the end. Note that anything that matches the list of keywords in Ruby
+ # will use a Kw, so if you use a keyword in a symbol literal for instance:
+ #
+ # :if
+ #
+ # then the contents of the symbol node will contain a Kw node.
+ class Kw < Node
+ # [String] the value of the keyword
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(value:, location:, comments: [])
+ @value = value
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ []
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { value: value, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.text(value)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("kw")
+
+ q.breakable
+ q.pp(value)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :kw, value: value, loc: location, cmts: comments }.to_json(*opts)
+ end
+ end
+
+ # KwRestParam represents defining a parameter in a method definition that
+ # accepts all remaining keyword parameters.
+ #
+ # def method(**kwargs) end
+ #
+ class KwRestParam < Node
+ # [nil | Ident] the name of the parameter
+ attr_reader :name
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(name:, location:, comments: [])
+ @name = name
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [name]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { name: name, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.text("**")
+ q.format(name) if name
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("kwrest_param")
+
+ q.breakable
+ q.pp(name)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :kwrest_param,
+ name: name,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # Label represents the use of an identifier to associate with an object. You
+ # can find it in a hash key, as in:
+ #
+ # { key: value }
+ #
+ # In this case "key:" would be the body of the label. You can also find it in
+ # pattern matching, as in:
+ #
+ # case value
+ # in key:
+ # end
+ #
+ # In this case "key:" would be the body of the label.
+ class Label < Node
+ # [String] the value of the label
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(value:, location:, comments: [])
+ @value = value
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ []
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { value: value, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.text(value)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("label")
+
+ q.breakable
+ q.text(":")
+ q.text(value[0...-1])
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :label, value: value, loc: location, cmts: comments }.to_json(
+ *opts
+ )
+ end
+ end
+
+ # LabelEnd represents the end of a dynamic symbol.
+ #
+ # { "key": value }
+ #
+ # In the example above, LabelEnd represents the "\":" token at the end of the
+ # hash key. This node is important for determining the type of quote being
+ # used by the label.
+ class LabelEnd < Node
+ # [String] the end of the label
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ def initialize(value:, location:)
+ @value = value
+ @location = location
+ end
+ end
+
+ # Lambda represents using a lambda literal (not the lambda method call).
+ #
+ # ->(value) { value * 2 }
+ #
+ class Lambda < Node
+ # [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
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(params:, statements:, location:, comments: [])
+ @params = params
+ @statements = statements
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [params, statements]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ params: params,
+ statements: statements,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ q.group(0, "->") do
+ if params.is_a?(Paren)
+ q.format(params) unless params.contents.empty?
+ elsif !params.empty?
+ q.group do
+ q.text("(")
+ q.format(params)
+ q.text(")")
+ end
+ end
+
+ q.text(" ")
+ q.if_break do
+ force_parens =
+ q.parents.any? do |node|
+ node.is_a?(Command) || node.is_a?(CommandCall)
+ end
+
+ q.text(force_parens ? "{" : "do")
+ q.indent do
+ q.breakable
+ q.format(statements)
+ end
+
+ q.breakable
+ q.text(force_parens ? "}" : "end")
+ end.if_flat do
+ q.text("{ ")
+ q.format(statements)
+ q.text(" }")
+ end
+ end
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("lambda")
+
+ q.breakable
+ q.pp(params)
+
+ q.breakable
+ q.pp(statements)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :lambda,
+ params: params,
+ stmts: statements,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # LBrace represents the use of a left brace, i.e., {.
+ class LBrace < Node
+ # [String] the left brace
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(value:, location:, comments: [])
+ @value = value
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ []
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { value: value, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.text(value)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("lbrace")
+
+ q.breakable
+ q.pp(value)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :lbrace, value: value, loc: location, cmts: comments }.to_json(
+ *opts
+ )
+ end
+ end
+
+ # LBracket represents the use of a left bracket, i.e., [.
+ class LBracket < Node
+ # [String] the left bracket
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(value:, location:, comments: [])
+ @value = value
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ []
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { value: value, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.text(value)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("lbracket")
+
+ q.breakable
+ q.pp(value)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :lbracket, value: value, loc: location, cmts: comments }.to_json(
+ *opts
+ )
+ end
+ end
+
+ # LParen represents the use of a left parenthesis, i.e., (.
+ class LParen < Node
+ # [String] the left parenthesis
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(value:, location:, comments: [])
+ @value = value
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ []
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { value: value, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.text(value)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("lparen")
+
+ q.breakable
+ q.pp(value)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :lparen, value: value, loc: location, cmts: comments }.to_json(
+ *opts
+ )
+ end
+ end
+
+ # MAssign is a parent node of any kind of multiple assignment. This includes
+ # splitting out variables on the left like:
+ #
+ # first, second, third = value
+ #
+ # as well as splitting out variables on the right, as in:
+ #
+ # value = first, second, third
+ #
+ # Both sides support splats, as well as variables following them. There's also
+ # destructuring behavior that you can achieve with the following:
+ #
+ # first, = value
+ #
+ class MAssign < Node
+ # [MLHS | MLHSParen] the target of the multiple assignment
+ attr_reader :target
+
+ # [untyped] the value being assigned
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(target:, value:, location:, comments: [])
+ @target = target
+ @value = value
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [target, value]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { target: target, value: value, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.group do
+ q.group { q.format(target) }
+ q.text(" =")
+ q.indent do
+ q.breakable
+ q.format(value)
+ end
+ end
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("massign")
+
+ q.breakable
+ q.pp(target)
+
+ q.breakable
+ q.pp(value)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :massign,
+ target: target,
+ value: value,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # MethodAddBlock represents a method call with a block argument.
+ #
+ # method {}
+ #
+ class MethodAddBlock < Node
+ # [Call | Command | CommandCall | FCall] 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
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(call:, block:, location:, comments: [])
+ @call = call
+ @block = block
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [call, block]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { call: call, block: block, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.format(call)
+ q.format(block)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("method_add_block")
+
+ q.breakable
+ q.pp(call)
+
+ q.breakable
+ q.pp(block)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :method_add_block,
+ call: call,
+ block: block,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # MLHS represents a list of values being destructured on the left-hand side
+ # of a multiple assignment.
+ #
+ # first, second, third = value
+ #
+ class MLHS < Node
+ # Array[ARefField | ArgStar | 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
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(parts:, comma: false, location:, comments: [])
+ @parts = parts
+ @comma = comma
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ parts
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { parts: parts, location: location, comma: comma, comments: comments }
+ end
+
+ def format(q)
+ q.seplist(parts) { |part| q.format(part) }
+ q.text(",") if comma
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("mlhs")
+
+ q.breakable
+ q.group(2, "(", ")") { q.seplist(parts) { |part| q.pp(part) } }
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :mlhs,
+ parts: parts,
+ comma: comma,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # MLHSParen represents parentheses being used to destruct values in a multiple
+ # assignment on the left hand side.
+ #
+ # (left, right) = value
+ #
+ class MLHSParen < Node
+ # [MLHS | MLHSParen] the contents inside of the parentheses
+ attr_reader :contents
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(contents:, location:, comments: [])
+ @contents = contents
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [contents]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { contents: contents, location: location, comments: comments }
+ end
+
+ def format(q)
+ parent = q.parent
+
+ if parent.is_a?(MAssign) || parent.is_a?(MLHSParen)
+ q.format(contents)
+ else
+ q.group(0, "(", ")") do
+ q.indent do
+ q.breakable("")
+ q.format(contents)
+ end
+
+ q.breakable("")
+ end
+ end
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("mlhs_paren")
+
+ q.breakable
+ q.pp(contents)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :mlhs_paren,
+ cnts: contents,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # ModuleDeclaration represents defining a module using the +module+ keyword.
+ #
+ # module Namespace
+ # end
+ #
+ class ModuleDeclaration < Node
+ # [ConstPathRef | ConstRef | TopConstRef] the name of the module
+ attr_reader :constant
+
+ # [BodyStmt] the expressions to be executed in the context of the module
+ attr_reader :bodystmt
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(constant:, bodystmt:, location:, comments: [])
+ @constant = constant
+ @bodystmt = bodystmt
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [constant, bodystmt]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ constant: constant,
+ bodystmt: bodystmt,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ declaration = -> do
+ q.group do
+ q.text("module ")
+ q.format(constant)
+ end
+ end
+
+ if bodystmt.empty?
+ q.group do
+ declaration.call
+ q.breakable(force: true)
+ q.text("end")
+ end
+ else
+ q.group do
+ declaration.call
+
+ q.indent do
+ q.breakable(force: true)
+ q.format(bodystmt)
+ end
+
+ q.breakable(force: true)
+ q.text("end")
+ end
+ end
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("module")
+
+ q.breakable
+ q.pp(constant)
+
+ q.breakable
+ q.pp(bodystmt)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :module,
+ constant: constant,
+ bodystmt: bodystmt,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # MRHS represents the values that are being assigned on the right-hand side of
+ # a multiple assignment.
+ #
+ # values = first, second, third
+ #
+ class MRHS < Node
+ # Array[untyped] the parts that are being assigned
+ attr_reader :parts
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(parts:, location:, comments: [])
+ @parts = parts
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ parts
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { parts: parts, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.seplist(parts) { |part| q.format(part) }
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("mrhs")
+
+ q.breakable
+ q.group(2, "(", ")") { q.seplist(parts) { |part| q.pp(part) } }
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :mrhs, parts: parts, loc: location, cmts: comments }.to_json(
+ *opts
+ )
+ end
+ end
+
+ # Next represents using the +next+ keyword.
+ #
+ # next
+ #
+ # The +next+ keyword can also optionally be called with an argument:
+ #
+ # next value
+ #
+ # +next+ can even be called with multiple arguments, but only if parentheses
+ # are omitted, as in:
+ #
+ # next first, second, third
+ #
+ # If a single value is being given, parentheses can be used, as in:
+ #
+ # next(value)
+ #
+ class Next < Node
+ # [Args] the arguments passed to the next keyword
+ attr_reader :arguments
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(arguments:, location:, comments: [])
+ @arguments = arguments
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [arguments]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { arguments: arguments, location: location, comments: comments }
+ end
+
+ def format(q)
+ FlowControlFormatter.new("next", self).format(q)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("next")
+
+ q.breakable
+ q.pp(arguments)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :next, args: arguments, loc: location, cmts: comments }.to_json(
+ *opts
+ )
+ end
+ end
+
+ # Op represents an operator literal in the source.
+ #
+ # 1 + 2
+ #
+ # In the example above, the Op node represents the + operator.
+ class Op < Node
+ # [String] the operator
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(value:, location:, comments: [])
+ @value = value
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ []
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { value: value, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.text(value)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("op")
+
+ q.breakable
+ q.pp(value)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :op, value: value, loc: location, cmts: comments }.to_json(*opts)
+ end
+ end
+
+ # OpAssign represents assigning a value to a variable or constant using an
+ # operator like += or ||=.
+ #
+ # variable += value
+ #
+ class OpAssign < Node
+ # [ARefField | ConstPathField | Field | TopConstField | VarField] the target
+ # to assign the result of the expression to
+ attr_reader :target
+
+ # [Op] the operator being used for the assignment
+ attr_reader :operator
+
+ # [untyped] the expression to be assigned
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(target:, operator:, value:, location:, comments: [])
+ @target = target
+ @operator = operator
+ @value = value
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [target, operator, value]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ target: target,
+ operator: operator,
+ value: value,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ q.group do
+ q.format(target)
+ q.text(" ")
+ q.format(operator)
+ q.indent do
+ q.breakable
+ q.format(value)
+ end
+ end
+ 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)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :opassign,
+ target: target,
+ op: operator,
+ value: value,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # If you have a modifier statement (for instance a modifier if statement or a
+ # modifier while loop) there are times when you need to wrap the entire
+ # statement in parentheses. This occurs when you have something like:
+ #
+ # foo[:foo] =
+ # if bar?
+ # baz
+ # end
+ #
+ # Normally we would shorten this to an inline version, which would result in:
+ #
+ # foo[:foo] = baz if bar?
+ #
+ # but this actually has different semantic meaning. The first example will
+ # result in a nil being inserted into the hash for the :foo key, whereas the
+ # second example will result in an empty hash because the if statement applies
+ # to the entire assignment.
+ #
+ # We can fix this in a couple of ways. We can use the then keyword, as in:
+ #
+ # foo[:foo] = if bar? then baz end
+ #
+ # But this isn't used very often. We can also just leave it as is with the
+ # multi-line version, but for a short predicate and short value it looks
+ # verbose. The last option and the one used here is to add parentheses on
+ # both sides of the expression, as in:
+ #
+ # foo[:foo] = (baz if bar?)
+ #
+ # This approach maintains the nice conciseness of the inline version, while
+ # keeping the correct semantic meaning.
+ module Parentheses
+ NODES = [Args, Assign, Assoc, Binary, Call, Defined, MAssign, OpAssign]
+
+ def self.flat(q)
+ return yield unless NODES.include?(q.parent.class)
+
+ q.text("(")
+ yield
+ q.text(")")
+ end
+
+ def self.break(q)
+ return yield unless NODES.include?(q.parent.class)
+
+ q.text("(")
+ q.indent do
+ q.breakable("")
+ yield
+ end
+ q.breakable("")
+ q.text(")")
+ end
+ end
+
+ # def on_operator_ambiguous(value)
+ # value
+ # end
+
+ # Params represents defining parameters on a method or lambda.
+ #
+ # def method(param) end
+ #
+ class Params < Node
+ class OptionalFormatter
+ # [Ident] the name of the parameter
+ attr_reader :name
+
+ # [untyped] the value of the parameter
+ attr_reader :value
+
+ def initialize(name, value)
+ @name = name
+ @value = value
+ end
+
+ def comments
+ []
+ end
+
+ def format(q)
+ q.format(name)
+ q.text(" = ")
+ q.format(value)
+ end
+ end
+
+ class KeywordFormatter
+ # [Ident] the name of the parameter
+ attr_reader :name
+
+ # [nil | untyped] the value of the parameter
+ attr_reader :value
+
+ def initialize(name, value)
+ @name = name
+ @value = value
+ end
+
+ def comments
+ []
+ end
+
+ def format(q)
+ q.format(name)
+
+ if value
+ q.text(" ")
+ q.format(value)
+ end
+ end
+ end
+
+ class KeywordRestFormatter
+ # [:nil | ArgsForward | KwRestParam] the value of the parameter
+ attr_reader :value
+
+ def initialize(value)
+ @value = value
+ end
+
+ def comments
+ []
+ end
+
+ def format(q)
+ if value == :nil
+ q.text("**nil")
+ else
+ q.format(value)
+ end
+ end
+ end
+
+ # [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
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(
+ requireds: [],
+ optionals: [],
+ rest: nil,
+ posts: [],
+ keywords: [],
+ keyword_rest: nil,
+ block: nil,
+ location:,
+ comments: []
+ )
+ @requireds = requireds
+ @optionals = optionals
+ @rest = rest
+ @posts = posts
+ @keywords = keywords
+ @keyword_rest = keyword_rest
+ @block = block
+ @location = location
+ @comments = comments
+ end
+
+ # Params nodes are the most complicated in the tree. Occasionally you want
+ # to know if they are "empty", which means not having any parameters
+ # declared. This logic accesses every kind of parameter and determines if
+ # it's missing.
+ def empty?
+ requireds.empty? && optionals.empty? && !rest && posts.empty? &&
+ keywords.empty? && !keyword_rest && !block
+ end
+
+ def child_nodes
+ [
+ *requireds,
+ *optionals.flatten(1),
+ rest,
+ *posts,
+ *keywords.flatten(1),
+ (keyword_rest if keyword_rest != :nil),
+ block
+ ]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ location: location,
+ requireds: requireds,
+ optionals: optionals,
+ rest: rest,
+ posts: posts,
+ keywords: keywords,
+ keyword_rest: keyword_rest,
+ block: block,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ parts = [
+ *requireds,
+ *optionals.map { |(name, value)| OptionalFormatter.new(name, value) }
+ ]
+
+ parts << rest if rest && !rest.is_a?(ExcessedComma)
+ parts +=
+ [
+ *posts,
+ *keywords.map { |(name, value)| KeywordFormatter.new(name, value) }
+ ]
+
+ parts << KeywordRestFormatter.new(keyword_rest) if keyword_rest
+ parts << block if block
+
+ contents = -> do
+ q.seplist(parts) { |part| q.format(part) }
+ q.format(rest) if rest && rest.is_a?(ExcessedComma)
+ end
+
+ if [Def, Defs, DefEndless].include?(q.parent.class)
+ q.group(0, "(", ")") do
+ q.indent do
+ q.breakable("")
+ contents.call
+ end
+ q.breakable("")
+ end
+ else
+ q.nest(0, &contents)
+ end
+ 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
+
+ q.pp(Comment::List.new(comments))
+ 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,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # Paren represents using balanced parentheses in a couple places in a Ruby
+ # program. In general parentheses can be used anywhere a Ruby expression can
+ # be used.
+ #
+ # (1 + 2)
+ #
+ class Paren < Node
+ # [LParen] the left parenthesis that opened this statement
+ attr_reader :lparen
+
+ # [nil | untyped] the expression inside the parentheses
+ attr_reader :contents
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(lparen:, contents:, location:, comments: [])
+ @lparen = lparen
+ @contents = contents
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [lparen, contents]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ lparen: lparen,
+ contents: contents,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ q.group do
+ q.format(lparen)
+
+ if contents && (!contents.is_a?(Params) || !contents.empty?)
+ q.indent do
+ q.breakable("")
+ q.format(contents)
+ end
+ end
+
+ q.breakable("")
+ q.text(")")
+ end
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("paren")
+
+ q.breakable
+ q.pp(contents)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :paren,
+ lparen: lparen,
+ cnts: contents,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # Period represents the use of the +.+ operator. It is usually found in method
+ # calls.
+ class Period < Node
+ # [String] the period
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(value:, location:, comments: [])
+ @value = value
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ []
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { value: value, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.text(value)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("period")
+
+ q.breakable
+ q.pp(value)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :period, value: value, loc: location, cmts: comments }.to_json(
+ *opts
+ )
+ end
+ end
+
+ # Program represents the overall syntax tree.
+ class Program < Node
+ # [Statements] the top-level expressions of the program
+ attr_reader :statements
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(statements:, location:, comments: [])
+ @statements = statements
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [statements]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { statements: statements, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.format(statements)
+
+ # We're going to put a newline on the end so that it always has one unless
+ # it ends with the special __END__ syntax. In that case we want to
+ # replicate the text exactly so we will just let it be.
+ q.breakable(force: true) unless statements.body.last.is_a?(EndContent)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("program")
+
+ q.breakable
+ q.pp(statements)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :program,
+ stmts: statements,
+ comments: comments,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # QSymbols represents a symbol literal array without interpolation.
+ #
+ # %i[one two three]
+ #
+ class QSymbols < Node
+ # [QSymbolsBeg] the token that opens this array literal
+ attr_reader :beginning
+
+ # [Array[ TStringContent ]] the elements of the array
+ attr_reader :elements
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(beginning:, elements:, location:, comments: [])
+ @beginning = beginning
+ @elements = elements
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ []
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ beginning: beginning,
+ elements: elements,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ opening, closing = "%i[", "]"
+
+ if elements.any? { |element| element.match?(/[\[\]]/) }
+ opening = beginning.value
+ closing = Quotes.matching(opening[2])
+ end
+
+ q.group(0, opening, closing) do
+ q.indent do
+ q.breakable("")
+ q.seplist(elements, -> { q.breakable }) do |element|
+ q.format(element)
+ end
+ end
+ q.breakable("")
+ end
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("qsymbols")
+
+ q.breakable
+ q.group(2, "(", ")") { q.seplist(elements) { |element| q.pp(element) } }
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :qsymbols,
+ elems: elements,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # QSymbolsBeg represents the beginning of a symbol literal array.
+ #
+ # %i[one two three]
+ #
+ # In the snippet above, QSymbolsBeg represents the "%i[" token. Note that
+ # these kinds of arrays can start with a lot of different delimiter types
+ # (e.g., %i| or %i<).
+ class QSymbolsBeg < Node
+ # [String] the beginning of the array literal
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ def initialize(value:, location:)
+ @value = value
+ @location = location
+ end
+ end
+
+ # QWords represents a string literal array without interpolation.
+ #
+ # %w[one two three]
+ #
+ class QWords < Node
+ # [QWordsBeg] the token that opens this array literal
+ attr_reader :beginning
+
+ # [Array[ TStringContent ]] the elements of the array
+ attr_reader :elements
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(beginning:, elements:, location:, comments: [])
+ @beginning = beginning
+ @elements = elements
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ []
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ beginning: beginning,
+ elements: elements,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ opening, closing = "%w[", "]"
+
+ if elements.any? { |element| element.match?(/[\[\]]/) }
+ opening = beginning.value
+ closing = Quotes.matching(opening[2])
+ end
+
+ q.group(0, opening, closing) do
+ q.indent do
+ q.breakable("")
+ q.seplist(elements, -> { q.breakable }) do |element|
+ q.format(element)
+ end
+ end
+ q.breakable("")
+ end
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("qwords")
+
+ q.breakable
+ q.group(2, "(", ")") { q.seplist(elements) { |element| q.pp(element) } }
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :qwords, elems: elements, loc: location, cmts: comments }.to_json(
+ *opts
+ )
+ end
+ end
+
+ # QWordsBeg represents the beginning of a string literal array.
+ #
+ # %w[one two three]
+ #
+ # In the snippet above, QWordsBeg represents the "%w[" token. Note that these
+ # kinds of arrays can start with a lot of different delimiter types (e.g.,
+ # %w| or %w<).
+ class QWordsBeg < Node
+ # [String] the beginning of the array literal
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ def initialize(value:, location:)
+ @value = value
+ @location = location
+ end
+ end
+
+ # RationalLiteral represents the use of a rational number literal.
+ #
+ # 1r
+ #
+ class RationalLiteral < Node
+ # [String] the rational number literal
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(value:, location:, comments: [])
+ @value = value
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ []
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { value: value, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.text(value)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("rational")
+
+ q.breakable
+ q.pp(value)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :rational, value: value, loc: location, cmts: comments }.to_json(
+ *opts
+ )
+ end
+ end
+
+ # RBrace represents the use of a right brace, i.e., +++.
+ class RBrace < Node
+ # [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
+
+ # RBracket represents the use of a right bracket, i.e., +]+.
+ class RBracket < Node
+ # [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
+
+ # Redo represents the use of the +redo+ keyword.
+ #
+ # redo
+ #
+ class Redo < Node
+ # [String] the value of the keyword
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(value:, location:, comments: [])
+ @value = value
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ []
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { value: value, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.text(value)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("redo")
+
+ q.breakable
+ q.pp(value)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :redo, value: value, loc: location, cmts: comments }.to_json(
+ *opts
+ )
+ end
+ end
+
+ # RegexpContent represents the body of a regular expression.
+ #
+ # /.+ #{pattern} .+/
+ #
+ # In the example above, a RegexpContent node represents everything contained
+ # within the forward slashes.
+ class RegexpContent < Node
+ # [String] the opening of the regular expression
+ attr_reader :beginning
+
+ # [Array[ StringDVar | StringEmbExpr | TStringContent ]] the parts of the
+ # regular expression
+ attr_reader :parts
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ def initialize(beginning:, parts:, location:)
+ @beginning = beginning
+ @parts = parts
+ @location = location
+ end
+ end
+
+ # RegexpBeg represents the start of a regular expression literal.
+ #
+ # /.+/
+ #
+ # In the example above, RegexpBeg represents the first / token. Regular
+ # expression literals can also be declared using the %r syntax, as in:
+ #
+ # %r{.+}
+ #
+ class RegexpBeg < Node
+ # [String] the beginning of the regular expression
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ def initialize(value:, location:)
+ @value = value
+ @location = location
+ end
+ end
+
+ # RegexpEnd represents the end of a regular expression literal.
+ #
+ # /.+/m
+ #
+ # In the example above, the RegexpEnd event represents the /m at the end of
+ # the regular expression literal. You can also declare regular expression
+ # literals using %r, as in:
+ #
+ # %r{.+}m
+ #
+ class RegexpEnd < Node
+ # [String] the end of the regular expression
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ def initialize(value:, location:)
+ @value = value
+ @location = location
+ end
+ end
+
+ # RegexpLiteral represents a regular expression literal.
+ #
+ # /.+/
+ #
+ class RegexpLiteral < Node
+ # [String] the beginning of the regular expression literal
+ attr_reader :beginning
+
+ # [String] the ending of the regular expression literal
+ attr_reader :ending
+
+ # [Array[ StringEmbExpr | StringDVar | TStringContent ]] the parts of the
+ # regular expression literal
+ attr_reader :parts
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(beginning:, ending:, parts:, location:, comments: [])
+ @beginning = beginning
+ @ending = ending
+ @parts = parts
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ parts
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ beginning: beginning,
+ ending: ending,
+ parts: parts,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ braces = ambiguous?(q) || include?(%r{\/})
+
+ if braces && include?(/[{}]/)
+ q.group do
+ q.text(beginning)
+ q.format_each(parts)
+ q.text(ending)
+ end
+ elsif braces
+ q.group do
+ q.text("%r{")
+
+ if beginning == "/"
+ # If we're changing from a forward slash to a %r{, then we can
+ # replace any escaped forward slashes with regular forward slashes.
+ parts.each do |part|
+ if part.is_a?(TStringContent)
+ q.text(part.value.gsub("\\/", "/"))
+ else
+ q.format(part)
+ end
+ end
+ else
+ q.format_each(parts)
+ end
+
+ q.text("}")
+ q.text(ending[1..-1])
+ end
+ else
+ q.group do
+ q.text("/")
+ q.format_each(parts)
+ q.text("/")
+ q.text(ending[1..-1])
+ end
+ end
+ 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) } }
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :regexp_literal,
+ beging: beginning,
+ ending: ending,
+ parts: parts,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+
+ private
+
+ def include?(pattern)
+ parts.any? do |part|
+ part.is_a?(TStringContent) && part.value.match?(pattern)
+ end
+ end
+
+ # If the first part of this regex is plain string content, we have a space
+ # or an =, and we're contained within a command or command_call node, then
+ # we want to use braces because otherwise we could end up with an ambiguous
+ # operator, e.g. foo / bar/ or foo /=bar/
+ def ambiguous?(q)
+ return false if parts.empty?
+ part = parts.first
+
+ part.is_a?(TStringContent) && part.value.start_with?(" ", "=") &&
+ q.parents.any? { |node| node.is_a?(Command) || node.is_a?(CommandCall) }
+ end
+ end
+
+ # RescueEx represents the list of exceptions being rescued in a rescue clause.
+ #
+ # begin
+ # rescue Exception => exception
+ # end
+ #
+ class RescueEx < Node
+ # [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
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(exceptions:, variable:, location:, comments: [])
+ @exceptions = exceptions
+ @variable = variable
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [*exceptions, variable]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ exceptions: exceptions,
+ variable: variable,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ q.group do
+ if exceptions
+ q.text(" ")
+ q.format(exceptions)
+ end
+
+ if variable
+ q.text(" => ")
+ q.format(variable)
+ end
+ end
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("rescue_ex")
+
+ q.breakable
+ q.pp(exceptions)
+
+ q.breakable
+ q.pp(variable)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :rescue_ex,
+ extns: exceptions,
+ var: variable,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # Rescue represents the use of the rescue keyword inside of a BodyStmt node.
+ #
+ # begin
+ # rescue
+ # end
+ #
+ class Rescue < Node
+ # [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
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(
+ exception:,
+ statements:,
+ consequent:,
+ location:,
+ comments: []
+ )
+ @exception = exception
+ @statements = statements
+ @consequent = consequent
+ @location = location
+ @comments = comments
+ 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 child_nodes
+ [exception, statements, consequent]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ exception: exception,
+ statements: statements,
+ consequent: consequent,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ q.group do
+ q.text("rescue")
+
+ if exception
+ q.nest("rescue ".length) { q.format(exception) }
+ else
+ q.text(" StandardError")
+ end
+
+ unless statements.empty?
+ q.indent do
+ q.breakable(force: true)
+ q.format(statements)
+ end
+ end
+
+ if consequent
+ q.breakable(force: true)
+ q.format(consequent)
+ end
+ 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
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :rescue,
+ extn: exception,
+ stmts: statements,
+ cons: consequent,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # RescueMod represents the use of the modifier form of a +rescue+ clause.
+ #
+ # expression rescue value
+ #
+ class RescueMod < Node
+ # [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
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(statement:, value:, location:, comments: [])
+ @statement = statement
+ @value = value
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [statement, value]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ statement: statement,
+ value: value,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ q.group(0, "begin", "end") do
+ q.indent do
+ q.breakable(force: true)
+ q.format(statement)
+ end
+ q.breakable(force: true)
+ q.text("rescue StandardError")
+ q.indent do
+ q.breakable(force: true)
+ q.format(value)
+ end
+ q.breakable(force: true)
+ end
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("rescue_mod")
+
+ q.breakable
+ q.pp(statement)
+
+ q.breakable
+ q.pp(value)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :rescue_mod,
+ stmt: statement,
+ value: value,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # RestParam represents defining a parameter in a method definition that
+ # accepts all remaining positional parameters.
+ #
+ # def method(*rest) end
+ #
+ class RestParam < Node
+ # [nil | Ident] the name of the parameter
+ attr_reader :name
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(name:, location:, comments: [])
+ @name = name
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [name]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { name: name, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.text("*")
+ q.format(name) if name
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("rest_param")
+
+ q.breakable
+ q.pp(name)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :rest_param, name: name, loc: location, cmts: comments }.to_json(
+ *opts
+ )
+ end
+ end
+
+ # Retry represents the use of the +retry+ keyword.
+ #
+ # retry
+ #
+ class Retry < Node
+ # [String] the value of the keyword
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(value:, location:, comments: [])
+ @value = value
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ []
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { value: value, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.text(value)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("retry")
+
+ q.breakable
+ q.pp(value)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :retry, value: value, loc: location, cmts: comments }.to_json(
+ *opts
+ )
+ end
+ end
+
+ # Return represents using the +return+ keyword with arguments.
+ #
+ # return value
+ #
+ class Return < Node
+ # [Args] the arguments being passed to the keyword
+ attr_reader :arguments
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(arguments:, location:, comments: [])
+ @arguments = arguments
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [arguments]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { arguments: arguments, location: location, comments: comments }
+ end
+
+ def format(q)
+ FlowControlFormatter.new("return", self).format(q)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("return")
+
+ q.breakable
+ q.pp(arguments)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :return, args: arguments, loc: location, cmts: comments }.to_json(
+ *opts
+ )
+ end
+ end
+
+ # Return0 represents the bare +return+ keyword with no arguments.
+ #
+ # return
+ #
+ class Return0 < Node
+ # [String] the value of the keyword
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(value:, location:, comments: [])
+ @value = value
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ []
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { value: value, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.text(value)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("return0")
+
+ q.breakable
+ q.pp(value)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :return0, value: value, loc: location, cmts: comments }.to_json(
+ *opts
+ )
+ end
+ end
+
+ # RParen represents the use of a right parenthesis, i.e., +)+.
+ class RParen < Node
+ # [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
+
+ # SClass represents a block of statements that should be evaluated within the
+ # context of the singleton class of an object. It's frequently used to define
+ # singleton methods.
+ #
+ # class << self
+ # end
+ #
+ class SClass < Node
+ # [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
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(target:, bodystmt:, location:, comments: [])
+ @target = target
+ @bodystmt = bodystmt
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [target, bodystmt]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ target: target,
+ bodystmt: bodystmt,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ q.group(0, "class << ", "end") do
+ q.format(target)
+ q.indent do
+ q.breakable(force: true)
+ q.format(bodystmt)
+ end
+ q.breakable(force: true)
+ end
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("sclass")
+
+ q.breakable
+ q.pp(target)
+
+ q.breakable
+ q.pp(bodystmt)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :sclass,
+ target: target,
+ bodystmt: bodystmt,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # Everything that has a block of code inside of it has a list of statements.
+ # Normally we would just track those as a node that has an array body, but we
+ # have some special handling in order to handle empty statement lists. They
+ # need to have the right location information, so all of the parent node of
+ # stmts nodes will report back down the location information. We then
+ # propagate that onto void_stmt nodes inside the stmts in order to make sure
+ # all comments get printed appropriately.
+ class Statements < Node
+ # [SyntaxTree] the parser that is generating 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
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(parser, body:, location:, comments: [])
+ @parser = parser
+ @body = body
+ @location = location
+ @comments = comments
+ 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 empty?
+ body.all? do |statement|
+ statement.is_a?(VoidStmt) && statement.comments.empty?
+ end
+ end
+
+ def child_nodes
+ body
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { parser: parser, body: body, location: location, comments: comments }
+ end
+
+ def format(q)
+ line = nil
+
+ # This handles a special case where you've got a block of statements where
+ # the only value is a comment. In that case a lot of nodes like
+ # brace_block will attempt to format as a single line, but since that
+ # wouldn't work with a comment, we intentionally break the parent group.
+ if body.length == 2
+ void_stmt, comment = body
+
+ if void_stmt.is_a?(VoidStmt) && comment.is_a?(Comment)
+ q.format(comment)
+ q.break_parent
+ return
+ end
+ end
+
+ access_controls =
+ Hash.new do |hash, node|
+ hash[node] = node.is_a?(VCall) &&
+ %w[private protected public].include?(node.value.value)
+ end
+
+ body.each_with_index do |statement, index|
+ next if statement.is_a?(VoidStmt)
+
+ if line.nil?
+ q.format(statement)
+ elsif (statement.location.start_line - line) > 1
+ q.breakable(force: true)
+ q.breakable(force: true)
+ q.format(statement)
+ elsif access_controls[statement] || access_controls[body[index - 1]]
+ q.breakable(force: true)
+ q.breakable(force: true)
+ q.format(statement)
+ elsif statement.location.start_line != line
+ q.breakable(force: true)
+ q.format(statement)
+ elsif !q.parent.is_a?(StringEmbExpr)
+ q.breakable(force: true)
+ q.format(statement)
+ else
+ q.text("; ")
+ q.format(statement)
+ end
+
+ line = statement.location.end_line
+ end
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("statements")
+
+ q.breakable
+ q.seplist(body) { |statement| q.pp(statement) }
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :statements, body: body, loc: location, cmts: comments }.to_json(
+ *opts
+ )
+ end
+
+ private
+
+ # As efficiently as possible, gather up all of the comments that have been
+ # found while this statements list was being parsed and add them into the
+ # body.
+ def attach_comments(start_char, end_char)
+ parser_comments = parser.comments
+
+ comment_index = 0
+ body_index = 0
+
+ while comment_index < parser_comments.size
+ comment = parser_comments[comment_index]
+ location = comment.location
+
+ if !comment.inline? && (start_char <= location.start_char) &&
+ (end_char >= location.end_char) && !comment.ignore?
+ parser_comments.delete_at(comment_index)
+
+ while (node = body[body_index]) &&
+ (
+ node.is_a?(VoidStmt) ||
+ node.location.start_char < location.start_char
+ )
+ body_index += 1
+ end
+
+ body.insert(body_index, comment)
+ else
+ comment_index += 1
+ end
+ end
+ end
+ end
+
+ # StringContent represents the contents of a string-like value.
+ #
+ # "string"
+ #
+ class StringContent < Node
+ # [Array[ StringEmbExpr | StringDVar | TStringContent ]] the parts of the
+ # string
+ attr_reader :parts
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ def initialize(parts:, location:)
+ @parts = parts
+ @location = location
+ end
+ end
+
+ # StringConcat represents concatenating two strings together using a backward
+ # slash.
+ #
+ # "first" \
+ # "second"
+ #
+ class StringConcat < Node
+ # [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
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(left:, right:, location:, comments: [])
+ @left = left
+ @right = right
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [left, right]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { left: left, right: right, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.group do
+ q.format(left)
+ q.text(' \\')
+ q.indent do
+ q.breakable(force: true)
+ q.format(right)
+ end
+ end
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("string_concat")
+
+ q.breakable
+ q.pp(left)
+
+ q.breakable
+ q.pp(right)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :string_concat,
+ left: left,
+ right: right,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # StringDVar represents shorthand interpolation of a variable into a string.
+ # It allows you to take an instance variable, class variable, or global
+ # variable and omit the braces when interpolating.
+ #
+ # "#@variable"
+ #
+ class StringDVar < Node
+ # [Backref | VarRef] the variable being interpolated
+ attr_reader :variable
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(variable:, location:, comments: [])
+ @variable = variable
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [variable]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { variable: variable, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.text('#{')
+ q.format(variable)
+ q.text("}")
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("string_dvar")
+
+ q.breakable
+ q.pp(variable)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :string_dvar,
+ var: variable,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # StringEmbExpr represents interpolated content. It can be contained within a
+ # couple of different parent nodes, including regular expressions, strings,
+ # and dynamic symbols.
+ #
+ # "string #{expression}"
+ #
+ class StringEmbExpr < Node
+ # [Statements] the expressions to be interpolated
+ attr_reader :statements
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(statements:, location:, comments: [])
+ @statements = statements
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [statements]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { statements: statements, location: location, comments: comments }
+ end
+
+ def format(q)
+ if location.start_line == location.end_line
+ # If the contents of this embedded expression were originally on the
+ # same line in the source, then we're going to leave them in place and
+ # assume that's the way the developer wanted this expression
+ # represented.
+ doc = q.group(0, '#{', "}") { q.format(statements) }
+ RemoveBreaks.call(doc)
+ else
+ q.group do
+ q.text('#{')
+ q.indent do
+ q.breakable("")
+ q.format(statements)
+ end
+ q.breakable("")
+ q.text("}")
+ end
+ end
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("string_embexpr")
+
+ q.breakable
+ q.pp(statements)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :string_embexpr,
+ stmts: statements,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # StringLiteral represents a string literal.
+ #
+ # "string"
+ #
+ class StringLiteral < Node
+ # [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
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(parts:, quote:, location:, comments: [])
+ @parts = parts
+ @quote = quote
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ parts
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { parts: parts, quote: quote, location: location, comments: comments }
+ end
+
+ def format(q)
+ if parts.empty?
+ q.text("#{q.quote}#{q.quote}")
+ return
+ end
+
+ opening_quote, closing_quote =
+ if !Quotes.locked?(self)
+ [q.quote, q.quote]
+ elsif quote.start_with?("%")
+ [quote, Quotes.matching(quote[/%[qQ]?(.)/, 1])]
+ else
+ [quote, quote]
+ end
+
+ q.group(0, opening_quote, closing_quote) do
+ parts.each do |part|
+ if part.is_a?(TStringContent)
+ value = Quotes.normalize(part.value, closing_quote)
+ separator = -> { q.breakable(force: true, indent: false) }
+ q.seplist(value.split(/\r?\n/, -1), separator) do |text|
+ q.text(text)
+ end
+ else
+ q.format(part)
+ end
+ end
+ end
+ 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) } }
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :string_literal,
+ parts: parts,
+ quote: quote,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # Super represents using the +super+ keyword with arguments. It can optionally
+ # use parentheses.
+ #
+ # super(value)
+ #
+ class Super < Node
+ # [ArgParen | Args] the arguments to the keyword
+ attr_reader :arguments
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(arguments:, location:, comments: [])
+ @arguments = arguments
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [arguments]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { arguments: arguments, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.group do
+ q.text("super")
+
+ if arguments.is_a?(ArgParen)
+ q.format(arguments)
+ else
+ q.text(" ")
+ q.nest("super ".length) { q.format(arguments) }
+ end
+ end
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("super")
+
+ q.breakable
+ q.pp(arguments)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :super, args: arguments, loc: location, cmts: comments }.to_json(
+ *opts
+ )
+ end
+ end
+
+ # SymBeg represents the beginning of a symbol literal.
+ #
+ # :symbol
+ #
+ # SymBeg is also used for dynamic symbols, as in:
+ #
+ # :"symbol"
+ #
+ # Finally, SymBeg is also used for symbols using the %s syntax, as in:
+ #
+ # %s[symbol]
+ #
+ # The value of this node is a string. In most cases (as in the first example
+ # above) it will contain just ":". In the case of dynamic symbols it will
+ # contain ":'" or ":\"". In the case of %s symbols, it will contain the start
+ # of the symbol including the %s and the delimiter.
+ class SymBeg < Node
+ # [String] the beginning of the symbol
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ def initialize(value:, location:)
+ @value = value
+ @location = location
+ end
+ end
+
+ # SymbolContent represents symbol contents and is always the child of a
+ # SymbolLiteral node.
+ #
+ # :symbol
+ #
+ class SymbolContent < Node
+ # [Backtick | Const | CVar | GVar | Ident | IVar | Kw | Op] the value of the
+ # symbol
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ def initialize(value:, location:)
+ @value = value
+ @location = location
+ end
+ end
+
+ # SymbolLiteral represents a symbol in the system with no interpolation
+ # (as opposed to a DynaSymbol which has interpolation).
+ #
+ # :symbol
+ #
+ class SymbolLiteral < Node
+ # [Backtick | Const | CVar | GVar | Ident | IVar | Kw | Op] the value of the
+ # symbol
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(value:, location:, comments: [])
+ @value = value
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [value]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { value: value, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.text(":")
+ q.format(value)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("symbol_literal")
+
+ q.breakable
+ q.pp(value)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :symbol_literal,
+ value: value,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # Symbols represents a symbol array literal with interpolation.
+ #
+ # %I[one two three]
+ #
+ class Symbols < Node
+ # [SymbolsBeg] the token that opens this array literal
+ attr_reader :beginning
+
+ # [Array[ Word ]] the words in the symbol array literal
+ attr_reader :elements
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(beginning:, elements:, location:, comments: [])
+ @beginning = beginning
+ @elements = elements
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ []
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ beginning: beginning,
+ elements: elements,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ opening, closing = "%I[", "]"
+
+ if elements.any? { |element| element.match?(/[\[\]]/) }
+ opening = beginning.value
+ closing = Quotes.matching(opening[2])
+ end
+
+ q.group(0, opening, closing) do
+ q.indent do
+ q.breakable("")
+ q.seplist(elements, -> { q.breakable }) do |element|
+ q.format(element)
+ end
+ end
+ q.breakable("")
+ end
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("symbols")
+
+ q.breakable
+ q.group(2, "(", ")") { q.seplist(elements) { |element| q.pp(element) } }
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :symbols,
+ elems: elements,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # SymbolsBeg represents the start of a symbol array literal with
+ # interpolation.
+ #
+ # %I[one two three]
+ #
+ # In the snippet above, SymbolsBeg represents the "%I[" token. Note that these
+ # kinds of arrays can start with a lot of different delimiter types
+ # (e.g., %I| or %I<).
+ class SymbolsBeg < Node
+ # [String] the beginning of the symbol literal array
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ def initialize(value:, location:)
+ @value = value
+ @location = location
+ end
+ end
+
+ # TLambda represents the beginning of a lambda literal.
+ #
+ # -> { value }
+ #
+ # In the example above the TLambda represents the +->+ operator.
+ class TLambda < Node
+ # [String] the beginning of the lambda literal
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ def initialize(value:, location:)
+ @value = value
+ @location = location
+ end
+ end
+
+ # TLamBeg represents the beginning of the body of a lambda literal using
+ # braces.
+ #
+ # -> { value }
+ #
+ # In the example above the TLamBeg represents the +{+ operator.
+ class TLamBeg < Node
+ # [String] the beginning of the body of the lambda literal
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ def initialize(value:, location:)
+ @value = value
+ @location = location
+ end
+ end
+
+ # TopConstField is always the child node of some kind of assignment. It
+ # represents when you're assigning to a constant that is being referenced at
+ # the top level.
+ #
+ # ::Constant = value
+ #
+ class TopConstField < Node
+ # [Const] the constant being assigned
+ attr_reader :constant
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(constant:, location:, comments: [])
+ @constant = constant
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [constant]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { constant: constant, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.text("::")
+ q.format(constant)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("top_const_field")
+
+ q.breakable
+ q.pp(constant)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :top_const_field,
+ constant: constant,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # TopConstRef is very similar to TopConstField except that it is not involved
+ # in an assignment.
+ #
+ # ::Constant
+ #
+ class TopConstRef < Node
+ # [Const] the constant being referenced
+ attr_reader :constant
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(constant:, location:, comments: [])
+ @constant = constant
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [constant]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { constant: constant, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.text("::")
+ q.format(constant)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("top_const_ref")
+
+ q.breakable
+ q.pp(constant)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :top_const_ref,
+ constant: constant,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # TStringBeg represents the beginning of a string literal.
+ #
+ # "string"
+ #
+ # In the example above, TStringBeg represents the first set of quotes. Strings
+ # can also use single quotes. They can also be declared using the +%q+ and
+ # +%Q+ syntax, as in:
+ #
+ # %q{string}
+ #
+ class TStringBeg < Node
+ # [String] the beginning of the string
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ def initialize(value:, location:)
+ @value = value
+ @location = location
+ end
+ end
+
+ # TStringContent represents plain characters inside of an entity that accepts
+ # string content like a string, heredoc, command string, or regular
+ # expression.
+ #
+ # "string"
+ #
+ # In the example above, TStringContent represents the +string+ token contained
+ # within the string.
+ class TStringContent < Node
+ # [String] the content of the string
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(value:, location:, comments: [])
+ @value = value
+ @location = location
+ @comments = comments
+ end
+
+ def match?(pattern)
+ value.match?(pattern)
+ end
+
+ def child_nodes
+ []
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { value: value, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.text(value)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("tstring_content")
+
+ q.breakable
+ q.pp(value)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :tstring_content,
+ value: value.force_encoding("UTF-8"),
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # TStringEnd represents the end of a string literal.
+ #
+ # "string"
+ #
+ # In the example above, TStringEnd represents the second set of quotes.
+ # Strings can also use single quotes. They can also be declared using the +%q+
+ # and +%Q+ syntax, as in:
+ #
+ # %q{string}
+ #
+ class TStringEnd < Node
+ # [String] the end of the string
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ def initialize(value:, location:)
+ @value = value
+ @location = location
+ end
+ end
+
+ # Not represents the unary +not+ method being called on an expression.
+ #
+ # not value
+ #
+ class Not < Node
+ # [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
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(statement:, parentheses:, location:, comments: [])
+ @statement = statement
+ @parentheses = parentheses
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [statement]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ statement: statement,
+ parentheses: parentheses,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ q.text(parentheses ? "not(" : "not ")
+ q.format(statement)
+ q.text(")") if parentheses
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("not")
+
+ q.breakable
+ q.pp(statement)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :not,
+ value: statement,
+ paren: parentheses,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # Unary represents a unary method being called on an expression, as in +!+ or
+ # +~+.
+ #
+ # !value
+ #
+ class Unary < Node
+ # [String] the operator being used
+ attr_reader :operator
+
+ # [untyped] the statement on which to operate
+ attr_reader :statement
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(operator:, statement:, location:, comments: [])
+ @operator = operator
+ @statement = statement
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [statement]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ operator: operator,
+ statement: statement,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ q.text(operator)
+ q.format(statement)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("unary")
+
+ q.breakable
+ q.pp(operator)
+
+ q.breakable
+ q.pp(statement)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :unary,
+ op: operator,
+ value: statement,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # Undef represents the use of the +undef+ keyword.
+ #
+ # undef method
+ #
+ class Undef < Node
+ class UndefArgumentFormatter
+ # [DynaSymbol | SymbolLiteral] the symbol to undefine
+ attr_reader :node
+
+ def initialize(node)
+ @node = node
+ end
+
+ def comments
+ if node.is_a?(SymbolLiteral)
+ node.comments + node.value.comments
+ else
+ node.comments
+ end
+ end
+
+ def format(q)
+ node.is_a?(SymbolLiteral) ? q.format(node.value) : q.format(node)
+ end
+ end
+
+ # [Array[ DynaSymbol | SymbolLiteral ]] the symbols to undefine
+ attr_reader :symbols
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(symbols:, location:, comments: [])
+ @symbols = symbols
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ symbols
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { symbols: symbols, location: location, comments: comments }
+ end
+
+ def format(q)
+ keyword = "undef "
+ formatters = symbols.map { |symbol| UndefArgumentFormatter.new(symbol) }
+
+ q.group do
+ q.text(keyword)
+ q.nest(keyword.length) do
+ q.seplist(formatters) { |formatter| q.format(formatter) }
+ end
+ end
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("undef")
+
+ q.breakable
+ q.group(2, "(", ")") { q.seplist(symbols) { |symbol| q.pp(symbol) } }
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :undef, syms: symbols, loc: location, cmts: comments }.to_json(
+ *opts
+ )
+ end
+ end
+
+ # Unless represents the first clause in an +unless+ chain.
+ #
+ # unless predicate
+ # end
+ #
+ class Unless < Node
+ # [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
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(
+ predicate:,
+ statements:,
+ consequent:,
+ location:,
+ comments: []
+ )
+ @predicate = predicate
+ @statements = statements
+ @consequent = consequent
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [predicate, statements, consequent]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ predicate: predicate,
+ statements: statements,
+ consequent: consequent,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ ConditionalFormatter.new("unless", self).format(q)
+ end
+
+ def 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
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :unless,
+ pred: predicate,
+ stmts: statements,
+ cons: consequent,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # UnlessMod represents the modifier form of an +unless+ statement.
+ #
+ # expression unless predicate
+ #
+ class UnlessMod < Node
+ # [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
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(statement:, predicate:, location:, comments: [])
+ @statement = statement
+ @predicate = predicate
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [statement, predicate]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ statement: statement,
+ predicate: predicate,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ ConditionalModFormatter.new("unless", self).format(q)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("unless_mod")
+
+ q.breakable
+ q.pp(statement)
+
+ q.breakable
+ q.pp(predicate)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :unless_mod,
+ stmt: statement,
+ pred: predicate,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # Formats an Until, UntilMod, While, or WhileMod node.
+ class LoopFormatter
+ # [String] the name of the keyword used for this loop
+ attr_reader :keyword
+
+ # [Until | UntilMod | While | WhileMod] the node that is being formatted
+ attr_reader :node
+
+ # [untyped] the statements associated with the node
+ attr_reader :statements
+
+ def initialize(keyword, node, statements)
+ @keyword = keyword
+ @node = node
+ @statements = statements
+ end
+
+ def format(q)
+ if ContainsAssignment.call(node.predicate)
+ format_break(q)
+ q.break_parent
+ return
+ end
+
+ q.group do
+ q.if_break { format_break(q) }.if_flat do
+ Parentheses.flat(q) do
+ q.format(statements)
+ q.text(" #{keyword} ")
+ q.format(node.predicate)
+ end
+ end
+ end
+ end
+
+ private
+
+ def format_break(q)
+ q.text("#{keyword} ")
+ q.nest(keyword.length + 1) { q.format(node.predicate) }
+ q.indent do
+ q.breakable("")
+ q.format(statements)
+ end
+ q.breakable("")
+ q.text("end")
+ end
+ end
+
+ # Until represents an +until+ loop.
+ #
+ # until predicate
+ # end
+ #
+ class Until < Node
+ # [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
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(predicate:, statements:, location:, comments: [])
+ @predicate = predicate
+ @statements = statements
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [predicate, statements]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ predicate: predicate,
+ statements: statements,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ if statements.empty?
+ keyword = "until "
+
+ q.group do
+ q.text(keyword)
+ q.nest(keyword.length) { q.format(predicate) }
+ q.breakable(force: true)
+ q.text("end")
+ end
+ else
+ LoopFormatter.new("until", self, statements).format(q)
+ end
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("until")
+
+ q.breakable
+ q.pp(predicate)
+
+ q.breakable
+ q.pp(statements)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :until,
+ pred: predicate,
+ stmts: statements,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # UntilMod represents the modifier form of a +until+ loop.
+ #
+ # expression until predicate
+ #
+ class UntilMod < Node
+ # [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
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(statement:, predicate:, location:, comments: [])
+ @statement = statement
+ @predicate = predicate
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [statement, predicate]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ statement: statement,
+ predicate: predicate,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ # If we're in the modifier form and we're modifying a `begin`, then this
+ # is a special case where we need to explicitly use the modifier form
+ # because otherwise the semantic meaning changes. This looks like:
+ #
+ # begin
+ # foo
+ # end until bar
+ #
+ # Also, if the statement of the modifier includes an assignment, then we
+ # can't know for certain that it won't impact the predicate, so we need to
+ # force it to stay as it is. This looks like:
+ #
+ # foo = bar until foo
+ #
+ if statement.is_a?(Begin) || ContainsAssignment.call(statement)
+ q.format(statement)
+ q.text(" until ")
+ q.format(predicate)
+ else
+ LoopFormatter.new("until", self, statement).format(q)
+ end
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("until_mod")
+
+ q.breakable
+ q.pp(statement)
+
+ q.breakable
+ q.pp(predicate)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :until_mod,
+ stmt: statement,
+ pred: predicate,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # VarAlias represents when you're using the +alias+ keyword with global
+ # variable arguments.
+ #
+ # alias $new $old
+ #
+ class VarAlias < Node
+ # [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
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(left:, right:, location:, comments: [])
+ @left = left
+ @right = right
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [left, right]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { left: left, right: right, location: location, comments: comments }
+ end
+
+ def format(q)
+ keyword = "alias "
+
+ q.text(keyword)
+ q.format(left)
+ q.text(" ")
+ q.format(right)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("var_alias")
+
+ q.breakable
+ q.pp(left)
+
+ q.breakable
+ q.pp(right)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :var_alias,
+ left: left,
+ right: right,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # VarField represents a variable that is being assigned a value. As such, it
+ # is always a child of an assignment type node.
+ #
+ # variable = value
+ #
+ # In the example above, the VarField node represents the +variable+ token.
+ class VarField < Node
+ # [nil | Const | CVar | GVar | Ident | IVar] the target of this node
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(value:, location:, comments: [])
+ @value = value
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [value]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { value: value, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.format(value) if value
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("var_field")
+
+ q.breakable
+ q.pp(value)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :var_field, value: value, loc: location, cmts: comments }.to_json(
+ *opts
+ )
+ end
+ end
+
+ # VarRef represents a variable reference.
+ #
+ # true
+ #
+ # This can be a plain local variable like the example above. It can also be a
+ # constant, a class variable, a global variable, an instance variable, a
+ # keyword (like +self+, +nil+, +true+, or +false+), or a numbered block
+ # variable.
+ class VarRef < Node
+ # [Const | CVar | GVar | Ident | IVar | Kw] the value of this node
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(value:, location:, comments: [])
+ @value = value
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [value]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { value: value, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.format(value)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("var_ref")
+
+ q.breakable
+ q.pp(value)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :var_ref, value: value, loc: location, cmts: comments }.to_json(
+ *opts
+ )
+ end
+ end
+
+ # PinnedVarRef represents a pinned variable reference within a pattern
+ # matching pattern.
+ #
+ # case value
+ # in ^variable
+ # end
+ #
+ # This can be a plain local variable like the example above. It can also be a
+ # a class variable, a global variable, or an instance variable.
+ class PinnedVarRef < Node
+ # [VarRef] the value of this node
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(value:, location:, comments: [])
+ @value = value
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [value]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { value: value, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.group do
+ q.text("^")
+ q.format(value)
+ end
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("pinned_var_ref")
+
+ q.breakable
+ q.pp(value)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :pinned_var_ref,
+ value: value,
+ loc: location,
+ cmts: comments
+ }.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 < Node
+ # [Ident] the value of this expression
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(value:, location:, comments: [])
+ @value = value
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [value]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { value: value, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.format(value)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("vcall")
+
+ q.breakable
+ q.pp(value)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :vcall, value: value, loc: location, cmts: comments }.to_json(
+ *opts
+ )
+ end
+ end
+
+ # VoidStmt represents an empty lexical block of code.
+ #
+ # ;;
+ #
+ class VoidStmt < Node
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(location:, comments: [])
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ []
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { location: location, comments: comments }
+ end
+
+ def format(q)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("void_stmt")
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :void_stmt, loc: location, cmts: comments }.to_json(*opts)
+ end
+ end
+
+ # When represents a +when+ clause in a +case+ chain.
+ #
+ # case value
+ # when predicate
+ # end
+ #
+ class When < Node
+ # [Args] the arguments to the when clause
+ attr_reader :arguments
+
+ # [Statements] the expressions to be executed
+ attr_reader :statements
+
+ # [nil | Else | When] the next clause in the chain
+ attr_reader :consequent
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(
+ arguments:,
+ statements:,
+ consequent:,
+ location:,
+ comments: []
+ )
+ @arguments = arguments
+ @statements = statements
+ @consequent = consequent
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [arguments, statements, consequent]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ arguments: arguments,
+ statements: statements,
+ consequent: consequent,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ keyword = "when "
+
+ q.group do
+ q.group do
+ q.text(keyword)
+ q.nest(keyword.length) do
+ if arguments.comments.any?
+ q.format(arguments)
+ else
+ separator = -> { q.group { q.comma_breakable } }
+ q.seplist(arguments.parts, separator) { |part| q.format(part) }
+ end
+
+ # Very special case here. If you're inside of a when clause and the
+ # last argument to the predicate is and endless range, then you are
+ # forced to use the "then" keyword to make it parse properly.
+ last = arguments.parts.last
+ if (last.is_a?(Dot2) || last.is_a?(Dot3)) && !last.right
+ q.text(" then")
+ end
+ end
+ end
+
+ unless statements.empty?
+ q.indent do
+ q.breakable(force: true)
+ q.format(statements)
+ end
+ end
+
+ if consequent
+ q.breakable(force: true)
+ q.format(consequent)
+ end
+ end
+ 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
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :when,
+ args: arguments,
+ stmts: statements,
+ cons: consequent,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # While represents a +while+ loop.
+ #
+ # while predicate
+ # end
+ #
+ class While < Node
+ # [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
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(predicate:, statements:, location:, comments: [])
+ @predicate = predicate
+ @statements = statements
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [predicate, statements]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ predicate: predicate,
+ statements: statements,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ if statements.empty?
+ keyword = "while "
+
+ q.group do
+ q.text(keyword)
+ q.nest(keyword.length) { q.format(predicate) }
+ q.breakable(force: true)
+ q.text("end")
+ end
+ else
+ LoopFormatter.new("while", self, statements).format(q)
+ end
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("while")
+
+ q.breakable
+ q.pp(predicate)
+
+ q.breakable
+ q.pp(statements)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :while,
+ pred: predicate,
+ stmts: statements,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # WhileMod represents the modifier form of a +while+ loop.
+ #
+ # expression while predicate
+ #
+ class WhileMod < Node
+ # [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
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(statement:, predicate:, location:, comments: [])
+ @statement = statement
+ @predicate = predicate
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [statement, predicate]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ statement: statement,
+ predicate: predicate,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ # If we're in the modifier form and we're modifying a `begin`, then this
+ # is a special case where we need to explicitly use the modifier form
+ # because otherwise the semantic meaning changes. This looks like:
+ #
+ # begin
+ # foo
+ # end while bar
+ #
+ # Also, if the statement of the modifier includes an assignment, then we
+ # can't know for certain that it won't impact the predicate, so we need to
+ # force it to stay as it is. This looks like:
+ #
+ # foo = bar while foo
+ #
+ if statement.is_a?(Begin) || ContainsAssignment.call(statement)
+ q.format(statement)
+ q.text(" while ")
+ q.format(predicate)
+ else
+ LoopFormatter.new("while", self, statement).format(q)
+ end
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("while_mod")
+
+ q.breakable
+ q.pp(statement)
+
+ q.breakable
+ q.pp(predicate)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :while_mod,
+ stmt: statement,
+ pred: predicate,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # Word represents an element within a special array literal that accepts
+ # interpolation.
+ #
+ # %W[a#{b}c xyz]
+ #
+ # In the example above, there would be two Word nodes within a parent Words
+ # node.
+ class Word < Node
+ # [Array[ StringEmbExpr | StringDVar | TStringContent ]] the parts of the
+ # word
+ attr_reader :parts
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(parts:, location:, comments: [])
+ @parts = parts
+ @location = location
+ @comments = comments
+ end
+
+ def match?(pattern)
+ parts.any? { |part| part.is_a?(TStringContent) && part.match?(pattern) }
+ end
+
+ def child_nodes
+ parts
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { parts: parts, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.format_each(parts)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("word")
+
+ q.breakable
+ q.group(2, "(", ")") { q.seplist(parts) { |part| q.pp(part) } }
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :word, parts: parts, loc: location, cmts: comments }.to_json(
+ *opts
+ )
+ end
+ end
+
+ # Words represents a string literal array with interpolation.
+ #
+ # %W[one two three]
+ #
+ class Words < Node
+ # [WordsBeg] the token that opens this array literal
+ attr_reader :beginning
+
+ # [Array[ Word ]] the elements of this array
+ attr_reader :elements
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(beginning:, elements:, location:, comments: [])
+ @beginning = beginning
+ @elements = elements
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ []
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ {
+ beginning: beginning,
+ elements: elements,
+ location: location,
+ comments: comments
+ }
+ end
+
+ def format(q)
+ opening, closing = "%W[", "]"
+
+ if elements.any? { |element| element.match?(/[\[\]]/) }
+ opening = beginning.value
+ closing = Quotes.matching(opening[2])
+ end
+
+ q.group(0, opening, closing) do
+ q.indent do
+ q.breakable("")
+ q.seplist(elements, -> { q.breakable }) do |element|
+ q.format(element)
+ end
+ end
+ q.breakable("")
+ end
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("words")
+
+ q.breakable
+ q.group(2, "(", ")") { q.seplist(elements) { |element| q.pp(element) } }
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :words, elems: elements, loc: location, cmts: comments }.to_json(
+ *opts
+ )
+ end
+ end
+
+ # WordsBeg represents the beginning of a string literal array with
+ # interpolation.
+ #
+ # %W[one two three]
+ #
+ # In the snippet above, a WordsBeg would be created with the value of "%W[".
+ # Note that these kinds of arrays can start with a lot of different delimiter
+ # types (e.g., %W| or %W<).
+ class WordsBeg < Node
+ # [String] the start of the word literal array
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ def initialize(value:, location:)
+ @value = value
+ @location = location
+ end
+ end
+
+ # XString represents the contents of an XStringLiteral.
+ #
+ # `ls`
+ #
+ class XString < Node
+ # [Array[ StringEmbExpr | StringDVar | TStringContent ]] the parts of the
+ # xstring
+ attr_reader :parts
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ def initialize(parts:, location:)
+ @parts = parts
+ @location = location
+ end
+ end
+
+ # XStringLiteral represents a string that gets executed.
+ #
+ # `ls`
+ #
+ class XStringLiteral < Node
+ # [Array[ StringEmbExpr | StringDVar | TStringContent ]] the parts of the
+ # xstring
+ attr_reader :parts
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(parts:, location:, comments: [])
+ @parts = parts
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ parts
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { parts: parts, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.text("`")
+ q.format_each(parts)
+ q.text("`")
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("xstring_literal")
+
+ q.breakable
+ q.group(2, "(", ")") { q.seplist(parts) { |part| q.pp(part) } }
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ {
+ type: :xstring_literal,
+ parts: parts,
+ loc: location,
+ cmts: comments
+ }.to_json(*opts)
+ end
+ end
+
+ # Yield represents using the +yield+ keyword with arguments.
+ #
+ # yield value
+ #
+ class Yield < Node
+ # [Args | Paren] the arguments passed to the yield
+ attr_reader :arguments
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(arguments:, location:, comments: [])
+ @arguments = arguments
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ [arguments]
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { arguments: arguments, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.group do
+ q.text("yield")
+
+ if arguments.is_a?(Paren)
+ q.format(arguments)
+ else
+ q.if_break { q.text("(") }.if_flat { q.text(" ") }
+ q.indent do
+ q.breakable("")
+ q.format(arguments)
+ end
+ q.breakable("")
+ q.if_break { q.text(")") }
+ end
+ end
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("yield")
+
+ q.breakable
+ q.pp(arguments)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :yield, args: arguments, loc: location, cmts: comments }.to_json(
+ *opts
+ )
+ end
+ end
+
+ # Yield0 represents the bare +yield+ keyword with no arguments.
+ #
+ # yield
+ #
+ class Yield0 < Node
+ # [String] the value of the keyword
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(value:, location:, comments: [])
+ @value = value
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ []
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { value: value, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.text(value)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("yield0")
+
+ q.breakable
+ q.pp(value)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :yield0, value: value, loc: location, cmts: comments }.to_json(
+ *opts
+ )
+ end
+ end
+
+ # ZSuper represents the bare +super+ keyword with no arguments.
+ #
+ # super
+ #
+ class ZSuper < Node
+ # [String] the value of the keyword
+ attr_reader :value
+
+ # [Location] the location of this node
+ attr_reader :location
+
+ # [Array[ Comment | EmbDoc ]] the comments attached to this node
+ attr_reader :comments
+
+ def initialize(value:, location:, comments: [])
+ @value = value
+ @location = location
+ @comments = comments
+ end
+
+ def child_nodes
+ []
+ end
+
+ alias deconstruct child_nodes
+
+ def deconstruct_keys(keys)
+ { value: value, location: location, comments: comments }
+ end
+
+ def format(q)
+ q.text(value)
+ end
+
+ def pretty_print(q)
+ q.group(2, "(", ")") do
+ q.text("zsuper")
+
+ q.breakable
+ q.pp(value)
+
+ q.pp(Comment::List.new(comments))
+ end
+ end
+
+ def to_json(*opts)
+ { type: :zsuper, value: value, loc: location, cmts: comments }.to_json(
+ *opts
+ )
+ end
+ end
+end
diff --git a/lib/syntax_tree/parser.rb b/lib/syntax_tree/parser.rb
new file mode 100644
index 00000000..33eeef8b
--- /dev/null
+++ b/lib/syntax_tree/parser.rb
@@ -0,0 +1,3098 @@
+# frozen_string_literal: true
+
+module SyntaxTree
+ class Parser < Ripper
+ # 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
+
+ # 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
+ attr_reader :start
+
+ 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
+ attr_reader :start, :indices
+
+ def initialize(start, line)
+ @start = start
+ @indices = []
+
+ line.each_char.with_index(start) do |char, index|
+ char.bytesize.times { @indices << index }
+ end
+ end
+
+ # Technically it's possible for the column index to be a negative value if
+ # there's a BOM at the beginning of the file, which is the reason we need
+ # to compare it to 0 here.
+ def [](byteindex)
+ indices[byteindex < 0 ? 0 : byteindex]
+ end
+ end
+
+ # [String] the source being parsed
+ attr_reader :source
+
+ # [Array[ String ]] the list of lines in the source
+ attr_reader :lines
+
+ # [Array[ SingleByteString | MultiByteString ]] the list of objects that
+ # represent the start of each line in character offsets
+ attr_reader :line_counts
+
+ # [Array[ untyped ]] a running list of tokens that have been found in the
+ # source. This list changes a lot as certain nodes will "consume" these
+ # tokens to determine their bounds.
+ attr_reader :tokens
+
+ # [Array[ Comment | EmbDoc ]] the list of comments that have been found
+ # while parsing the source.
+ attr_reader :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(/\r?\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
+
+ # Make sure line counts is filled out with the first and last line at
+ # minimum so that it has something to compare against if the parser is in
+ # a lineno=2 state for an empty file.
+ @line_counts << SingleByteString.new(0) if @line_counts.empty?
+ @line_counts << SingleByteString.new(last_index)
+ 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, location: nil)
+ 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
+ token = value == :any ? type.name.split("::", 2).last : value
+ message = "Cannot find expected #{token}"
+
+ if location
+ lineno = location.start_line
+ column = location.start_char - line_counts[lineno - 1].start
+ raise ParseError.new(message, lineno, column)
+ else
+ raise ParseError.new(message, lineno, column)
+ end
+ 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.
+ # --------------------------------------------------------------------------
+
+ # :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
+
+ # :call-seq:
+ # on_CHAR: (String value) -> CHAR
+ def on_CHAR(value)
+ CHAR.new(
+ value: value,
+ location: Location.token(line: lineno, char: char_pos, size: value.size)
+ )
+ 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
+
+ # :call-seq:
+ # on___end__: (String value) -> EndContent
+ def on___end__(value)
+ @__end__ =
+ EndContent.new(
+ value: source[(char_pos + value.length)..-1],
+ location: Location.token(line: lineno, char: char_pos, size: value.size)
+ )
+ 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
+
+ # :call-seq:
+ # on_aref: (untyped collection, (nil | Args) 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
+
+ # :call-seq:
+ # on_aref_field: (
+ # untyped collection,
+ # (nil | Args) 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
+
+ # :call-seq:
+ # on_arg_paren: (
+ # (nil | Args | 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
+
+ # :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
+
+ # :call-seq:
+ # on_args_add_block: (
+ # Args arguments,
+ # (false | untyped) block
+ # ) -> Args
+ def on_args_add_block(arguments, block)
+ operator = find_token(Op, "&", consume: false)
+
+ # If we can't find the & operator, then there's no block to add to the
+ # list, so we're just going to return the arguments as-is.
+ return arguments unless operator
+
+ # Now we know we have an & operator, so we're going to delete it from the
+ # list of tokens to make sure it doesn't get confused with anything else.
+ tokens.delete(operator)
+
+ # Construct the location that represents the block argument.
+ location = operator.location
+ location = operator.location.to(block.location) if block
+
+ # If there are any arguments and the operator we found from the list is
+ # not after them, then we're going to return the arguments as-is because
+ # we're looking at an & that occurs before the arguments are done.
+ if arguments.parts.any? && location.start_char < arguments.location.end_char
+ return arguments
+ end
+
+ # Otherwise, we're looking at an actual block argument (with or without a
+ # block, which could be missing because it could be a bare & since 3.1.0).
+ arg_block = ArgBlock.new(value: block, location: location)
+
+ Args.new(
+ parts: arguments.parts << arg_block,
+ location: arguments.location.to(location)
+ )
+ 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
+
+ # :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
+
+ # :call-seq:
+ # on_array: ((nil | Args) contents) ->
+ # ArrayLiteral | QSymbols | QWords | Symbols | Words
+ def on_array(contents)
+ if !contents || contents.is_a?(Args)
+ lbracket = find_token(LBracket)
+ rbracket = find_token(RBracket)
+
+ ArrayLiteral.new(
+ lbracket: lbracket,
+ contents: contents,
+ location: lbracket.location.to(rbracket.location)
+ )
+ else
+ tstring_end =
+ find_token(TStringEnd, location: contents.beginning.location)
+
+ contents.class.new(
+ beginning: contents.beginning,
+ elements: contents.elements,
+ location: contents.location.to(tstring_end.location)
+ )
+ 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
+
+ # :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
+
+ # :call-seq:
+ # on_assoc_new: (untyped key, untyped value) -> Assoc
+ def on_assoc_new(key, value)
+ location = key.location
+ location = location.to(value.location) if value
+
+ Assoc.new(key: key, value: value, location: location)
+ 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
+
+ # def on_assoclist_from_args(assocs)
+ # assocs
+ # end
+
+ # :call-seq:
+ # on_backref: (String value) -> Backref
+ def on_backref(value)
+ Backref.new(
+ value: value,
+ location: Location.token(line: lineno, char: char_pos, size: value.size)
+ )
+ 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
+
+ # :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
+
+ # :call-seq:
+ # on_begin: (untyped bodystmt) -> Begin | PinnedBegin
+ def on_begin(bodystmt)
+ pin = find_token(Op, "^", consume: false)
+
+ if pin && pin.location.start_char < bodystmt.location.start_char
+ tokens.delete(pin)
+ find_token(LParen)
+
+ rparen = find_token(RParen)
+ location = pin.location.to(rparen.location)
+
+ PinnedBegin.new(statement: bodystmt, location: location)
+ else
+ 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)
+ location = keyword.location.to(bodystmt.location)
+
+ Begin.new(bodystmt: bodystmt, location: location)
+ end
+ end
+
+ # :call-seq:
+ # on_binary: (untyped left, (Op | Symbol) operator, untyped right) -> Binary
+ def on_binary(left, operator, right)
+ if operator.is_a?(Symbol)
+ # Here, we're going to search backward for the token that's between the
+ # two operands that matches the operator so we can delete it from the
+ # list.
+ index =
+ tokens.rindex do |token|
+ location = token.location
+
+ token.is_a?(Op) && token.value == operator.to_s &&
+ location.start_char > left.location.end_char &&
+ location.end_char < right.location.start_char
+ end
+
+ tokens.delete_at(index) if index
+ else
+ # 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
+ end
+
+ Binary.new(
+ left: left,
+ operator: operator,
+ right: right,
+ location: left.location.to(right.location)
+ )
+ 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
+
+ # :call-seq:
+ # on_blockarg: (Ident name) -> BlockArg
+ def on_blockarg(name)
+ operator = find_token(Op, "&")
+
+ location = operator.location
+ location = location.to(name.location) if name
+
+ BlockArg.new(name: name, location: location)
+ 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
+
+ # :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
+
+ # :call-seq:
+ # on_break: (Args arguments) -> Break
+ def on_break(arguments)
+ keyword = find_token(Kw, "break")
+
+ location = keyword.location
+ location = location.to(arguments.location) if arguments.parts.any?
+
+ Break.new(arguments: arguments, location: location)
+ 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,
+ arguments: nil,
+ location: receiver.location.to(ending.location)
+ )
+ 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(
+ keyword: keyword,
+ 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
+
+ # :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
+
+ # :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
+
+ # :call-seq:
+ # on_command: ((Const | Ident) message, Args arguments) -> Command
+ def on_command(message, arguments)
+ Command.new(
+ message: message,
+ arguments: arguments,
+ location: message.location.to(arguments.location)
+ )
+ end
+
+ # :call-seq:
+ # on_command_call: (
+ # untyped receiver,
+ # (:"::" | Op | Period) operator,
+ # (Const | Ident | Op) message,
+ # (nil | Args) 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
+
+ # :call-seq:
+ # on_comment: (String value) -> Comment
+ def on_comment(value)
+ line = lineno
+ comment =
+ Comment.new(
+ value: value.chomp,
+ inline: value.strip != lines[line - 1].strip,
+ location:
+ Location.token(line: line, char: char_pos, size: value.size - 1)
+ )
+
+ @comments << comment
+ comment
+ end
+
+ # :call-seq:
+ # on_const: (String value) -> Const
+ def on_const(value)
+ Const.new(
+ value: value,
+ location: Location.token(line: lineno, char: char_pos, size: value.size)
+ )
+ 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
+
+ # :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
+
+ # :call-seq:
+ # on_const_ref: (Const constant) -> ConstRef
+ def on_const_ref(constant)
+ ConstRef.new(constant: constant, location: constant.location)
+ end
+
+ # :call-seq:
+ # on_cvar: (String value) -> CVar
+ def on_cvar(value)
+ CVar.new(
+ value: value,
+ location: Location.token(line: lineno, char: char_pos, size: value.size)
+ )
+ end
+
+ # :call-seq:
+ # on_def: (
+ # (Backtick | Const | Ident | Kw | Op) name,
+ # (nil | 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 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", consume: false)
+
+ if ending
+ tokens.delete(ending)
+ 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)
+ )
+ else
+ # In Ruby >= 3.1.0, this is a BodyStmt that wraps a single statement in
+ # the statements list. Before, it was just the individual statement.
+ statement = bodystmt.is_a?(BodyStmt) ? bodystmt.statements : bodystmt
+
+ DefEndless.new(
+ target: nil,
+ operator: nil,
+ name: name,
+ paren: params,
+ statement: statement,
+ location: beginning.location.to(bodystmt.location)
+ )
+ 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
+
+ # :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", consume: false)
+
+ if ending
+ tokens.delete(ending)
+ 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)
+ )
+ else
+ # In Ruby >= 3.1.0, this is a BodyStmt that wraps a single statement in
+ # the statements list. Before, it was just the individual statement.
+ statement = bodystmt.is_a?(BodyStmt) ? bodystmt.statements : bodystmt
+
+ DefEndless.new(
+ target: target,
+ operator: operator,
+ name: name,
+ paren: params,
+ statement: statement,
+ location: beginning.location.to(bodystmt.location)
+ )
+ 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
+
+ # :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
+
+ # :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
+
+ # :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, location: symbeg.location)
+
+ 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
+
+ # :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
+ # ending = node
+
+ statements.bind(beginning.location.end_char, ending.location.start_char)
+
+ Else.new(
+ statements: statements,
+ location: beginning.location.to(ending.location)
+ )
+ 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
+
+ # :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
+
+ # :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
+
+ # :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
+
+ # :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
+
+ # :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
+
+ # 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
+
+ # :call-seq:
+ # on_fcall: ((Const | Ident) value) -> FCall
+ def on_fcall(value)
+ FCall.new(value: value, arguments: nil, location: value.location)
+ 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
+
+ # :call-seq:
+ # on_float: (String value) -> FloatLiteral
+ def on_float(value)
+ FloatLiteral.new(
+ value: value,
+ location: Location.token(line: lineno, char: char_pos, size: value.size)
+ )
+ 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
+
+ # :call-seq:
+ # on_for: (
+ # (MLHS | VarField) value,
+ # untyped collection,
+ # Statements statements
+ # ) -> For
+ def on_for(index, collection, statements)
+ beginning = find_token(Kw, "for")
+ in_keyword = find_token(Kw, "in")
+ 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
+ )
+
+ if index.is_a?(MLHS)
+ comma_range = index.location.end_char...in_keyword.location.start_char
+ index.comma = true if source[comma_range].strip.start_with?(",")
+ end
+
+ For.new(
+ index: index,
+ collection: collection,
+ statements: statements,
+ location: beginning.location.to(ending.location)
+ )
+ end
+
+ # :call-seq:
+ # on_gvar: (String value) -> GVar
+ def on_gvar(value)
+ GVar.new(
+ value: value,
+ location: Location.token(line: lineno, char: char_pos, size: value.size)
+ )
+ end
+
+ # :call-seq:
+ # on_hash: ((nil | Array[AssocNew | AssocSplat]) assocs) -> HashLiteral
+ def on_hash(assocs)
+ lbrace = find_token(LBrace)
+ rbrace = find_token(RBrace)
+
+ HashLiteral.new(
+ lbrace: lbrace,
+ assocs: assocs || [],
+ location: lbrace.location.to(rbrace.location)
+ )
+ 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
+
+ # :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
+
+ # :call-seq:
+ # on_ident: (String value) -> Ident
+ def on_ident(value)
+ Ident.new(
+ value: value,
+ location: Location.token(line: lineno, char: char_pos, size: value.size)
+ )
+ 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
+
+ # :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
+
+ # :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
+
+ # :call-seq:
+ # on_imaginary: (String value) -> Imaginary
+ def on_imaginary(value)
+ Imaginary.new(
+ value: value,
+ location: Location.token(line: lineno, char: char_pos, size: value.size)
+ )
+ 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_start = pattern
+ if token = find_token(Kw, "then", consume: false)
+ tokens.delete(token)
+ statements_start = token
+ end
+
+ statements.bind(
+ find_next_statement_start(statements_start.location.end_char),
+ ending.location.start_char
+ )
+
+ In.new(
+ pattern: pattern,
+ statements: statements,
+ consequent: consequent,
+ location: beginning.location.to(ending.location)
+ )
+ end
+
+ # :call-seq:
+ # on_int: (String value) -> Int
+ def on_int(value)
+ Int.new(
+ value: value,
+ location: Location.token(line: lineno, char: char_pos, size: value.size)
+ )
+ end
+
+ # :call-seq:
+ # on_ivar: (String value) -> IVar
+ def on_ivar(value)
+ IVar.new(
+ value: value,
+ location: Location.token(line: lineno, char: char_pos, size: value.size)
+ )
+ 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
+
+ # :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
+
+ # :call-seq:
+ # on_label: (String value) -> Label
+ def on_label(value)
+ Label.new(
+ value: value,
+ location: Location.token(line: lineno, char: char_pos, size: value.size)
+ )
+ 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
+
+ # :call-seq:
+ # on_lambda: (
+ # (Params | Paren) params,
+ # (BodyStmt | Statements) statements
+ # ) -> Lambda
+ def on_lambda(params, statements)
+ beginning = find_token(TLambda)
+
+ if tokens.any? { |token|
+ token.is_a?(TLamBeg) &&
+ token.location.start_char > beginning.location.start_char
+ }
+ opening = find_token(TLamBeg)
+ 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
+
+ # :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
+
+ # :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
+
+ # :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
+
+ # :call-seq:
+ # on_massign: ((MLHS | 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
+
+ # :call-seq:
+ # on_method_add_arg: (
+ # (Call | FCall) call,
+ # (ArgParen | Args) arguments
+ # ) -> Call | FCall
+ def on_method_add_arg(call, arguments)
+ location = call.location
+ location = location.to(arguments.location) if arguments.is_a?(ArgParen)
+
+ if call.is_a?(FCall)
+ FCall.new(value: call.value, arguments: arguments, location: location)
+ else
+ Call.new(
+ receiver: call.receiver,
+ operator: call.operator,
+ message: call.message,
+ arguments: arguments,
+ location: location
+ )
+ end
+ end
+
+ # :call-seq:
+ # on_method_add_block: (
+ # (Call | Command | CommandCall | FCall) 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
+
+ # :call-seq:
+ # on_mlhs_add: (
+ # MLHS mlhs,
+ # (ARefField | Field | Ident | MLHSParen | VarField) part
+ # ) -> MLHS
+ def on_mlhs_add(mlhs, part)
+ location =
+ mlhs.parts.empty? ? part.location : mlhs.location.to(part.location)
+
+ MLHS.new(parts: mlhs.parts << part, location: location)
+ end
+
+ # :call-seq:
+ # on_mlhs_add_post: (MLHS left, MLHS right) -> MLHS
+ def on_mlhs_add_post(left, right)
+ MLHS.new(
+ parts: left.parts + right.parts,
+ location: left.location.to(right.location)
+ )
+ end
+
+ # :call-seq:
+ # on_mlhs_add_star: (
+ # MLHS mlhs,
+ # (nil | ARefField | Field | Ident | VarField) part
+ # ) -> MLHS
+ def on_mlhs_add_star(mlhs, part)
+ beginning = find_token(Op, "*")
+ ending = part || beginning
+
+ location = beginning.location.to(ending.location)
+ arg_star = ArgStar.new(value: part, location: location)
+
+ location = mlhs.location.to(location) unless mlhs.parts.empty?
+ MLHS.new(parts: mlhs.parts << arg_star, location: location)
+ end
+
+ # :call-seq:
+ # on_mlhs_new: () -> MLHS
+ def on_mlhs_new
+ MLHS.new(parts: [], location: Location.fixed(line: lineno, char: char_pos))
+ end
+
+ # :call-seq:
+ # on_mlhs_paren: ((MLHS | 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
+
+ # :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
+
+ # :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)
+ location =
+ if mrhs.parts.empty?
+ mrhs.location
+ else
+ mrhs.location.to(part.location)
+ end
+
+ MRHS.new(parts: mrhs.parts << part, location: location)
+ end
+
+ # :call-seq:
+ # on_mrhs_add_star: (MRHS mrhs, untyped value) -> MRHS
+ def on_mrhs_add_star(mrhs, value)
+ beginning = find_token(Op, "*")
+ ending = value || beginning
+
+ arg_star =
+ ArgStar.new(
+ value: value,
+ location: beginning.location.to(ending.location)
+ )
+
+ location =
+ if mrhs.parts.empty?
+ arg_star.location
+ else
+ mrhs.location.to(arg_star.location)
+ end
+
+ MRHS.new(parts: mrhs.parts << arg_star, location: location)
+ end
+
+ # :call-seq:
+ # on_mrhs_new_from_args: (Args arguments) -> MRHS
+ def on_mrhs_new_from_args(arguments)
+ MRHS.new(parts: arguments.parts, location: arguments.location)
+ end
+
+ # :call-seq:
+ # on_next: (Args arguments) -> Next
+ def on_next(arguments)
+ keyword = find_token(Kw, "next")
+
+ location = keyword.location
+ location = location.to(arguments.location) if arguments.parts.any?
+
+ Next.new(arguments: arguments, location: location)
+ end
+
+ # def on_nl(value)
+ # value
+ # end
+
+ # def on_nokw_param(value)
+ # value
+ # 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
+
+ # :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
+
+ # :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 | ArgsForward | 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 if 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 if block != :&),
+ location: location
+ )
+ 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 || nil,
+ 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
+
+ # :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
+
+ # :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 = Program.new(statements: statements, location: location)
+ attach_comments(program, @comments)
+
+ program
+ end
+
+ # Attaches comments to the nodes in the tree that most closely correspond to
+ # the location of the comments.
+ def attach_comments(program, comments)
+ comments.each do |comment|
+ preceding, enclosing, following = nearest_nodes(program, comment)
+
+ if comment.inline?
+ if preceding
+ preceding.comments << comment
+ comment.trailing!
+ elsif following
+ following.comments << comment
+ comment.leading!
+ elsif enclosing
+ enclosing.comments << comment
+ else
+ program.comments << comment
+ end
+ else
+ # If a comment exists on its own line, prefer a leading comment.
+ if following
+ following.comments << comment
+ comment.leading!
+ elsif preceding
+ preceding.comments << comment
+ comment.trailing!
+ elsif enclosing
+ enclosing.comments << comment
+ else
+ program.comments << comment
+ end
+ end
+ end
+ end
+
+ # Responsible for finding the nearest nodes to the given comment within the
+ # context of the given encapsulating node.
+ def nearest_nodes(node, comment)
+ comment_start = comment.location.start_char
+ comment_end = comment.location.end_char
+
+ child_nodes = node.child_nodes.compact
+ preceding = nil
+ following = nil
+
+ left = 0
+ right = child_nodes.length
+
+ # This is a custom binary search that finds the nearest nodes to the given
+ # comment. When it finds a node that completely encapsulates the comment,
+ # it recursed downward into the tree.
+ while left < right
+ middle = (left + right) / 2
+ child = child_nodes[middle]
+
+ node_start = child.location.start_char
+ node_end = child.location.end_char
+
+ if node_start <= comment_start && comment_end <= node_end
+ # The comment is completely contained by this child node. Abandon the
+ # binary search at this level.
+ return nearest_nodes(child, comment)
+ end
+
+ if node_end <= comment_start
+ # This child node falls completely before the comment. Because we will
+ # never consider this node or any nodes before it again, this node
+ # must be the closest preceding node we have encountered so far.
+ preceding = child
+ left = middle + 1
+ next
+ end
+
+ if comment_end <= node_start
+ # This child node falls completely after the comment. Because we will
+ # never consider this node or any nodes after it again, this node must
+ # be the closest following node we have encountered so far.
+ following = child
+ right = middle
+ next
+ end
+
+ # This should only happen if there is a bug in this parser.
+ raise "Comment location overlaps with node location"
+ end
+
+ [preceding, node, following]
+ end
+
+ # :call-seq:
+ # on_qsymbols_add: (QSymbols qsymbols, TStringContent element) -> QSymbols
+ def on_qsymbols_add(qsymbols, element)
+ QSymbols.new(
+ beginning: qsymbols.beginning,
+ elements: qsymbols.elements << element,
+ location: qsymbols.location.to(element.location)
+ )
+ 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
+ beginning = find_token(QSymbolsBeg)
+
+ QSymbols.new(
+ beginning: beginning,
+ elements: [],
+ location: beginning.location
+ )
+ end
+
+ # :call-seq:
+ # on_qwords_add: (QWords qwords, TStringContent element) -> QWords
+ def on_qwords_add(qwords, element)
+ QWords.new(
+ beginning: qwords.beginning,
+ elements: qwords.elements << element,
+ location: qwords.location.to(element.location)
+ )
+ 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
+ beginning = find_token(QWordsBeg)
+
+ QWords.new(beginning: beginning, elements: [], location: beginning.location)
+ end
+
+ # :call-seq:
+ # on_rational: (String value) -> RationalLiteral
+ def on_rational(value)
+ RationalLiteral.new(
+ value: value,
+ location: Location.token(line: lineno, char: char_pos, size: value.size)
+ )
+ 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
+
+ # :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
+
+ # :call-seq:
+ # on_redo: () -> Redo
+ def on_redo
+ keyword = find_token(Kw, "redo")
+
+ Redo.new(value: keyword.value, location: keyword.location)
+ 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
+
+ # :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
+
+ # :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
+
+ # :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
+
+ # :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
+
+ # :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
+
+ # :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
+
+ # :call-seq:
+ # on_retry: () -> Retry
+ def on_retry
+ keyword = find_token(Kw, "retry")
+
+ Retry.new(value: keyword.value, location: keyword.location)
+ end
+
+ # :call-seq:
+ # on_return: (Args arguments) -> Return
+ def on_return(arguments)
+ keyword = find_token(Kw, "return")
+
+ Return.new(
+ arguments: arguments,
+ location: keyword.location.to(arguments.location)
+ )
+ end
+
+ # :call-seq:
+ # on_return0: () -> Return0
+ def on_return0
+ keyword = find_token(Kw, "return")
+
+ Return0.new(value: keyword.value, location: keyword.location)
+ 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
+
+ # :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)
+ location =
+ if statements.body.empty?
+ statement.location
+ else
+ statements.location.to(statement.location)
+ end
+
+ Statements.new(self, body: statements.body << statement, location: location)
+ end
+
+ # :call-seq:
+ # on_stmts_new: () -> Statements
+ def on_stmts_new
+ Statements.new(
+ self,
+ body: [],
+ location: Location.fixed(line: lineno, char: char_pos)
+ )
+ 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
+
+ # :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
+
+ # :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
+
+ # :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
+ )
+
+ location =
+ Location.new(
+ start_line: embexpr_beg.location.start_line,
+ start_char: embexpr_beg.location.start_char,
+ end_line: [
+ embexpr_end.location.end_line,
+ statements.location.end_line
+ ].max,
+ end_char: embexpr_end.location.end_char
+ )
+
+ StringEmbExpr.new(statements: statements, location: location)
+ 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, location: tstring_beg.location)
+
+ location =
+ Location.new(
+ start_line: tstring_beg.location.start_line,
+ start_char: tstring_beg.location.start_char,
+ end_line: [
+ tstring_end.location.end_line,
+ string.location.end_line
+ ].max,
+ end_char: tstring_end.location.end_char
+ )
+
+ StringLiteral.new(
+ parts: string.parts,
+ quote: tstring_beg.value,
+ location: location
+ )
+ end
+ end
+
+ # :call-seq:
+ # on_super: ((ArgParen | Args) arguments) -> Super
+ def on_super(arguments)
+ keyword = find_token(Kw, "super")
+
+ Super.new(
+ arguments: arguments,
+ location: keyword.location.to(arguments.location)
+ )
+ 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
+
+ # :call-seq:
+ # on_symbol: (
+ # (Backtick | Const | CVar | GVar | Ident | IVar | Kw | Op) value
+ # ) -> SymbolContent
+ def on_symbol(value)
+ tokens.delete(value)
+
+ SymbolContent.new(value: value, location: value.location)
+ end
+
+ # :call-seq:
+ # on_symbol_literal: (
+ # (
+ # Backtick | Const | CVar | GVar | Ident |
+ # IVar | Kw | Op | SymbolContent
+ # ) value
+ # ) -> SymbolLiteral
+ def on_symbol_literal(value)
+ if value.is_a?(SymbolContent)
+ symbeg = find_token(SymBeg)
+
+ SymbolLiteral.new(
+ value: value.value,
+ location: symbeg.location.to(value.location)
+ )
+ else
+ tokens.delete(value)
+ SymbolLiteral.new(value: value, location: value.location)
+ end
+ end
+
+ # :call-seq:
+ # on_symbols_add: (Symbols symbols, Word word) -> Symbols
+ def on_symbols_add(symbols, word)
+ Symbols.new(
+ beginning: symbols.beginning,
+ elements: symbols.elements << word,
+ location: symbols.location.to(word.location)
+ )
+ 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
+ beginning = find_token(SymbolsBeg)
+
+ Symbols.new(
+ beginning: beginning,
+ elements: [],
+ location: beginning.location
+ )
+ 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
+
+ # :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
+
+ # :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
+
+ # :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
+
+ # :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
+
+ # :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
+
+ # :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
+
+ # :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
+
+ # :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
+
+ # :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
+
+ # :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
+
+ # :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
+
+ # :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
+
+ # :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
+
+ # :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
+
+ # :call-seq:
+ # on_var_ref: ((Const | CVar | GVar | Ident | IVar | Kw) value) -> VarRef
+ def on_var_ref(value)
+ pin = find_token(Op, "^", consume: false)
+
+ if pin && pin.location.start_char == value.location.start_char - 1
+ tokens.delete(pin)
+ PinnedVarRef.new(value: value, location: pin.location.to(value.location))
+ else
+ VarRef.new(value: value, location: value.location)
+ end
+ end
+
+ # :call-seq:
+ # on_vcall: (Ident ident) -> VCall
+ def on_vcall(ident)
+ VCall.new(value: ident, location: ident.location)
+ end
+
+ # :call-seq:
+ # on_void_stmt: () -> VoidStmt
+ def on_void_stmt
+ VoidStmt.new(location: Location.fixed(line: lineno, char: char_pos))
+ end
+
+ # :call-seq:
+ # on_when: (
+ # Args 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_start = arguments
+ if token = find_token(Kw, "then", consume: false)
+ tokens.delete(token)
+ statements_start = token
+ end
+
+ statements.bind(
+ find_next_statement_start(statements_start.location.end_char),
+ ending.location.start_char
+ )
+
+ When.new(
+ arguments: arguments,
+ statements: statements,
+ consequent: consequent,
+ location: beginning.location.to(ending.location)
+ )
+ 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
+
+ # :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
+
+ # :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
+
+ # :call-seq:
+ # on_words_add: (Words words, Word word) -> Words
+ def on_words_add(words, word)
+ Words.new(
+ beginning: words.beginning,
+ elements: words.elements << word,
+ location: words.location.to(word.location)
+ )
+ 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
+ beginning = find_token(WordsBeg)
+
+ Words.new(beginning: beginning, elements: [], location: beginning.location)
+ end
+
+ # def on_words_sep(value)
+ # value
+ # 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
+
+ # :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, location: xstring.location)
+
+ XStringLiteral.new(
+ parts: xstring.parts,
+ location: xstring.location.to(ending.location)
+ )
+ end
+ end
+
+ # :call-seq:
+ # on_yield: ((Args | Paren) arguments) -> Yield
+ def on_yield(arguments)
+ keyword = find_token(Kw, "yield")
+
+ Yield.new(
+ arguments: arguments,
+ location: keyword.location.to(arguments.location)
+ )
+ end
+
+ # :call-seq:
+ # on_yield0: () -> Yield0
+ def on_yield0
+ keyword = find_token(Kw, "yield")
+
+ Yield0.new(value: keyword.value, location: keyword.location)
+ end
+
+ # :call-seq:
+ # on_zsuper: () -> ZSuper
+ def on_zsuper
+ keyword = find_token(Kw, "super")
+
+ ZSuper.new(value: keyword.value, location: keyword.location)
+ end
+ end
+end
diff --git a/lib/syntax_tree/version.rb b/lib/syntax_tree/version.rb
index ed245aa7..ddee0eb0 100644
--- a/lib/syntax_tree/version.rb
+++ b/lib/syntax_tree/version.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require "ripper"
-
-class SyntaxTree < Ripper
- VERSION = "1.2.0"
+module SyntaxTree
+ VERSION = "2.0.0"
end
diff --git a/test/behavior_test.rb b/test/behavior_test.rb
index 81308464..707cdd9b 100644
--- a/test/behavior_test.rb
+++ b/test/behavior_test.rb
@@ -2,7 +2,7 @@
require_relative "test_helper"
-class SyntaxTree
+module SyntaxTree
class BehaviorTest < Minitest::Test
def test_empty
void_stmt = SyntaxTree.parse("").statements.body.first
@@ -15,7 +15,7 @@ def test_multibyte
end
def test_parse_error
- assert_raises(ParseError) { SyntaxTree.parse("<>") }
+ assert_raises(Parser::ParseError) { SyntaxTree.parse("<>") }
end
def test_next_statement_start
diff --git a/test/formatting_test.rb b/test/formatting_test.rb
index 8fcb6ae2..f7c3b3f7 100644
--- a/test/formatting_test.rb
+++ b/test/formatting_test.rb
@@ -2,7 +2,7 @@
require_relative "test_helper"
-class SyntaxTree
+module SyntaxTree
class FormattingTest < Minitest::Test
delimiter = /%(?: # (.+?))?\n/
diff --git a/test/idempotency_test.rb b/test/idempotency_test.rb
index ba2bc1c0..fde141f5 100644
--- a/test/idempotency_test.rb
+++ b/test/idempotency_test.rb
@@ -3,7 +3,7 @@
return if ENV["FAST"]
require_relative "test_helper"
-class SyntaxTree
+module SyntaxTree
class IdempotencyTest < Minitest::Test
Dir[File.join(RbConfig::CONFIG["libdir"], "**/*.rb")].each do |filepath|
define_method(:"test_#{filepath}") do
diff --git a/test/node_test.rb b/test/node_test.rb
index b5b064e6..1382ed79 100644
--- a/test/node_test.rb
+++ b/test/node_test.rb
@@ -2,7 +2,7 @@
require_relative "test_helper"
-class SyntaxTree
+module SyntaxTree
class NodeTest < Minitest::Test
def self.guard_version(version)
yield if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new(version)
@@ -151,7 +151,7 @@ def test_assoc
guard_version("3.1.0") do
def test_assoc_no_value
source = "{ key1:, key2: }"
-
+
at = location(chars: 2..7)
assert_node(Assoc, "assoc", source, at: at) { |node| node.assocs.first }
end
@@ -238,7 +238,7 @@ def test_blockarg
guard_version("3.1.0") do
def test_blockarg_anonymous
source = "def method(&); end"
-
+
at = location(chars: 11..12)
assert_node(BlockArg, "blockarg", source, at: at) do |node|
node.params.contents.block
@@ -729,7 +729,7 @@ def test_period
end
def test_program
- parser = SyntaxTree.new("variable")
+ parser = SyntaxTree::Parser.new("variable")
program = parser.parse
refute(parser.error?)
@@ -1013,7 +1013,7 @@ def assert_node(kind, type, source, at: nil)
# Parse the example, get the outputted parse tree, and assert that it was
# able to successfully parse.
- parser = SyntaxTree.new(source)
+ parser = SyntaxTree::Parser.new(source)
program = parser.parse
refute(parser.error?)