diff --git a/.gitattributes b/.gitattributes
deleted file mode 100644
index b24bb2da..00000000
--- a/.gitattributes
+++ /dev/null
@@ -1 +0,0 @@
-bin/* linguist-language=Ruby
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 468591bd..77b7f6b2 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -9,19 +9,20 @@ jobs:
strategy:
fail-fast: false
matrix:
+ os:
+ - ubuntu-latest
+ - macos-latest
+ - windows-latest
ruby:
- - '2.7.0'
- - '3.0'
- - '3.1'
- '3.2'
- '3.3'
- '3.4'
- truffleruby-head
+ exclude:
+ - os: windows-latest
+ ruby: truffleruby-head
name: CI
- runs-on: ubuntu-latest
- env:
- CI: true
- # TESTOPTS: --verbose
+ runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@master
- uses: ruby/setup-ruby@v1
@@ -34,8 +35,6 @@ jobs:
check:
name: Check
runs-on: ubuntu-latest
- env:
- CI: true
steps:
- uses: actions/checkout@master
- uses: ruby/setup-ruby@v1
diff --git a/.rubocop.yml b/.rubocop.yml
index 1b81a535..51159813 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -5,7 +5,7 @@ AllCops:
DisplayStyleGuide: true
NewCops: enable
SuggestExtensions: false
- TargetRubyVersion: 2.7
+ TargetRubyVersion: 3.2
Exclude:
- '{.git,.github,.ruby-lsp,bin,coverage,doc,pkg,sorbet,spec,test/fixtures,vendor,tmp}/**/*'
- test.rb
@@ -14,7 +14,7 @@ Gemspec/DevelopmentDependencies:
Enabled: false
Layout/LineLength:
- Max: 80
+ Max: 100
Lint/AmbiguousBlockAssociation:
Enabled: false
@@ -70,6 +70,9 @@ Naming/MethodName:
Naming/MethodParameterName:
Enabled: false
+Naming/PredicateMethod:
+ Enabled: false
+
Naming/RescuedExceptionsVariableName:
PreferredName: error
@@ -79,12 +82,18 @@ Naming/VariableNumber:
Security/Eval:
Enabled: false
+Style/AccessModifierDeclarations:
+ Enabled: false
+
Style/AccessorGrouping:
Enabled: false
Style/Alias:
Enabled: false
+Style/BlockDelimiters:
+ Enabled: false
+
Style/CaseEquality:
Enabled: false
@@ -151,6 +160,9 @@ Style/Next:
Style/NumericPredicate:
Enabled: false
+Style/OptionalBooleanParameter:
+ Enabled: false
+
Style/ParallelAssignment:
Enabled: false
diff --git a/Gemfile b/Gemfile
index b4252fb5..8a08c3e6 100644
--- a/Gemfile
+++ b/Gemfile
@@ -4,4 +4,8 @@ source "https://rubygems.org"
gemspec
-gem "fiddle"
+gem "bundler"
+gem "minitest"
+gem "rake"
+gem "rubocop"
+gem "simplecov"
diff --git a/Gemfile.lock b/Gemfile.lock
index 7be345b8..17b50c13 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -2,23 +2,21 @@ PATH
remote: .
specs:
syntax_tree (6.3.0)
- prettier_print (>= 1.2.0)
+ prism
GEM
remote: https://rubygems.org/
specs:
ast (2.4.3)
docile (1.4.1)
- fiddle (1.1.8)
- json (2.15.1)
+ json (2.15.2)
language_server-protocol (3.17.0.5)
lint_roller (1.1.0)
minitest (5.26.0)
parallel (1.27.0)
- parser (3.3.9.0)
+ parser (3.3.10.0)
ast (~> 2.4.1)
racc
- prettier_print (1.2.1)
prism (1.6.0)
racc (1.8.1)
rainbow (3.1.1)
@@ -43,22 +41,19 @@ GEM
docile (~> 1.1)
simplecov-html (~> 0.11)
simplecov_json_formatter (~> 0.1)
- simplecov-html (0.13.1)
+ simplecov-html (0.13.2)
simplecov_json_formatter (0.1.4)
unicode-display_width (3.2.0)
unicode-emoji (~> 4.1)
unicode-emoji (4.1.0)
PLATFORMS
- arm64-darwin-21
- ruby
- x86_64-darwin-19
- x86_64-darwin-21
+ arm64-darwin-23
+ x64-mingw-ucrt
x86_64-linux
DEPENDENCIES
bundler
- fiddle
minitest
rake
rubocop
@@ -66,4 +61,4 @@ DEPENDENCIES
syntax_tree!
BUNDLED WITH
- 2.3.6
+ 2.4.19
diff --git a/README.md b/README.md
index b5984a63..f6434051 100644
--- a/README.md
+++ b/README.md
@@ -2,14 +2,12 @@
-# SyntaxTree
+# Syntax Tree
[](https://github.com/ruby-syntax-tree/syntax_tree/actions/workflows/main.yml)
[](https://rubygems.org/gems/syntax_tree)
-Syntax Tree is a suite of tools built on top of the internal CRuby parser. It provides the ability to generate a syntax tree from source, as well as the tools necessary to inspect and manipulate that syntax tree. It can be used to build formatters, linters, language servers, and more.
-
-It is built with only standard library dependencies. It additionally ships with a plugin system so that you can build your own syntax trees from other languages and incorporate these tools.
+Syntax Tree is fast Ruby parser built on top of the [prism](https://github.com/ruby/prism) Ruby parser. It is built with only standard library dependencies.
- [Installation](#installation)
- [CLI](#cli)
@@ -19,11 +17,9 @@ It is built with only standard library dependencies. It additionally ships with
- [Configuration](#configuration)
- [Globbing](#globbing)
- [Language server](#language-server)
- - [textDocument/formatting](#textdocumentformatting)
- [Customization](#customization)
- [Ignoring code](#ignoring-code)
- [Plugins](#plugins)
- - [Languages](#languages)
- [Integration](#integration)
- [Rake](#rake)
- [RuboCop](#rubocop)
@@ -144,16 +140,28 @@ stree write --ignore-files='db/**/*.rb' '**/*.rb'
### Configuration
-Any of the above CLI commands can also read configuration options from a `.streerc` file in the directory where the commands are executed.
+All of the above commands accept additional configuration options. Those are:
+
+- `--print-width=?` - The print width is the suggested line length that should be used when formatting the source. Note that this is not a hard limit like a linter. Instead, it is used as a guideline for how long lines _should_ be. For example, if you have the following code:
+
+```ruby
+foo do
+ bar
+end
+```
+
+In this case, the formatter will see that the block fits into the print width and will rewrite it using the `{}` syntax. This will actually make the line longer than originally written. This is why it is helpful to think of it as a suggestion, rather than a limit.
+- `--preferred-quote=?` - The quote to use for string and character literals. This can be either `"` or `'`. It is "preferred" because in the case that the formatter encounters a string that contains interpolation or certain escape sequences, it will not attempt to change the quote style to avoid accidentally changing the semantic meaning of the code.
+- `--[no-]trailing-comma` - Whether or not to add trailing commas to multiline array literals, hash literals, and method calls that can support trailing commas.
-This should be a text file with each argument on a separate line.
+Any of the above CLI commands can also read configuration options from a `.streerc` file in the directory where the commands are executed. This should be a text file with each argument on a separate line.
```txt
--print-width=100
---plugins=plugin/trailing_comma
+--trailing-comma
```
-If this file is present, it will _always_ be used for CLI commands. You can also pass options from the command line as in the examples above. The options in the `.streerc` file are passed to the CLI first, then the arguments from the command line. In the case of exclusive options (e.g. `--print-width`), this means that the command line options override what's in the config file. In the case of options that can take multiple inputs (e.g. `--plugins`), the effect is additive. That is, the plugins passed from the command line will be loaded _in addition to_ the plugins in the config file.
+If this file is present, it will _always_ be used for CLI commands. The options in the `.streerc` file are passed to the CLI first, then the arguments from the command line. In the case of exclusive options (e.g. `--print-width`), this means that the command line options override what's in the config file. In the case of options that can take multiple inputs (e.g. `--plugins`), the effect is additive. That is, the plugins passed from the command line will be loaded _in addition to_ the plugins in the config file.
### Globbing
@@ -220,32 +228,8 @@ To register plugins, define a file somewhere in your load path named `syntax_tre
* `plugin/single_quotes` - This will change all of your string literals to use single quotes instead of the default double quotes.
* `plugin/trailing_comma` - This will put trailing commas into multiline array literals, hash literals, and method calls that can support trailing commas.
-* `plugin/disable_auto_ternary` - This will prevent the automatic conversion of `if ... else` to ternary expressions.
-
-If you're using Syntax Tree as a library, you can require those files directly or manually pass those options to the formatter initializer through the `SyntaxTree::Formatter::Options` class.
-
-### Languages
-
-To register a new language, call:
-
-```ruby
-SyntaxTree.register_handler(".mylang", MyLanguage)
-```
-
-In this case, whenever the CLI encounters a filepath that ends with the given extension, it will invoke methods on `MyLanguage` instead of `SyntaxTree` itself. To make sure your object conforms to each of the necessary APIs, it should implement:
-
-* `MyLanguage.read(filepath)` - usually this is just an alias to `File.read(filepath)`, but if you need anything else that hook is here.
-* `MyLanguage.parse(source)` - this should return the syntax tree corresponding to the given source. Those objects should implement the `pretty_print` interface.
-* `MyLanguage.format(source)` - this should return the formatted version of the given source.
-Below are listed all of the "official" language plugins hosted under the same GitHub organization, which can be used as references for how to implement other plugins.
-
-* [bf](https://github.com/ruby-syntax-tree/syntax_tree-bf) for the [brainf*** language](https://esolangs.org/wiki/Brainfuck).
-* [css](https://github.com/ruby-syntax-tree/syntax_tree-css) for the [CSS stylesheet language](https://www.w3.org/Style/CSS/).
-* [haml](https://github.com/ruby-syntax-tree/syntax_tree-haml) for the [Haml template language](https://haml.info/).
-* [json](https://github.com/ruby-syntax-tree/syntax_tree-json) for the [JSON notation language](https://www.json.org/).
-* [rbs](https://github.com/ruby-syntax-tree/syntax_tree-rbs) for the [RBS type language](https://github.com/ruby/rbs).
-* [xml](https://github.com/ruby-syntax-tree/syntax_tree-xml) for the [XML markup language](https://www.w3.org/XML/).
+If you're using Syntax Tree as a library, you can require those files directly or manually pass those options to the formatter initializer through the `SyntaxTree::Options` class.
## Integration
@@ -256,12 +240,12 @@ Syntax Tree's goal is to seamlessly integrate into your workflow. To this end, i
Syntax Tree ships with the ability to define [rake](https://github.com/ruby/rake) tasks that will trigger runs of the CLI. To define them in your application, add the following configuration to your `Rakefile`:
```ruby
-require "syntax_tree/rake_tasks"
+require "syntax_tree/rake"
SyntaxTree::Rake::CheckTask.new
SyntaxTree::Rake::WriteTask.new
```
-These calls will define `rake stree:check` and `rake stree:write` (equivalent to calling `stree check` and `stree write` with the CLI respectively). You can configure them by either passing arguments to the `new` method or by using a block.
+These calls will define `rake stree:check` and `rake stree:write` (equivalent to calling `stree check` and `stree write` with the CLI respectively). You can configure them by either passing arguments to the `new` method or by using a block. In addition to the regular configuration options used for the formatter, there are a few additional options specific to the rake tasks.
#### `name`
@@ -292,26 +276,6 @@ SyntaxTree::Rake::WriteTask.new do |t|
end
```
-#### `print_width`
-
-If you want to use a different print width from the default (80), you can pass that to the `print_width` field, as in:
-
-```ruby
-SyntaxTree::Rake::WriteTask.new do |t|
- t.print_width = 100
-end
-```
-
-#### `plugins`
-
-If you're running Syntax Tree with plugins (either your own or the pre-built ones), you can pass that to the `plugins` field, as in:
-
-```ruby
-SyntaxTree::Rake::WriteTask.new do |t|
- t.plugins = ["plugin/single_quotes"]
-end
-```
-
### RuboCop
RuboCop and Syntax Tree serve different purposes, but there is overlap with some of RuboCop's functionality. Syntax Tree provides a RuboCop configuration file to disable rules that are redundant with Syntax Tree. To use this configuration file, add the following snippet to the top of your project's `.rubocop.yml`:
diff --git a/Rakefile b/Rakefile
index 69bb0f1c..f12d6446 100644
--- a/Rakefile
+++ b/Rakefile
@@ -2,7 +2,7 @@
require "bundler/gem_tasks"
require "rake/testtask"
-require "syntax_tree/rake_tasks"
+require "syntax_tree/rake"
Rake::TestTask.new(:test) do |t|
t.libs << "test"
@@ -14,23 +14,7 @@ task default: :test
configure = ->(task) do
task.source_files =
- FileList[
- %w[
- Gemfile
- Rakefile
- syntax_tree.gemspec
- lib/**/*.rb
- tasks/*.rake
- test/*.rb
- ]
- ]
-
- # Since Syntax Tree supports back to Ruby 2.7.0, we need to make sure that we
- # format our code such that it's compatible with that version. This actually
- # has very little effect on the output, the only change at the moment is that
- # Ruby < 2.7.3 didn't allow a newline before the closing brace of a hash
- # pattern.
- task.target_ruby_version = Gem::Version.new("2.7.0")
+ FileList[%w[Gemfile Rakefile syntax_tree.gemspec lib/**/*.rb tasks/*.rake test/*.rb]]
end
SyntaxTree::Rake::CheckTask.new(&configure)
diff --git a/bin/bench b/bin/bench
deleted file mode 100755
index 46f47184..00000000
--- a/bin/bench
+++ /dev/null
@@ -1,42 +0,0 @@
-#!/usr/bin/env ruby
-# frozen_string_literal: true
-
-require "bundler/inline"
-
-gemfile do
- source "https://rubygems.org"
- gem "benchmark-ips"
- gem "parser", require: "parser/current"
- gem "ruby_parser"
-end
-
-$:.unshift(File.expand_path("../lib", __dir__))
-require "syntax_tree"
-
-def compare(filepath)
- prefix = "#{File.expand_path("..", __dir__)}/"
- puts "=== #{filepath.delete_prefix(prefix)} ==="
-
- source = File.read(filepath)
-
- Benchmark.ips do |x|
- 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
-
-filepaths = ARGV
-
-# If the user didn't supply any files to parse to benchmark, then we're going to
-# default to parsing this file and the main syntax_tree file (a small and large
-# file).
-if filepaths.empty?
- filepaths = [
- File.expand_path("bench", __dir__),
- File.expand_path("../lib/syntax_tree/node.rb", __dir__)
- ]
-end
-
-filepaths.each { |filepath| compare(filepath) }
diff --git a/bin/profile b/bin/profile
index 15bd28ae..34cba40f 100755
--- a/bin/profile
+++ b/bin/profile
@@ -6,7 +6,7 @@ require "bundler/inline"
gemfile do
source "https://rubygems.org"
gem "stackprof"
- gem "prettier_print"
+ gem "prism"
end
$:.unshift(File.expand_path("../lib", __dir__))
@@ -14,7 +14,7 @@ require "syntax_tree"
StackProf.run(mode: :cpu, out: "tmp/profile.dump", raw: true) do
Dir[File.join(RbConfig::CONFIG["libdir"], "**/*.rb")].each do |filepath|
- SyntaxTree.format(SyntaxTree.read(filepath))
+ SyntaxTree.format_file(filepath)
end
end
diff --git a/exe/stree b/exe/stree
index 3bae88e9..71fbe6ac 100755
--- a/exe/stree
+++ b/exe/stree
@@ -4,6 +4,5 @@
$:.unshift(File.expand_path("../lib", __dir__))
require "syntax_tree"
-require "syntax_tree/cli"
exit(SyntaxTree::CLI.run(ARGV))
diff --git a/lib/syntax_tree.rb b/lib/syntax_tree.rb
index c5458c90..e5b3e3b1 100644
--- a/lib/syntax_tree.rb
+++ b/lib/syntax_tree.rb
@@ -1,104 +1,141 @@
# frozen_string_literal: true
-require "prettier_print"
-require "ripper"
-
-require_relative "syntax_tree/formatter"
-require_relative "syntax_tree/node"
-require_relative "syntax_tree/parser"
-require_relative "syntax_tree/version"
-
-# Syntax Tree is a suite of tools built on top of the internal CRuby parser. It
-# provides the ability to generate a syntax tree from source, as well as the
-# tools necessary to inspect and manipulate that syntax tree. It can be used to
-# build formatters, linters, language servers, and more.
+require "thread"
+require "syntax_tree/format"
+
+# Syntax Tree is a formatter built on top of the internal CRuby parser.
module SyntaxTree
- autoload :LanguageServer, "syntax_tree/language_server"
-
- # 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 the default print width when formatting. It can be overridden in the
- # CLI by passing the --print-width option or here in the API by passing the
- # optional second argument to ::format.
- DEFAULT_PRINT_WIDTH = 80
-
- # This is the default ruby version that we're going to target for formatting.
- # It shouldn't really be changed except in very niche circumstances.
- DEFAULT_RUBY_VERSION = Formatter::SemanticVersion.new(RUBY_VERSION).freeze
-
- # The default indentation level for formatting. We allow changing this so
- # that Syntax Tree can format arbitrary parts of a document.
- DEFAULT_INDENTATION = 0
-
- # Parses the given source and returns the formatted source.
- def self.format(
- source,
- maxwidth = DEFAULT_PRINT_WIDTH,
- base_indentation = DEFAULT_INDENTATION,
- options: Formatter::Options.new
- )
- format_node(
- source,
- parse(source),
- maxwidth,
- base_indentation,
- options: options
- )
+ autoload :CLI, "syntax_tree/cli"
+ autoload :LSP, "syntax_tree/lsp"
+ autoload :Rake, "syntax_tree/rake"
+ autoload :Version, "syntax_tree/version"
+
+ # Raised when an error is encountered while parsing the source to be
+ # formatted through the #format or #format_file methods.
+ class ParseError < StandardError
end
- # Parses the given file and returns the formatted source.
- def self.format_file(
- filepath,
- maxwidth = DEFAULT_PRINT_WIDTH,
- base_indentation = DEFAULT_INDENTATION,
- options: Formatter::Options.new
- )
- format(read(filepath), maxwidth, base_indentation, options: options)
+ # We want to minimize as much as possible the number of options that are
+ # available in the formatter. For the most part, if users want non-default
+ # formatting, they should override the visit methods below. However, because
+ # of some history with prettier and the fact that folks have become entrenched
+ # in their ways, we decided to provide a small amount of configurability.
+ class Options
+ # The print width is the suggested line length that should be used when
+ # formatting the source. Note that this is not a hard limit like a linter.
+ # Instead, it is used as a guideline for how long lines _should_ be. For
+ # example, if you have the following code:
+ #
+ # foo do
+ # bar
+ # end
+ #
+ # In this case, the formatter will see that the block fits into the print
+ # width and will rewrite it using the `{}` syntax. This will actually make
+ # the line longer than originally written. This is why it is helpful to
+ # think of it as a suggestion, rather than a limit.
+ attr_accessor :print_width
+
+ # The quote style to use when formatting string literals. This can be
+ # either a single quote (`'`) or a double quote (`"`). This is a
+ # preference, but not a hard rule. If a string contains interpolation,
+ # the formatter will leave it as it is in the source to avoid changing the
+ # meaning of the code.
+ attr_accessor :preferred_quote
+
+ # Trailing commas can be used in multi-line collection literals and when
+ # specifying arguments to a method call, in most cases (there are a few
+ # rare exceptions). This option controls whether or not they should be
+ # used.
+ attr_accessor :trailing_comma
+
+ def initialize(print_width: 100, preferred_quote: '"', trailing_comma: false)
+ @print_width = print_width
+ @preferred_quote = preferred_quote
+ @trailing_comma = trailing_comma
+ end
end
- # Accepts a node in the tree and returns the formatted source.
- def self.format_node(
- source,
- node,
- maxwidth = DEFAULT_PRINT_WIDTH,
- base_indentation = DEFAULT_INDENTATION,
- options: Formatter::Options.new
- )
- formatter = Formatter.new(source, [], maxwidth, options: options)
- node.format(formatter)
-
- formatter.flush(base_indentation)
- formatter.output.join
+ # Mutex to synchronize modifications to the module configuration.
+ @lock = Mutex.new
+
+ # The default formatting options used by the formatter when an options object
+ # is not explicitly provided.
+ @options = Options.new.freeze
+
+ # Configure the default formatting options that will be used when options are
+ # not explicitly provided.
+ def self.configure
+ @lock.synchronize do
+ options = @options.dup
+ yield options
+ @options = options.freeze
+ end
end
- # Parses the given source and returns the syntax tree.
- def self.parse(source)
- parser = Parser.new(source)
- response = parser.parse
- response unless parser.error?
+ # Create a new set of options that falls back to the default options for any
+ # unspecified values.
+ def self.options(print_width: :default, preferred_quote: :default, trailing_comma: :default)
+ options = @lock.synchronize { @options.dup }
+ options.print_width = print_width unless print_width == :default
+ options.preferred_quote = preferred_quote unless preferred_quote == :default
+ options.trailing_comma = trailing_comma unless trailing_comma == :default
+ options.freeze
end
- # Returns the source from the given filepath taking into account any potential
- # magic encoding comments.
- def self.read(filepath)
- encoding =
- File.open(filepath, "r") do |file|
- break Encoding.default_external if file.eof?
-
- header = file.readline
- header += file.readline if !file.eof? && header.start_with?("#!")
- Ripper.new(header).tap(&:parse).encoding
- end
+ # Options should not be directly used by consumers. Instead they should create
+ # new options object through the SyntaxTree.options method.
+ private_constant :Options
- File.read(filepath, encoding: encoding)
- end
+ # It is possible to extend the formatter to support other languages by
+ # registering extension. An extension is any object that responds to both
+ # the #format and #format_file methods.
+ @handlers = Hash.new(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
+ @lock.synchronize { @handlers[extension] = handler }
+ end
+
+ # Unregisters the handler for the given file extension.
+ def self.unregister_handler(extension)
+ @lock.synchronize { @handlers.delete(extension) }
+ end
+
+ # Retrieves the handler registered for the given file extension.
+ def self.handler_for(extension)
+ @lock.synchronize { @handlers[extension] }
+ end
+
+ class << self
+ # Parses the given source and returns the formatted source.
+ def format(source, options = @options)
+ process(Prism.parse(source), options)
+ end
+
+ # Parses the given file and returns the formatted source.
+ def format_file(filepath, options = @options)
+ process(Prism.parse_file(filepath), options)
+ end
+
+ private
+
+ # Processes the result of parsing the source and returns the formatted
+ # source.
+ def process(result, options)
+ raise ParseError, result.errors_format if result.failure?
+ result.attach_comments!
+
+ formatter = Prism::Format.new(result.source.source, options)
+ result.value.accept(formatter)
+
+ if (data_loc = result.data_loc)
+ formatted = formatter.format
+ formatted.empty? ? data_loc.slice : "#{formatted}\n\n#{data_loc.slice}"
+ else
+ "#{formatter.format}\n"
+ end
+ end
end
end
diff --git a/lib/syntax_tree/cli.rb b/lib/syntax_tree/cli.rb
index 6437fd3b..a7f2b3e0 100644
--- a/lib/syntax_tree/cli.rb
+++ b/lib/syntax_tree/cli.rb
@@ -28,10 +28,6 @@ def self.gray(value)
new(value, "38;5;102")
end
- def self.red(value)
- new(value, "1;31")
- end
-
def self.yellow(value)
new(value, "33")
end
@@ -39,18 +35,18 @@ def self.yellow(value)
# An item of work that corresponds to a file to be processed.
class FileItem
- attr_reader :filepath
-
def initialize(filepath)
@filepath = filepath
end
def handler
- HANDLERS[File.extname(filepath)]
+ SyntaxTree.handler_for(File.extname(filepath))
end
+ attr_reader :filepath
+
def source
- handler.read(filepath)
+ File.read(filepath)
end
def writable?
@@ -61,21 +57,21 @@ def writable?
# An item of work that corresponds to a script content passed via the
# command line.
class ScriptItem
- attr_reader :source
-
def initialize(source, extension)
@source = source
@extension = extension
end
def handler
- HANDLERS[@extension]
+ SyntaxTree.handler_for(@extension)
end
def filepath
:script
end
+ attr_reader :source
+
def writable?
false
end
@@ -88,7 +84,7 @@ def initialize(extension)
end
def handler
- HANDLERS[@extension]
+ SyntaxTree.handler_for(@extension)
end
def filepath
@@ -122,13 +118,6 @@ def failure
end
end
- # An action of the CLI that prints out the AST for the given source.
- class AST < Action
- def run(item)
- pp item.handler.parse(item.source)
- end
- end
-
# An action of the CLI that ensures that the filepath is formatted as
# expected.
class Check < Action
@@ -137,13 +126,7 @@ class UnformattedError < StandardError
def run(item)
source = item.source
- formatted =
- item.handler.format(
- source,
- options.print_width,
- options: options.formatter_options
- )
-
+ formatted = item.handler.format(source, options)
raise UnformattedError if source != formatted
rescue StandardError
warn("[#{Color.yellow("warn")}] #{item.filepath}")
@@ -167,25 +150,11 @@ class NonIdempotentFormatError < StandardError
def run(item)
handler = item.handler
- warning = "[#{Color.yellow("warn")}] #{item.filepath}"
-
- formatted =
- handler.format(
- item.source,
- options.print_width,
- options: options.formatter_options
- )
-
- double_formatted =
- handler.format(
- formatted,
- options.print_width,
- options: options.formatter_options
- )
-
+ formatted = handler.format(item.source, options)
+ double_formatted = handler.format(formatted, options)
raise NonIdempotentFormatError if formatted != double_formatted
rescue StandardError
- warn(warning)
+ warn("[#{Color.yellow("warn")}] #{item.filepath}")
raise
end
@@ -198,30 +167,10 @@ def failure
end
end
- # An action of the CLI that prints out the doc tree IR for the given source.
- class Doc < Action
- def run(item)
- source = item.source
-
- formatter_options = options.formatter_options
- formatter = Formatter.new(source, [], options: formatter_options)
-
- item.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(item)
- formatted =
- item.handler.format(
- item.source,
- options.print_width,
- options: options.formatter_options
- )
-
- puts formatted
+ puts item.handler.format(item.source, options)
end
end
@@ -233,12 +182,7 @@ def run(item)
start = Time.now
source = item.source
- formatted =
- item.handler.format(
- source,
- options.print_width,
- options: options.formatter_options
- )
+ formatted = item.handler.format(source, options)
changed = source != formatted
File.write(filepath, formatted) if item.writable? && changed
@@ -256,33 +200,32 @@ def run(item)
# The help message displayed if the input arguments are not correctly
# ordered or formatted.
HELP = <<~HELP
- #{Color.bold("stree ast [--plugins=...] [--print-width=NUMBER] [-e SCRIPT] FILE")}
- Print out the AST corresponding to the given files
-
- #{Color.bold("stree check [--plugins=...] [--print-width=NUMBER] [-e SCRIPT] FILE")}
+ #{Color.bold("stree check OPTIONS? SOURCE")}
Check that the given files are formatted as syntax tree would format them
- #{Color.bold("stree debug [--plugins=...] [--print-width=NUMBER] [-e SCRIPT] FILE")}
+ #{Color.bold("stree debug OPTIONS? SOURCE")}
Check that the given files can be formatted idempotently
- #{Color.bold("stree doc [--plugins=...] [-e SCRIPT] FILE")}
- Print out the doc tree that would be used to format the given files
-
- #{Color.bold("stree format [--plugins=...] [--print-width=NUMBER] [-e SCRIPT] FILE")}
+ #{Color.bold("stree format OPTIONS? SOURCE")}
Print out the formatted version of the given files
#{Color.bold("stree help")}
Display this help message
- #{Color.bold("stree lsp [--plugins=...] [--print-width=NUMBER]")}
+ #{Color.bold("stree lsp OPTIONS?")}
Run syntax tree in language server mode
#{Color.bold("stree version")}
Output the current version of syntax tree
- #{Color.bold("stree write [--plugins=...] [--print-width=NUMBER] [-e SCRIPT] FILE")}
+ #{Color.bold("stree write OPTIONS? SOURCE")}
Read, format, and write back the source of the given files
+ OPTIONS:
+
+ --config=...
+ Path to a configuration file. Defaults to ./.streerc.
+
--ignore-files=...
A glob pattern to ignore files when processing. This can be specified
multiple times to ignore multiple patterns.
@@ -290,50 +233,58 @@ def run(item)
--plugins=...
A comma-separated list of plugins to load.
+ --extension=...
+ A file extension matching the content passed in via STDIN or -e.
+ Defaults to '.rb'.
+
--print-width=...
- The maximum line width to use when formatting.
+ The print width to use when formatting.
+
+ --preferred-quote=...
+ The preferred quote style to use when formatting. Valid styles are
+ single, double, ', and ".
+
+ --[no-]trailing-comma
+ Whether or not to add trailing commas to multi-line collections and
+ method calls.
+
+ SOURCE:
-e ...
Parse an inline string.
- --extension=...
- A file extension matching the content passed in via STDIN or -e.
- Defaults to '.rb'.
-
- --config=...
- Path to a configuration file. Defaults to .streerc in the current
- working directory.
+ path
+ One or more file paths or glob patterns to process.
HELP
# This represents all of the options that can be passed to the CLI. It is
# responsible for parsing the list and then returning the file paths at the
# end.
class Options
- attr_reader :ignore_files,
- :plugins,
- :print_width,
- :scripts,
- :extension,
- :target_ruby_version
-
- def initialize
+ attr_reader :ignore_files, :plugins, :scripts, :extension
+ attr_reader :print_width, :preferred_quote, :trailing_comma
+
+ def initialize(arguments)
@ignore_files = []
@plugins = []
- @print_width = DEFAULT_PRINT_WIDTH
@scripts = []
@extension = ".rb"
- @target_ruby_version = DEFAULT_RUBY_VERSION
- end
- def formatter_options
- @formatter_options ||=
- Formatter::Options.new(target_ruby_version: target_ruby_version)
- end
+ @print_width = :default
+ @preferred_quote = :default
+ @trailing_comma = :default
- def parse(arguments)
parser.parse!(arguments)
end
+ def options
+ SyntaxTree.options(
+ print_width: print_width,
+ preferred_quote: preferred_quote,
+ trailing_comma: trailing_comma
+ )
+ end
+
private
def parser
@@ -360,12 +311,6 @@ def parser
@plugins.each { |plugin| require "syntax_tree/#{plugin}" }
end
- # If there is a print width specified on the command line, then
- # parse that out here and use it when formatting.
- opts.on("--print-width=NUMBER", Integer) do |print_width|
- @print_width = print_width
- end
-
# If there is a script specified on the command line, then parse
# it and add it to the list of scripts to run.
opts.on("-e SCRIPT") { |script| @scripts << script }
@@ -377,11 +322,27 @@ def parser
@extension = ".#{extension.delete_prefix(".")}"
end
- # If there is a target ruby version specified on the command line,
- # parse that out and use it when formatting.
- opts.on("--target-ruby-version=VERSION") do |version|
- @target_ruby_version = Formatter::SemanticVersion.new(version)
+ # If there is a print width specified on the command line, then
+ # parse that out here and use it when formatting.
+ opts.on("--print-width=NUMBER", Integer) { |print_width| @print_width = print_width }
+
+ # If there is a preferred quote style specified on the command line,
+ # then parse that out here and use it when formatting.
+ opts.on("--preferred-quote=STYLE") do |preferred_quote|
+ @preferred_quote =
+ case preferred_quote
+ when "single", "'"
+ "'"
+ when "double", '"'
+ '"'
+ else
+ raise ArgumentError, "Invalid preferred quote style: #{preferred_quote}"
+ end
end
+
+ # If there is a trailing comma style specified on the command line,
+ # then parse that out here and use it when formatting.
+ opts.on("--[no-]trailing-comma") { |trailing_comma| @trailing_comma = trailing_comma }
end
end
end
@@ -390,8 +351,8 @@ def parser
# arguments to the CLI. Each line of the config file should be a new
# argument, as in:
#
- # --plugins=plugin/single_quote
# --print-width=100
+ # --trailing-comma
#
# When invoking the CLI, we will read this config file and then parse it if
# it exists in the current working directory.
@@ -446,42 +407,33 @@ def run(argv)
config_file = ConfigFile.new(config_filepath)
arguments = config_file.arguments.concat(arguments)
- options = Options.new
- options.parse(arguments)
-
+ options = Options.new(arguments)
action =
case name
- when "a", "ast"
- AST.new(options)
when "c", "check"
- Check.new(options)
+ Check.new(options.options)
when "debug"
- Debug.new(options)
- when "doc"
- Doc.new(options)
+ Debug.new(options.options)
when "f", "format"
- Format.new(options)
+ Format.new(options.options)
when "help"
puts HELP
return 0
when "lsp"
- LanguageServer.new(
- print_width: options.print_width,
- ignore_files: options.ignore_files
- ).run
+ LSP.new(options: options.options, ignore_files: options.ignore_files).run
return 0
when "version"
- puts SyntaxTree::VERSION
+ puts VERSION
return 0
when "w", "write"
- Write.new(options)
+ Write.new(options.options)
else
warn(HELP)
return 1
end
# We're going to build up a queue of items to process.
- queue = Queue.new
+ queue = []
# If there are any arguments or scripts, then we'll add those to the
# queue. Otherwise we'll read the content off STDIN.
@@ -501,16 +453,14 @@ def run(argv)
end
end
- options.scripts.each do |script|
- queue << ScriptItem.new(script, options.extension)
- end
+ options.scripts.each { |script| queue << ScriptItem.new(script, options.extension) }
else
queue << STDINItem.new(options.extension)
end
- # At the end, we're going to return whether or not this worker ever
+ # At the end, we're going to return whether or not this CLI ever
# encountered an error.
- if process_queue(queue, action)
+ if process_queue(action, queue)
action.failure
1
else
@@ -521,93 +471,123 @@ def run(argv)
private
- # Processes each item in the queue with the given action. Returns whether
- # or not any errors were encountered.
- def process_queue(queue, action)
- workers =
- [Etc.nprocessors, queue.size].min.times.map do
- Thread.new do
- # Propagate errors in the worker threads up to the parent thread.
- Thread.current.abort_on_exception = true
-
- # Track whether or not there are any errors from any of the files
- # that we take action on so that we can properly clean up and
- # exit.
- errored = false
-
- # While there is still work left to do, shift off the queue and
- # process the item.
- until queue.empty?
- item = queue.shift
- errored |=
- begin
- action.run(item)
- false
- rescue Parser::ParseError => error
- warn("Error: #{error.message}")
- highlight_error(error, item.source)
- true
- rescue Check::UnformattedError,
- Debug::NonIdempotentFormatError
- true
- rescue StandardError => error
- warn(error.message)
- warn(error.backtrace)
- true
- end
+ def process_item(item, action)
+ action.run(item)
+ false
+ rescue ParseError => error
+ warn("syntax error:\n#{error.message}")
+ true
+ rescue Check::UnformattedError, Debug::NonIdempotentFormatError
+ true
+ rescue StandardError => error
+ warn(error.message)
+ warn(error.backtrace)
+ true
+ end
+
+ if Process.respond_to?(:fork)
+ def process_queue(action, queue)
+ queue.freeze
+ nworkers = [Etc.nprocessors - 1, queue.size].min
+
+ requests = Array.new(nworkers) { IO.pipe }
+ responses = Array.new(nworkers) { IO.pipe }
+
+ pids =
+ nworkers.times.map do |nworker|
+ # In each child process, we will continuously write to the request
+ # pipe when we are available for work. The parent process will
+ # then write back on the response pipe either an index to work on
+ # or a -1 to indicate that there is no more work to be done. At
+ # the end of the loop, we will write the status of our work back
+ # to the parent process to indicate if there were any errors.
+ fork do
+ requests.each_with_index do |(reader, writer), npipe|
+ reader.close
+ writer.close if npipe != nworker
+ end
+
+ responses.each_with_index do |(reader, writer), npipe|
+ writer.close
+ reader.close if npipe != nworker
+ end
+
+ request_writer = requests[nworker][1]
+ response_reader = responses[nworker][0]
+ errored = false
+
+ loop do
+ request_writer.puts(0)
+ break if (response = Integer(response_reader.gets)) == -1
+ errored |= process_item(queue[response], action)
+ end
+
+ request_writer.puts(errored ? -1 : 1)
+ request_writer.close
+ response_reader.close
end
-
- # At the end, we're going to return whether or not this worker
- # ever encountered an error.
- errored
end
- end
-
- workers.map(&:value).inject(:|)
- end
-
- # Highlights a snippet from a source and parse error.
- def highlight_error(error, source)
- lines = source.lines
- maximum = [error.lineno + 3, lines.length].min
- digits = Math.log10(maximum).ceil
+ requests.each { |(_, writer)| writer.close }
+ responses.each { |(reader, _)| reader.close }
+
+ indices = queue.each_index
+ errored = false
+
+ request_readers = requests.map(&:first)
+ response_writers =
+ requests.zip(responses).to_h { |(reader, _), (_, writer)| [reader, writer] }
+
+ # The parent process will continuously listen for requests from the
+ # child processes and respond accordingly. When the child processes
+ # write that they are ready for work, we will send them the next index
+ # to work on. When they write that they are done, we will remove them
+ # from the list of active workers.
+ until request_readers.empty?
+ IO
+ .select(request_readers)[0]
+ .each do |request_reader|
+ case Integer(request_reader.gets)
+ when 0
+ response_writer = response_writers[request_reader]
- ([error.lineno - 3, 0].max...maximum).each do |line_index|
- line_number = line_index + 1
-
- if line_number == error.lineno
- part1 = Color.red(">")
- part2 = Color.gray("%#{digits}d |" % line_number)
- warn("#{part1} #{part2} #{colorize_line(lines[line_index])}")
-
- part3 = Color.gray(" %#{digits}s |" % " ")
- warn("#{part3} #{" " * error.column}#{Color.red("^")}")
- else
- prefix = Color.gray(" %#{digits}d |" % line_number)
- warn("#{prefix} #{colorize_line(lines[line_index])}")
+ begin
+ response_writer.puts(indices.next)
+ rescue StopIteration
+ response_writer.puts(-1)
+ end
+ when -1
+ errored = true
+ request_readers.delete(request_reader)
+ when 1
+ request_readers.delete(request_reader)
+ end
+ end
end
- end
- end
-
- # Take a line of Ruby source and colorize the output.
- def colorize_line(line)
- require "irb"
- IRB::Color.colorize_code(line, **colorize_options)
- end
- # These are the options we're going to pass into IRB::Color.colorize_code.
- # Since we support multiple versions of IRB, we're going to need to do
- # some reflection to make sure we always pass valid options.
- def colorize_options
- options = { complete: false }
-
- parameters = IRB::Color.method(:colorize_code).parameters
- if parameters.any? { |(_type, name)| name == :ignore_error }
- options[:ignore_error] = true
+ pids.each { |pid| Process.waitpid(pid) }
+ errored
end
+ else
+ def process_queue(action, queue)
+ queue = Queue.new(queue).tap(&:close)
+ workers = [Etc.nprocessors, queue.size].min
+ .times
+ .map do
+ Thread.new do
+ Thread.current.abort_on_exception = true
+ errored = false
+
+ while (item = queue.shift)
+ errored |= process_item(item, action)
+ end
+
+ errored
+ end
+ end
- options
+ workers.map(&:value).any?
+ end
end
end
end
diff --git a/lib/syntax_tree/format.rb b/lib/syntax_tree/format.rb
new file mode 100644
index 00000000..6a527946
--- /dev/null
+++ b/lib/syntax_tree/format.rb
@@ -0,0 +1,4750 @@
+# frozen_string_literal: true
+
+require "prism"
+
+module Prism
+ class Node
+ def heredoc_end_line
+ @heredoc_end_line ||= [end_line, *compact_child_nodes.map(&:heredoc_end_line)].max
+ end
+ end
+
+ class StringNode < Node
+ def heredoc_end_line
+ @heredoc_end_line ||= (heredoc? ? closing_loc.start_line : end_line)
+ end
+ end
+
+ class XStringNode < Node
+ def heredoc_end_line
+ @heredoc_end_line ||= (heredoc? ? closing_loc.start_line : end_line)
+ end
+ end
+
+ class InterpolatedStringNode < Node
+ def heredoc_end_line
+ @heredoc_end_line ||= [
+ heredoc? ? closing_loc.start_line : end_line,
+ *compact_child_nodes.map(&:heredoc_end_line)
+ ].max
+ end
+ end
+
+ class InterpolatedXStringNode < Node
+ def heredoc_end_line
+ @heredoc_end_line ||= [
+ heredoc? ? closing_loc.start_line : end_line,
+ *compact_child_nodes.map(&:heredoc_end_line)
+ ].max
+ end
+ end
+
+ # Philip Wadler, A prettier printer, March 1998
+ # https://homepages.inf.ed.ac.uk/wadler/papers/prettier/prettier.pdf
+ class Format
+ # A node in the print tree that represents aligning nested nodes to a
+ # certain prefix width or string.
+ class Align
+ attr_reader :indent, :contents
+
+ def initialize(indent, contents)
+ @indent = indent
+ @contents = contents
+ end
+
+ def type
+ :align
+ end
+ end
+
+ # A node in the print tree that represents a place in the buffer that the
+ # content can be broken onto multiple lines.
+ class Breakable
+ attr_reader :separator, :width, :force, :indent
+
+ def initialize(separator, width, force, indent)
+ @separator = separator
+ @width = width
+ @force = force
+ @indent = indent
+ end
+
+ def type
+ :breakable
+ end
+ end
+
+ # Below here are the most common combination of options that are created
+ # when creating new breakables. They are here to cut down on some
+ # allocations.
+ BREAKABLE_SPACE = Breakable.new(" ", 1, false, true).freeze
+ BREAKABLE_EMPTY = Breakable.new("", 0, false, true).freeze
+ BREAKABLE_FORCE = Breakable.new(" ", 1, true, true).freeze
+ BREAKABLE_RETURN = Breakable.new(" ", 1, true, false).freeze
+
+ # A node in the print tree that forces the surrounding group to print out in
+ # the "break" mode as opposed to the "flat" mode. Useful for when you need
+ # to force a newline into a group.
+ class BreakParent
+ def type
+ :break_parent
+ end
+ end
+
+ # Since there's really no difference in these instances, just using the same
+ # one saves on some allocations.
+ BREAK_PARENT = BreakParent.new.freeze
+
+ # A node in the print tree that represents a group of items which the printer
+ # should try to fit onto one line. This is the basic command to tell the
+ # printer when to break. Groups are usually nested, and the printer will try
+ # to fit everything on one line, but if it doesn't fit it will break the
+ # outermost group first and try again. It will continue breaking groups until
+ # everything fits (or there are no more groups to break).
+ class Group
+ attr_reader :contents, :break
+
+ def initialize(contents)
+ @contents = contents
+ @break = false
+ end
+
+ def unbreak!
+ @break = false
+ end
+
+ def break!
+ @break = true
+ end
+
+ def type
+ :group
+ end
+ end
+
+ # A node in the print tree that represents printing one thing if the
+ # surrounding group node is broken and another thing if the surrounding group
+ # node is flat.
+ class IfBreak
+ attr_reader :break_contents, :flat_contents
+
+ def initialize(break_contents, flat_contents)
+ @break_contents = break_contents
+ @flat_contents = flat_contents
+ end
+
+ def type
+ :if_break
+ end
+ end
+
+ # A node in the print tree that is a variant of the Align node that indents
+ # its contents by one level.
+ class Indent
+ attr_reader :contents
+
+ def initialize(contents)
+ @contents = contents
+ end
+
+ def type
+ :indent
+ end
+ end
+
+ # A node in the print tree that has its own special buffer for implementing
+ # content that should flush before any newline.
+ #
+ # Useful for implementating trailing content, as it's not always practical to
+ # constantly check where the line ends to avoid accidentally printing some
+ # content after a line suffix node.
+ class LineSuffix
+ attr_reader :priority, :contents
+
+ def initialize(priority, contents)
+ @priority = priority
+ @contents = contents
+ end
+
+ def type
+ :line_suffix
+ end
+ end
+
+ # A node in the print tree that represents trimming all of the indentation of
+ # the current line, in the rare case that you need to ignore the indentation
+ # that you've already created. This node should be placed after a Breakable.
+ class Trim
+ def type
+ :trim
+ end
+ end
+
+ # Since all of the instances here are the same, we can reuse the same one to
+ # cut down on allocations.
+ TRIM = Trim.new.freeze
+
+ # Refine string so that we can consistently call #type in case statements and
+ # treat all of the nodes in the tree as homogenous.
+ using Module.new {
+ refine String do
+ def type
+ :string
+ end
+ end
+ }
+
+ # There are two modes in printing, break and flat. When we're in break mode,
+ # any lines will use their newline, any if-breaks will use their break
+ # contents, etc.
+ MODE_BREAK = 1
+
+ # This is another print mode much like MODE_BREAK. When we're in flat mode, we
+ # attempt to print everything on one line until we either hit a broken group,
+ # a forced line, or the maximum width.
+ MODE_FLAT = 2
+
+ # The default indentation for printing is zero, assuming that the code starts
+ # at the top level. That can be changed if desired to start from a different
+ # indentation level.
+ DEFAULT_INDENTATION = 0
+
+ COMMENT_PRIORITY = 1
+ HEREDOC_PRIORITY = 2
+
+ attr_reader :source, :stack
+ attr_reader :print_width, :preferred_quote, :trailing_comma
+
+ # This is an output buffer that contains the various parts of the printed
+ # source code.
+ attr_reader :buffer
+
+ # The stack of groups that are being printed.
+ attr_reader :groups
+
+ # The current array of contents that calls to methods that generate print
+ # tree nodes will append to.
+ attr_reader :target
+
+ def initialize(source, options)
+ @source = source
+ @stack = []
+ @buffer = []
+
+ @print_width = options.print_width
+ @preferred_quote = options.preferred_quote
+ @trailing_comma = options.trailing_comma
+
+ contents = []
+ @groups = [Group.new(contents)]
+ @target = contents
+ end
+
+ # The main API for this visitor.
+ def format
+ flush
+ buffer.join
+ end
+
+ # A convenience method used by a lot of the print tree node builders that
+ # temporarily changes the target that the builders will append to.
+ def with_target(target)
+ previous_target, @target = @target, target
+ yield
+ @target = previous_target
+ end
+
+ # --------------------------------------------------------------------------
+ # Visit methods
+ # --------------------------------------------------------------------------
+
+ # alias $foo $bar
+ # ^^^^^^^^^^^^^^^
+ def visit_alias_global_variable_node(node)
+ group do
+ text("alias ")
+ visit(node.new_name)
+ align(6) do
+ breakable_space
+ visit(node.old_name)
+ end
+ end
+ end
+
+ # alias foo bar
+ # ^^^^^^^^^^^^^
+ def visit_alias_method_node(node)
+ new_name = node.new_name
+ old_name = node.old_name
+
+ group do
+ text("alias ")
+
+ if new_name.is_a?(SymbolNode)
+ text(new_name.value)
+ new_name.location.comments.each { |comment| visit_comment(comment) }
+ else
+ visit(new_name)
+ end
+
+ align(6) do
+ breakable_space
+ if old_name.is_a?(SymbolNode)
+ text(old_name.value)
+ else
+ visit(old_name)
+ end
+ end
+ end
+ end
+
+ # foo => bar | baz
+ # ^^^^^^^^^
+ def visit_alternation_pattern_node(node)
+ group do
+ visit(node.left)
+ text(" ")
+ visit_location(node.operator_loc)
+ breakable_space
+ visit(node.right)
+ end
+ end
+
+ # a and b
+ # ^^^^^^^
+ def visit_and_node(node)
+ visit_binary(node.left, node.operator_loc, node.right)
+ end
+
+ # []
+ # ^^
+ def visit_array_node(node)
+ opening_loc = node.opening_loc
+ elements = node.elements
+
+ # If we have no opening location, then this is an implicit array by virtue
+ # of an assignment operator. In this case we will print out the elements
+ # of the array separated by commas and newlines.
+ if opening_loc.nil?
+ group { seplist(elements) { |element| visit(element) } }
+ return
+ end
+
+ # If this is a specially formatted array, we will leave it be and format
+ # it according to how the source has it formatted.
+ opening = opening_loc.slice
+ if opening.start_with?("%")
+ group do
+ text(opening)
+ indent do
+ breakable_empty
+ seplist(elements, -> { breakable_space }) { |element| visit(element) }
+ end
+ breakable_empty
+ text(node.closing)
+ end
+ return
+ end
+
+ # If this array has no comments on the start of the end location and it
+ # has more than 2 elements, we'll check if we can automatically convert it
+ # into a %w or %i array.
+ closing_loc = node.closing_loc
+ if opening_loc.comments.empty? && closing_loc.comments.empty? && elements.length >= 2
+ if elements.all? { |element|
+ element.is_a?(StringNode) && element.location.comments.empty? &&
+ !(content = element.content).empty? &&
+ !content.match?(/[\s\[\]\\]/)
+ }
+ group do
+ text("%w[")
+ indent do
+ breakable_empty
+ seplist(elements, -> { breakable_space }) { |element| text(element.content) }
+ end
+ breakable_empty
+ text("]")
+ end
+ return
+ elsif elements.all? { |element|
+ element.is_a?(SymbolNode) && element.location.comments.empty?
+ }
+ group do
+ text("%i[")
+ indent do
+ breakable_empty
+ seplist(elements, -> { breakable_space }) { |element| text(element.value) }
+ end
+ breakable_empty
+ text("]")
+ end
+ return
+ end
+ end
+
+ # Otherwise we'll format the array normally.
+ group do
+ visit_location(opening_loc)
+ visit_elements(elements, closing_loc.comments)
+ breakable_empty
+ text("]")
+ end
+ end
+
+ # foo => [bar]
+ # ^^^^^
+ def visit_array_pattern_node(node)
+ constant = node.constant
+ opening_loc = node.opening_loc
+
+ targets = [*node.requireds, *node.rest, *node.posts]
+ implicit_rest = targets.pop if targets.last.is_a?(ImplicitRestNode)
+
+ group do
+ visit(constant) unless constant.nil?
+ text("[")
+ opening_loc.comments.each { |comment| visit_comment(comment) } unless opening_loc.nil?
+ visit_elements(targets, node.closing_loc&.comments || [])
+ visit(implicit_rest) if implicit_rest
+ breakable_empty
+ text("]")
+ end
+ end
+
+ # foo(bar)
+ # ^^^
+ def visit_arguments_node(node)
+ seplist(node.arguments) { |argument| visit(argument) }
+ end
+
+ # { a: 1 }
+ # ^^^^
+ def visit_assoc_node(node)
+ if node.value.is_a?(HashNode)
+ visit_assoc_node_inner(node)
+ else
+ group { visit_assoc_node_inner(node) }
+ end
+ end
+
+ # Visit an assoc node and format the key and value.
+ private def visit_assoc_node_inner(node)
+ operator_loc = node.operator_loc
+ value = node.value
+
+ if operator_loc.nil?
+ visit(node.key)
+ visit_assoc_node_value(value) if !value.nil? && !value.is_a?(ImplicitNode)
+ else
+ visit(node.key)
+ text(" ")
+ visit_location(operator_loc)
+ visit_assoc_node_value(value)
+ end
+ end
+
+ # Visit the value of an association node.
+ private def visit_assoc_node_value(node)
+ if indent_write?(node)
+ indent do
+ breakable_space
+ visit(node)
+ end
+ else
+ text(" ")
+ visit(node)
+ end
+ end
+
+ # Visit an assoc node and format the key as a label.
+ private def visit_assoc_node_label(node)
+ if node.operator_loc.nil?
+ visit(node)
+ else
+ case (key = node.key).type
+ when :interpolated_symbol_node
+ opening = key.opening
+
+ if opening.start_with?("%")
+ group do
+ text(preferred_quote)
+ key.parts.each { |part| visit(part) }
+ text("#{preferred_quote}:")
+ end
+ else
+ group do
+ text(key.opening[1..])
+ key.parts.each { |part| visit(part) }
+ text(key.closing)
+ text(":")
+ end
+ end
+ when :symbol_node
+ value = key.value
+ if value.match?(/^[_A-Za-z]+$/)
+ text(value)
+ else
+ text("#{preferred_quote}#{value}#{preferred_quote}")
+ end
+
+ text(":")
+ else
+ raise "Unexpected key: #{key.inspect}"
+ end
+
+ visit_assoc_node_value(node.value)
+ end
+ end
+
+ # Visit an assoc node and format the key as a rocket.
+ private def visit_assoc_node_rocket(node)
+ case (key = node.key).type
+ when :interpolated_symbol_node
+ opening = key.opening
+
+ if opening.start_with?("%")
+ visit(key)
+ else
+ group do
+ text(":")
+ text(opening)
+ key.parts.each { |part| visit(part) }
+ text(key.closing.chomp(":"))
+ end
+ end
+ when :symbol_node
+ if key.closing&.end_with?(":")
+ text(":")
+ text(key.value)
+ else
+ visit(key)
+ end
+ else
+ visit(key)
+ end
+
+ text(" ")
+ operator_loc = node.operator_loc
+ operator_loc ? visit_location(operator_loc) : text("=>")
+ visit_assoc_node_value(node.value)
+ end
+
+ # def foo(**); bar(**); end
+ # ^^
+ #
+ # { **foo }
+ # ^^^^^
+ def visit_assoc_splat_node(node)
+ visit_prefix(node.operator_loc, node.value)
+ end
+
+ # $+
+ # ^^
+ def visit_back_reference_read_node(node)
+ text(node.slice)
+ end
+
+ # begin end
+ # ^^^^^^^^^
+ def visit_begin_node(node)
+ begin_keyword_loc = node.begin_keyword_loc
+ statements = node.statements
+
+ rescue_clause = node.rescue_clause
+ else_clause = node.else_clause
+ ensure_clause = node.ensure_clause
+
+ if begin_keyword_loc.nil?
+ group do
+ visit(statements) unless statements.nil?
+
+ unless rescue_clause.nil?
+ align(-2) do
+ breakable_force
+ visit(rescue_clause)
+ end
+ end
+
+ unless else_clause.nil?
+ align(-2) do
+ breakable_force
+ visit(else_clause)
+ end
+ end
+
+ unless ensure_clause.nil?
+ align(-2) do
+ breakable_force
+ visit(ensure_clause)
+ end
+ end
+ end
+ else
+ group do
+ visit_location(begin_keyword_loc)
+
+ unless statements.nil?
+ indent do
+ breakable_force
+ visit(statements)
+ end
+ end
+
+ unless rescue_clause.nil?
+ breakable_force
+ visit(rescue_clause)
+ end
+
+ unless else_clause.nil?
+ breakable_force
+ visit(else_clause)
+ end
+
+ unless ensure_clause.nil?
+ breakable_force
+ visit(ensure_clause)
+ end
+
+ breakable_force
+ text("end")
+ end
+ end
+ end
+
+ # foo(&bar)
+ # ^^^^
+ def visit_block_argument_node(node)
+ visit_prefix(node.operator_loc, node.expression)
+ end
+
+ # foo { |; bar| }
+ # ^^^
+ def visit_block_local_variable_node(node)
+ text(node.name.name)
+ end
+
+ private def inside_command?(end_index)
+ stack[0...end_index].reverse_each.any? do |parent|
+ case parent.type
+ when :statements_node
+ return false
+ when :call_node
+ return true if parent.arguments && parent.opening_loc.nil?
+ return false unless parent.opening_loc.nil?
+ end
+ false
+ end
+ end
+
+ # A block on a keyword or method call.
+ def visit_block_node(node)
+ parameters = node.parameters
+ body = node.body
+ opening = node.opening
+
+ # 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.
+ previous = nil
+ break_opening, break_closing, flat_opening, flat_closing =
+ if inside_command?(-2)
+ block_close = opening == "do" ? "end" : "}"
+ [opening, block_close, opening, block_close]
+ elsif %i[forwarding_super_node super_node].include?(stack[-2].type)
+ %w[do end do end]
+ elsif stack[0...-1].reverse_each.any? { |parent|
+ case parent.type
+ when :parentheses_node, :statements_node
+ break false
+ when :if_node, :unless_node, :while_node, :until_node
+ break true if parent.predicate == previous
+ end
+
+ previous = parent
+ false
+ }
+ %w[{ } { }]
+ else
+ %w[do end { }]
+ end
+
+ parent = stack[-2]
+
+ # If the receiver of this block a call without parentheses, so we need to
+ # break the block.
+ if parent.is_a?(CallNode) && parent.arguments && parent.opening_loc.nil?
+ break_parent
+ visit_block_node_break(node, break_opening, break_closing)
+ else
+ group do
+ if_break { visit_block_node_break(node, break_opening, break_closing) }.if_flat do
+ text(flat_opening)
+
+ if parameters.is_a?(BlockParametersNode)
+ text(" ")
+ visit(parameters)
+ end
+
+ breakable_space if body || node.closing_loc.comments.any?
+ visit_body(body, node.closing_loc.comments, false)
+ breakable_space if parameters || body
+
+ text(flat_closing)
+ end
+ end
+ end
+ end
+
+ # Visit a block node in the break form.
+ private def visit_block_node_break(node, break_opening, break_closing)
+ parameters = node.parameters
+
+ text(break_opening)
+ node.opening_loc.comments.each { |comment| visit_comment(comment) }
+
+ if parameters.is_a?(BlockParametersNode)
+ text(" ")
+ visit(parameters)
+ end
+
+ visit_body(node.body, node.closing_loc.comments, false)
+ breakable_space
+ text(break_closing)
+ end
+
+ # def foo(&bar); end
+ # ^^^^
+ def visit_block_parameter_node(node)
+ name = node.name
+
+ group do
+ visit_location(node.operator_loc)
+
+ if name
+ align(1) do
+ breakable_empty
+ text(name.name)
+ end
+ end
+ end
+ end
+
+ # A block's parameters.
+ def visit_block_parameters_node(node)
+ parameters = node.parameters
+ locals = node.locals
+ opening_loc = node.opening_loc
+
+ group do
+ if parameters || locals.any?
+ if opening_loc
+ visit_location(opening_loc)
+ else
+ text("(")
+ end
+ end
+
+ remove_breaks(visit(parameters)) if parameters
+
+ if locals.any?
+ text("; ")
+ seplist(locals) { |local| visit(local) }
+ end
+
+ text(node.closing || ")") if parameters || locals.any?
+ end
+ end
+
+ # break
+ # ^^^^^
+ #
+ # break foo
+ # ^^^^^^^^^
+ def visit_break_node(node)
+ visit_jump("break", node.arguments)
+ end
+
+ ATTACH_DIRECTLY = %i[
+ array_node
+ hash_node
+ string_node
+ interpolated_string_node
+ x_string_node
+ interpolated_x_string_node
+ if_node
+ unless_node
+ ].freeze
+
+ # foo
+ # ^^^
+ #
+ # foo.bar
+ # ^^^^^^^
+ #
+ # foo.bar() {}
+ # ^^^^^^^^^^^^
+ def visit_call_node(node)
+ receiver = node.receiver
+ message = node.message
+ name = node.name
+
+ opening_loc = node.opening_loc
+ closing_loc = node.closing_loc
+
+ arguments = [*node.arguments&.arguments]
+ block = node.block
+
+ if block.is_a?(BlockArgumentNode)
+ arguments << block
+ block = nil
+ end
+
+ unless node.safe_navigation?
+ case name
+ when :!
+ if message == "not"
+ if receiver
+ group do
+ text("not")
+
+ if opening_loc
+ visit_location(opening_loc)
+ else
+ if_break { text("(") }.if_flat { text(" ") }
+ end
+
+ indent do
+ breakable_empty
+ visit(receiver)
+ end
+
+ if closing_loc
+ breakable_empty
+ visit_location(closing_loc)
+ else
+ if_break do
+ breakable_empty
+ text(")")
+ end
+ end
+ end
+ else
+ text("not()")
+ end
+
+ return
+ end
+
+ if arguments.empty? && block.nil?
+ visit_prefix(node.message_loc, receiver)
+ return
+ end
+ when :+@, :-@, :~
+ if arguments.empty? && block.nil?
+ visit_prefix(node.message_loc, receiver)
+ return
+ end
+ when :+, :-, :*, :/, :%, :==, :===, :!=, :!~, :=~, :>, :<, :>=, :<=, :<=>, :<<, :>>, :&, :|,
+ :^
+ if arguments.length == 1 && block.nil?
+ visit_binary(receiver, node.message_loc, arguments.first)
+ return
+ end
+ when :**
+ if arguments.length == 1 && block.nil?
+ group do
+ visit(receiver)
+ text("**")
+ indent do
+ breakable_empty
+ seplist(arguments) { |argument| visit(argument) }
+ end
+ end
+
+ return
+ end
+ when :[]
+ group do
+ visit(receiver)
+ text("[")
+
+ if arguments.any?
+ indent do
+ breakable_empty
+ seplist(arguments) { |argument| visit(argument) }
+ end
+
+ breakable_empty
+ end
+
+ text("]")
+
+ if block
+ text(" ")
+ visit(block)
+ end
+ end
+
+ return
+ when :[]=
+ if arguments.any?
+ group do
+ *before, after = arguments
+
+ group do
+ visit(receiver)
+ text("[")
+
+ if before.any?
+ indent do
+ breakable_empty
+ seplist(before) { |argument| visit(argument) }
+ end
+ breakable_empty
+ end
+
+ text("]")
+ end
+
+ text(" ")
+ group do
+ text("=")
+ indent do
+ breakable_space
+ visit(after)
+ end
+ end
+
+ if block
+ text(" ")
+ visit(block)
+ end
+ end
+ else
+ group do
+ visit(receiver)
+ text("[]")
+
+ if block
+ text(" ")
+ visit(block)
+ end
+ end
+ end
+
+ return
+ when :to, :to_not, :not_to
+ # Very special handling here for RSpec. Methods on expectation objects
+ # are almost always used without parentheses. This can result in
+ # pretty ugly formatting, because the DSL gets super confusing.
+ if opening_loc.nil?
+ group do
+ visit(receiver) if receiver
+ visit_call_node_call_operator(node.call_operator_loc) if node.call_operator_loc
+ visit_location(node.message_loc) if node.message_loc
+ visit_call_node_rhs(node, 0)
+ end
+
+ return
+ end
+ end
+ end
+
+ # Now that we've passed through all of the special handling for specific
+ # method names, we can handle the general case of a method call. In this
+ # case we'll first build up a call chain for all of the calls in a row.
+ # This could potentially be just a single method call.
+ chain = [node]
+ current = node
+
+ while (receiver = current.receiver).is_a?(CallNode)
+ chain.unshift(receiver)
+ current = receiver
+ end
+
+ chain.unshift(receiver) if receiver
+
+ if chain.length > 1
+ if !ATTACH_DIRECTLY.include?(receiver&.type) &&
+ chain[0...-1].all? { |node|
+ !node.is_a?(CallNode) ||
+ (
+ ((node.opening_loc.nil? && !node.arguments) || node.name == :[]) &&
+ !node.block &&
+ node.location.comments.none? &&
+ !node.call_operator_loc&.comments&.any? &&
+ !node.message_loc&.comments&.any?
+ )
+ } &&
+ !chain[-1].call_operator_loc&.comments&.any? &&
+ !chain[-1].message_loc&.comments&.any?
+ # Special handling here for the case that we have a call chain that is
+ # just method names and operators, ending with a call that has
+ # anything else. In this case we'll put everything on the same line
+ # and break the chain at the end. This can look like:
+ #
+ # foo.bar.baz { |qux| qux }
+ #
+ # In this case if it gets broken, we don't want multiple lines of
+ # method calls, instead we want to only break the block at the end,
+ # like:
+ #
+ # foo.bar.baz do |qux|
+ # qux
+ # end
+ #
+ group do
+ *rest, last = chain
+ doc =
+ align(0) do
+ visit(rest.shift) unless rest.first.is_a?(CallNode)
+
+ rest.each do |node|
+ visit_call_node_call_operator(node.call_operator_loc)
+
+ if node.name == :[]
+ visit_call_node_rhs(node, 0)
+ else
+ visit_location(node.message_loc)
+ end
+ end
+
+ visit_call_node_call_operator(last.call_operator_loc)
+ end
+
+ group do
+ visit_location(last.message_loc) if last.message_loc && last.name != :[]
+ visit_call_node_rhs(last, last_position(doc) + (last.message&.length || 0) + 1)
+ end
+ end
+ else
+ # Otherwise we'll break the chain at each node, indenting all of the
+ # calls beyond the first one by one level of indentation.
+ group do
+ first, *rest = chain
+
+ # If a call operator has a trailing comment on it, then we need to
+ # put it on the previous line. In this case we need to communicate
+ # to the next iteration in the loop that we have already printed the
+ # call operator.
+ call_operator_printed = false
+
+ case first.type
+ when :call_node
+ # If the first node in the chain is a call node, we only need to
+ # print the message because we will not have a receiver and we
+ # will handle the arguments and block in the loop below.
+ visit_location(first.message_loc)
+ visit_call_node_rhs(first, first.message.length + 1)
+
+ # If the first call in the chain has a trailing comment on its
+ # call operator, then we need to print it within this group.
+ if (subseq = rest.first) &&
+ (
+ (call_operator_loc = subseq.call_operator_loc)&.trailing_comments&.any? ||
+ subseq.message_loc&.leading_comments&.any?
+ )
+ call_operator_printed = true
+ visit_call_node_call_operator(call_operator_loc)
+ end
+
+ if first.block.is_a?(BlockNode)
+ node = rest.shift
+
+ group do
+ if rest.any?
+ node.location.leading_comments.each { |comment| visit_comment(comment) }
+ end
+
+ visit_call_node_call_operator(node.call_operator_loc) unless call_operator_printed
+ visit_location(node.message_loc) if node.message_loc && node.name != :[]
+ visit_call_node_rhs(node, (message&.length || 0) + 2)
+
+ if rest.any?
+ node.location.trailing_comments.each { |comment| visit_comment(comment) }
+
+ # If the first call in the chain has a trailing comment on its
+ # call operator, then we need to print it within this group.
+ if (subseq = rest.first) &&
+ (
+ (
+ call_operator_loc = subseq.call_operator_loc
+ )&.trailing_comments&.any? ||
+ subseq.message_loc&.leading_comments&.any?
+ )
+ call_operator_printed = true
+ visit_call_node_call_operator(call_operator_loc)
+ else
+ call_operator_printed = false
+ end
+ end
+ end
+ end
+ when *ATTACH_DIRECTLY
+ # Certain nodes we want to attach our message directly to them,
+ # because it looks strange to have a message on a separate line.
+ group do
+ visit(first)
+ node = rest.shift
+
+ group do
+ if rest.any?
+ node.location.leading_comments.each { |comment| visit_comment(comment) }
+ end
+
+ visit_call_node_call_operator(node.call_operator_loc)
+ visit_location(node.message_loc) if node.message_loc && node.name != :[]
+ visit_call_node_rhs(node, (message&.length || 0) + 2)
+
+ if rest.any?
+ node.location.trailing_comments.each { |comment| visit_comment(comment) }
+
+ # If the first call in the chain has a trailing comment on its
+ # call operator, then we need to print it within this group.
+ if (subseq = rest.first) &&
+ (
+ (
+ call_operator_loc = subseq.call_operator_loc
+ )&.trailing_comments&.any? ||
+ subseq.message_loc&.leading_comments&.any?
+ )
+ call_operator_printed = true
+ visit_call_node_call_operator(call_operator_loc)
+ end
+ end
+ end
+ end
+ else
+ # Otherwise, we'll format the receiver of the first member of the
+ # call chain and then indent all of the calls by one level.
+ visit(first)
+
+ # If the first call in the chain has a trailing comment on its
+ # call operator, then we need to print it within this group.
+ if (subseq = rest.first) &&
+ (
+ (call_operator_loc = subseq.call_operator_loc)&.trailing_comments&.any? ||
+ subseq.message_loc&.leading_comments&.any?
+ )
+ call_operator_printed = true
+ visit_call_node_call_operator(call_operator_loc)
+ end
+ end
+
+ inside_command = inside_command?(-1)
+ indent do
+ rest.each_with_index do |node, index|
+ if inside_command && !call_operator_printed
+ # Do not break the chain if we're inside a command, because
+ # that would lead to this method call being placed on the
+ # command as opposed to this chain.
+ elsif node.name == :[]
+ # If this is a call to `[]`, then we don't want to break the
+ # chain here, because we want to effectively treat it as a
+ # postfix operator.
+ elsif node.name == :not && (receiver = node.receiver).is_a?(CallNode) &&
+ receiver.name == :where &&
+ !receiver.arguments &&
+ !receiver.block
+ # Generally we will always break the chain at each node.
+ # However, there is some nice behavior here if we have a call
+ # chain with `where.not` in it (common in Rails). In that case
+ # it's nice to keep the `not` on the same line as the `where`.
+ elsif call_operator_printed &&
+ node.message_loc&.leading_comments&.any? { |comment|
+ comment.is_a?(EmbDocComment)
+ }
+ # If we have already printed the call operator and the message
+ # location has a leading embdoc comment, then we already have
+ # a newline printed in this chain.
+ else
+ breakable_empty
+ end
+
+ group do
+ if index != rest.length - 1
+ node.location.leading_comments.each { |comment| visit_comment(comment) }
+ end
+
+ visit_call_node_call_operator(node.call_operator_loc) unless call_operator_printed
+ visit_location(node.message_loc) if node.message_loc && node.name != :[]
+ visit_call_node_rhs(node, (node.message&.length || 0) + 2)
+
+ if index != rest.length - 1
+ node.location.trailing_comments.each { |comment| visit_comment(comment) }
+
+ # If the call operator has a trailing comment, then we need
+ # to print it within this group.
+ if (subseq = rest[index + 1]) &&
+ (
+ (
+ call_operator_loc = subseq.call_operator_loc
+ )&.trailing_comments&.any? ||
+ subseq.message_loc&.leading_comments&.any?
+ )
+ call_operator_printed = true
+ visit_call_node_call_operator(call_operator_loc)
+ else
+ call_operator_printed = false
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ else
+ # If there is no call chain, then it's not possible that there's a
+ # receiver. In this case we'll visit the message and then the arguments
+ # and block.
+ group do
+ visit_location(node.message_loc)
+ visit_call_node_rhs(node, node.message.length + 1)
+ end
+ end
+ end
+
+ private def visit_call_node_call_operator(location)
+ visit_location(location, location.slice == "&." ? "&." : ".") if location
+ end
+
+ private def visit_call_node_rhs(node, position)
+ arguments = [*node.arguments&.arguments]
+ block = node.block
+
+ if block.is_a?(BlockArgumentNode)
+ arguments << block
+ block = nil
+ end
+
+ if arguments.length == 1 && node.name.end_with?("=") &&
+ !%i[== === != >= <=].include?(node.name) &&
+ block.nil?
+ argument = arguments.first
+ text(" =")
+
+ if indent_write?(argument)
+ indent do
+ breakable_space
+ visit(argument)
+ end
+ else
+ text(" ")
+ visit(argument)
+ end
+ elsif !node.opening_loc.nil? && arguments.any? && !node.closing_loc.nil?
+ group do
+ visit_location(node.message_loc) if node.name == :[] && node.call_operator_loc
+ visit_location(node.opening_loc)
+ indent do
+ breakable_empty
+ seplist(arguments) { |argument| visit(argument) }
+
+ if trailing_comma && !arguments.last.is_a?(BlockArgumentNode) &&
+ !(
+ arguments.length == 1 && (argument = arguments.first).is_a?(CallNode) &&
+ argument.arguments &&
+ argument.opening_loc.nil?
+ )
+ if_break { text(",") }
+ end
+ end
+ breakable_empty
+ visit_location(node.closing_loc)
+ end
+ elsif arguments.any?
+ text(" ")
+ group { visit_call_node_command_arguments(node, arguments, position) }
+ elsif node.opening_loc && node.closing_loc
+ visit_location(node.opening_loc)
+ visit_location(node.closing_loc)
+ end
+
+ if block
+ text(" ")
+ visit(block)
+ end
+ end
+
+ # Align the contents of the given node with the last position. This is used
+ # to align method calls without parentheses.
+ private def visit_call_node_command_arguments(node, arguments, position)
+ if node.arguments && node.arguments.arguments.length == 1
+ argument = node.arguments.arguments.first
+
+ case argument.type
+ when :def_node
+ seplist(arguments) { |argument| visit(argument) }
+ return
+ when :call_node
+ if argument.opening_loc.nil?
+ visit_call_node_command_arguments(argument, arguments, position)
+ return
+ elsif argument.block.is_a?(BlockNode)
+ seplist(arguments) { |argument| visit(argument) }
+ return
+ end
+ end
+ end
+
+ align(position > (print_width / 2) ? 0 : position) do
+ seplist(arguments) { |argument| visit(argument) }
+ end
+ end
+
+ # foo.bar += baz
+ # ^^^^^^^^^^^^^^^
+ def visit_call_operator_write_node(node)
+ receiver = node.receiver
+ call_operator_loc = node.call_operator_loc
+
+ visit_write(node.binary_operator_loc, node.value) do
+ group do
+ if receiver
+ visit(receiver)
+ visit_call_node_call_operator(call_operator_loc)
+ end
+
+ text(node.message)
+ end
+ end
+ end
+
+ # foo.bar &&= baz
+ # ^^^^^^^^^^^^^^^
+ def visit_call_and_write_node(node)
+ receiver = node.receiver
+ call_operator_loc = node.call_operator_loc
+
+ visit_write(node.operator_loc, node.value) do
+ group do
+ if receiver
+ visit(receiver)
+ visit_call_node_call_operator(call_operator_loc)
+ end
+
+ text(node.message)
+ end
+ end
+ end
+
+ # foo.bar ||= baz
+ # ^^^^^^^^^^^^^^^
+ def visit_call_or_write_node(node)
+ receiver = node.receiver
+ call_operator_loc = node.call_operator_loc
+
+ visit_write(node.operator_loc, node.value) do
+ group do
+ if receiver
+ visit(receiver)
+ visit_call_node_call_operator(call_operator_loc)
+ end
+
+ text(node.message)
+ end
+ end
+ end
+
+ # foo.bar, = 1
+ # ^^^^^^^
+ def visit_call_target_node(node)
+ group do
+ visit(node.receiver)
+ visit_location(node.call_operator_loc)
+ text(node.message)
+ end
+ end
+
+ # foo => bar => baz
+ # ^^^^^^^^^^
+ def visit_capture_pattern_node(node)
+ visit_binary(node.value, node.operator_loc, node.target)
+ end
+
+ # case foo; when bar; end
+ # ^^^^^^^^^^^^^^^^^^^^^^^
+ def visit_case_node(node)
+ visit_case(
+ node.case_keyword_loc,
+ node.predicate,
+ node.conditions,
+ node.else_clause,
+ node.end_keyword_loc
+ )
+ end
+
+ # case foo; in bar; end
+ # ^^^^^^^^^^^^^^^^^^^^^
+ def visit_case_match_node(node)
+ visit_case(
+ node.case_keyword_loc,
+ node.predicate,
+ node.conditions,
+ node.else_clause,
+ node.end_keyword_loc
+ )
+ end
+
+ private def visit_case(case_keyword_loc, predicate, conditions, else_clause, end_keyword_loc)
+ group do
+ group do
+ visit_location(case_keyword_loc)
+
+ if predicate
+ text(" ")
+ align(5) { visit(predicate) }
+ end
+ end
+
+ breakable_force
+ seplist(conditions, -> { breakable_force }) { |condition| visit(condition) }
+
+ if else_clause
+ breakable_force
+ visit(else_clause)
+ end
+
+ indent do
+ end_keyword_loc.comments.each do |comment|
+ breakable_force
+ text(comment.location.slice)
+ end
+ end
+
+ breakable_force
+ text("end")
+ end
+ end
+
+ # class Foo; end
+ # ^^^^^^^^^^^^^^
+ def visit_class_node(node)
+ class_keyword_loc = node.class_keyword_loc
+ inheritance_operator_loc = node.inheritance_operator_loc
+ superclass = node.superclass
+
+ group do
+ group do
+ visit_location(class_keyword_loc)
+
+ if class_keyword_loc.comments.any?
+ indent do
+ breakable_space
+ visit(node.constant_path)
+ end
+ else
+ text(" ")
+ visit(node.constant_path)
+ end
+
+ if superclass
+ text(" ")
+ visit_location(inheritance_operator_loc)
+
+ if inheritance_operator_loc.comments.any?
+ indent do
+ breakable_space
+ visit(superclass)
+ end
+ else
+ text(" ")
+ visit(superclass)
+ end
+ end
+ end
+
+ visit_body(node.body, node.end_keyword_loc.comments)
+ breakable_force
+ text("end")
+ end
+ end
+
+ # @@foo
+ # ^^^^^
+ def visit_class_variable_read_node(node)
+ text(node.name.name)
+ end
+
+ # @@foo = 1
+ # ^^^^^^^^^
+ #
+ # @@foo, @@bar = 1
+ # ^^^^^ ^^^^^
+ def visit_class_variable_write_node(node)
+ visit_write(node.operator_loc, node.value) { text(node.name.name) }
+ end
+
+ # @@foo += bar
+ # ^^^^^^^^^^^^
+ def visit_class_variable_operator_write_node(node)
+ visit_write(node.binary_operator_loc, node.value) { text(node.name.name) }
+ end
+
+ # @@foo &&= bar
+ # ^^^^^^^^^^^^^
+ def visit_class_variable_and_write_node(node)
+ visit_write(node.operator_loc, node.value) { text(node.name.name) }
+ end
+
+ # @@foo ||= bar
+ # ^^^^^^^^^^^^^
+ def visit_class_variable_or_write_node(node)
+ visit_write(node.operator_loc, node.value) { text(node.name.name) }
+ end
+
+ # @@foo, = bar
+ # ^^^^^
+ def visit_class_variable_target_node(node)
+ text(node.name.name)
+ end
+
+ # Foo
+ # ^^^
+ def visit_constant_read_node(node)
+ text(node.name.name)
+ end
+
+ # Foo = 1
+ # ^^^^^^^
+ #
+ # Foo, Bar = 1
+ # ^^^ ^^^
+ def visit_constant_write_node(node)
+ visit_write(node.operator_loc, node.value) { text(node.name.name) }
+ end
+
+ # Foo += bar
+ # ^^^^^^^^^^^
+ def visit_constant_operator_write_node(node)
+ visit_write(node.binary_operator_loc, node.value) { text(node.name.name) }
+ end
+
+ # Foo &&= bar
+ # ^^^^^^^^^^^^
+ def visit_constant_and_write_node(node)
+ visit_write(node.operator_loc, node.value) { text(node.name.name) }
+ end
+
+ # Foo ||= bar
+ # ^^^^^^^^^^^^
+ def visit_constant_or_write_node(node)
+ visit_write(node.operator_loc, node.value) { text(node.name.name) }
+ end
+
+ # Foo, = bar
+ # ^^^
+ def visit_constant_target_node(node)
+ text(node.name.name)
+ end
+
+ # Foo::Bar
+ # ^^^^^^^^
+ def visit_constant_path_node(node)
+ parent = node.parent
+
+ group do
+ visit(parent) if parent
+ visit_location(node.delimiter_loc)
+ indent do
+ breakable_empty
+ visit_location(node.name_loc)
+ end
+ end
+ end
+
+ # Foo::Bar = 1
+ # ^^^^^^^^^^^^
+ #
+ # Foo::Foo, Bar::Bar = 1
+ # ^^^^^^^^ ^^^^^^^^
+ def visit_constant_path_write_node(node)
+ visit_write(node.operator_loc, node.value) { visit(node.target) }
+ end
+
+ # Foo::Bar += baz
+ # ^^^^^^^^^^^^^^^
+ def visit_constant_path_operator_write_node(node)
+ visit_write(node.binary_operator_loc, node.value) { visit(node.target) }
+ end
+
+ # Foo::Bar &&= baz
+ # ^^^^^^^^^^^^^^^^
+ def visit_constant_path_and_write_node(node)
+ visit_write(node.operator_loc, node.value) { visit(node.target) }
+ end
+
+ # Foo::Bar ||= baz
+ # ^^^^^^^^^^^^^^^^
+ def visit_constant_path_or_write_node(node)
+ visit_write(node.operator_loc, node.value) { visit(node.target) }
+ end
+
+ # Foo::Bar, = baz
+ # ^^^^^^^^
+ def visit_constant_path_target_node(node)
+ parent = node.parent
+
+ group do
+ visit(parent) if parent
+ visit_location(node.delimiter_loc)
+ indent do
+ breakable_empty
+ visit_location(node.name_loc)
+ end
+ end
+ end
+
+ # def foo; end
+ # ^^^^^^^^^^^^
+ #
+ # def self.foo; end
+ # ^^^^^^^^^^^^^^^^^
+ def visit_def_node(node)
+ receiver = node.receiver
+ name_loc = node.name_loc
+ parameters = node.parameters
+ lparen_loc = node.lparen_loc
+ rparen_loc = node.rparen_loc
+
+ group do
+ group do
+ group do
+ text("def")
+ text(" ") if !receiver.nil? || name_loc.leading_comments.none?
+
+ group do
+ if receiver
+ visit(receiver)
+ text(".")
+ end
+
+ visit_location(name_loc)
+ end
+ end
+
+ if parameters
+ lparen_loc ? visit_location(lparen_loc) : text("(")
+
+ if parameters
+ indent do
+ breakable_empty
+ visit(parameters)
+ end
+ end
+
+ breakable_empty
+ text(")")
+
+ # Very specialized behavior here where inline comments do not force
+ # a break parent. This should probably be an option on
+ # visit_location.
+ rparen_loc&.comments&.each do |comment|
+ if comment.is_a?(InlineComment)
+ line_suffix(COMMENT_PRIORITY) do
+ comment.trailing? ? text(" ") : breakable
+ text(comment.location.slice)
+ end
+ else
+ breakable_force
+ trim
+ text(comment.location.slice.rstrip)
+ end
+ end
+ else
+ visit_location(lparen_loc) if lparen_loc
+ breakable_empty if lparen_loc&.comments&.any?
+ visit_location(rparen_loc) if rparen_loc
+ end
+ end
+
+ if node.equal_loc
+ text(" ")
+ visit_location(node.equal_loc)
+ text(" ")
+ visit(node.body)
+ else
+ visit_body(node.body, node.end_keyword_loc.comments)
+ breakable_force
+ text("end")
+ end
+ end
+ end
+
+ # defined? a
+ # ^^^^^^^^^^
+ #
+ # defined?(a)
+ # ^^^^^^^^^^^
+ def visit_defined_node(node)
+ group do
+ visit_location(node.keyword_loc)
+ if (lparen_loc = node.lparen_loc)
+ visit_location(lparen_loc)
+ else
+ text("(")
+ end
+
+ visit_body(node.value, node.rparen_loc&.comments || [], false)
+ breakable_empty
+ text(")")
+ end
+ end
+
+ # if foo then bar else baz end
+ # ^^^^^^^^^^^^
+ def visit_else_node(node)
+ group do
+ visit_location(node.else_keyword_loc)
+ visit_body(node.statements, node.end_keyword_loc.comments)
+ end
+ end
+
+ # "foo #{bar}"
+ # ^^^^^^
+ def visit_embedded_statements_node(node)
+ group do
+ visit_location(node.opening_loc)
+
+ if (statements = node.statements)
+ indent do
+ breakable_empty
+ visit(statements)
+ end
+ breakable_empty
+ end
+
+ text("}")
+ end
+ end
+
+ # "foo #@bar"
+ # ^^^^^
+ def visit_embedded_variable_node(node)
+ group do
+ text("\#{")
+ indent do
+ breakable_empty
+ visit(node.variable)
+ end
+ breakable_empty
+ text("}")
+ end
+ end
+
+ # begin; foo; ensure; bar; end
+ # ^^^^^^^^^^^^
+ def visit_ensure_node(node)
+ group do
+ visit_location(node.ensure_keyword_loc)
+ visit_body(node.statements, node.end_keyword_loc.comments)
+ end
+ end
+
+ # false
+ # ^^^^^
+ def visit_false_node(_node)
+ text("false")
+ end
+
+ # foo => [*, bar, *]
+ # ^^^^^^^^^^^
+ def visit_find_pattern_node(node)
+ constant = node.constant
+
+ group do
+ visit(constant) if constant
+ text("[")
+
+ indent do
+ breakable_empty
+ seplist([node.left, *node.requireds, node.right]) { |element| visit(element) }
+ end
+
+ breakable_empty
+ text("]")
+ end
+ end
+
+ # if foo .. bar; end
+ # ^^^^^^^^^^
+ def visit_flip_flop_node(node)
+ left = node.left
+ right = node.right
+
+ group do
+ visit(left) if left
+ text(" ")
+ visit_location(node.operator_loc)
+
+ if right
+ indent do
+ breakable_space
+ visit(right)
+ end
+ end
+ end
+ end
+
+ # 1.0
+ # ^^^
+ def visit_float_node(node)
+ text(node.slice)
+ end
+
+ # for foo in bar do end
+ # ^^^^^^^^^^^^^^^^^^^^^
+ def visit_for_node(node)
+ group do
+ text("for ")
+ group { visit(node.index) }
+ text(" in ")
+ group { visit(node.collection) }
+ visit_body(node.statements, node.end_keyword_loc.comments)
+ breakable_force
+ text("end")
+ end
+ end
+
+ # def foo(...); bar(...); end
+ # ^^^
+ def visit_forwarding_arguments_node(_node)
+ text("...")
+ end
+
+ # def foo(...); end
+ # ^^^
+ def visit_forwarding_parameter_node(_node)
+ text("...")
+ end
+
+ # super
+ # ^^^^^
+ #
+ # super {}
+ # ^^^^^^^^
+ def visit_forwarding_super_node(node)
+ block = node.block
+
+ if block
+ group do
+ text("super ")
+ visit(block)
+ end
+ else
+ text("super")
+ end
+ end
+
+ # $foo
+ # ^^^^
+ def visit_global_variable_read_node(node)
+ text(node.name.name)
+ end
+
+ # $foo = 1
+ # ^^^^^^^^
+ #
+ # $foo, $bar = 1
+ # ^^^^ ^^^^
+ def visit_global_variable_write_node(node)
+ visit_write(node.operator_loc, node.value) { text(node.name.name) }
+ end
+
+ # $foo += bar
+ # ^^^^^^^^^^^
+ def visit_global_variable_operator_write_node(node)
+ visit_write(node.binary_operator_loc, node.value) { text(node.name.name) }
+ end
+
+ # $foo &&= bar
+ # ^^^^^^^^^^^^
+ def visit_global_variable_and_write_node(node)
+ visit_write(node.operator_loc, node.value) { text(node.name.name) }
+ end
+
+ # $foo ||= bar
+ # ^^^^^^^^^^^^
+ def visit_global_variable_or_write_node(node)
+ visit_write(node.operator_loc, node.value) { text(node.name.name) }
+ end
+
+ # $foo, = bar
+ # ^^^^
+ def visit_global_variable_target_node(node)
+ text(node.name.name)
+ end
+
+ # {}
+ # ^^
+ def visit_hash_node(node)
+ elements = node.elements
+
+ group_if(!stack[-2].is_a?(AssocNode)) do
+ if elements.any? { |element| element.value.is_a?(ImplicitNode) }
+ visit_hash_node_layout(node) { |element| visit(element) }
+ elsif elements.all? { |element|
+ !element.is_a?(AssocNode) || element.operator_loc.nil? ||
+ element.key.is_a?(InterpolatedSymbolNode) ||
+ (
+ element.key.is_a?(SymbolNode) && (value = element.key.value) &&
+ value.match?(/^[_A-Za-z]/) &&
+ !value.end_with?("=")
+ )
+ }
+ visit_hash_node_layout(node) { |element| visit_assoc_node_label(element) }
+ else
+ visit_hash_node_layout(node) { |element| visit_assoc_node_rocket(element) }
+ end
+ end
+ end
+
+ # Visit a hash node and yield out each plain association element for
+ # formatting by the caller.
+ private def visit_hash_node_layout(node)
+ elements = node.elements
+
+ group do
+ visit_location(node.opening_loc)
+ indent do
+ if elements.any?
+ breakable_space
+ seplist(elements) do |element|
+ if element.is_a?(AssocNode)
+ if element.value.is_a?(HashNode)
+ yield element
+ else
+ group { yield element }
+ end
+ else
+ visit(element)
+ end
+ end
+ if_break { text(",") } if trailing_comma
+ end
+
+ node.closing_loc.comments.each do |comment|
+ breakable_force
+ text(comment.location.slice)
+ end
+ end
+
+ elements.any? ? breakable_space : breakable_empty
+ text("}")
+ end
+ end
+
+ # foo => {}
+ # ^^
+ def visit_hash_pattern_node(node)
+ constant = node.constant
+ opening_loc = node.opening_loc
+ closing_loc = node.closing_loc
+
+ elements = [*node.elements, *node.rest]
+
+ if constant
+ group do
+ visit(constant)
+ text("[")
+ opening_loc.comments.each { |comment| visit_comment(comment) } if opening_loc
+ visit_elements(elements, closing_loc&.comments || [])
+ breakable_empty
+ text("]")
+ end
+ else
+ group do
+ text("{")
+ opening_loc.comments.each { |comment| visit_comment(comment) } if opening_loc
+ visit_elements_spaced(elements, closing_loc&.comments || [])
+ elements.any? ? breakable_space : breakable_empty
+ text("}")
+ end
+ end
+ end
+
+ # if foo then bar end
+ # ^^^^^^^^^^^^^^^^^^^
+ #
+ # bar if foo
+ # ^^^^^^^^^^
+ #
+ # foo ? bar : baz
+ # ^^^^^^^^^^^^^^^
+ def visit_if_node(node)
+ if_keyword_loc = node.if_keyword_loc
+ if_keyword = node.if_keyword
+
+ statements = node.statements
+ subsequent = node.subsequent
+
+ if if_keyword == "elsif"
+ # If we get here, then this is an if node that was expressed as an elsif
+ # clause in a larger chain. In this case we can simplify formatting
+ # because there are many things we don't need to check.
+ group do
+ visit_location(if_keyword_loc)
+ text(" ")
+ align(6) { visit(node.predicate) }
+
+ if subsequent
+ visit_body(statements, [], true)
+ breakable_force
+ visit(subsequent)
+ else
+ visit_body(statements, node.end_keyword_loc.comments, true)
+ end
+ end
+ elsif !if_keyword_loc
+ # If there is no keyword location, then this if node was expressed as a
+ # ternary. In this case we know quite a bit about the structure of the
+ # node and will format it quite differently.
+ truthy = statements.body.first
+ falsy = subsequent.statements.body.first
+
+ if stack[-2].is_a?(ParenthesesNode) || forced_ternary?(truthy) || forced_ternary?(falsy)
+ group { visit_ternary_node_flat(node, truthy, falsy) }
+ else
+ group do
+ if_break { visit_ternary_node_break(node, truthy, falsy) }.if_flat do
+ visit_ternary_node_flat(node, truthy, falsy)
+ end
+ end
+ end
+ elsif !statements || subsequent || contains_conditional?(statements.body.first)
+ # If there are no statements, no subsequent clause, or the body of the
+ # node has a conditional, then we will format the node in a break form,
+ # which is to say the keyword first.
+ group do
+ visit_if_node_break(node)
+ break_parent
+ end
+ elsif contains_write?(node.predicate) || contains_write?(statements)
+ # If the predicate or the body of the node contains a write, then
+ # changing the form of the conditional could impact the meaning of the
+ # expression. In this case we will respect the form of the source.
+ if node.end_keyword_loc.nil?
+ group { visit_if_node_flat(node) }
+ else
+ group do
+ visit_if_node_break(node)
+ break_parent
+ end
+ end
+ else
+ # Otherwise, we will attempt to format the node in the flat form if it
+ # fits, and otherwise we will break it into multiple lines.
+ group do
+ if_break { visit_if_node_break(node) }.if_flat do
+ ensure_parentheses { visit_if_node_flat(node) }
+ end
+ end
+ end
+ end
+
+ private def forced_ternary?(node)
+ case node.type
+ when :alias_node, :alias_global_variable_node, :break_node, :if_node, :unless_node,
+ :lambda_node, :multi_write_node, :next_node, :rescue_modifier_node, :super_node,
+ :forwarding_super_node, :undef_node, :yield_node, :return_node, :call_and_write_node,
+ :call_or_write_node, :call_operator_write_node, :class_variable_write_node,
+ :class_variable_and_write_node, :class_variable_or_write_node,
+ :class_variable_operator_write_node, :constant_write_node, :constant_and_write_node,
+ :constant_or_write_node, :constant_operator_write_node, :constant_path_write_node,
+ :constant_path_and_write_node, :constant_path_or_write_node,
+ :constant_path_operator_write_node, :global_variable_write_node,
+ :global_variable_and_write_node, :global_variable_or_write_node,
+ :global_variable_operator_write_node, :instance_variable_write_node,
+ :instance_variable_and_write_node, :instance_variable_or_write_node,
+ :instance_variable_operator_write_node, :local_variable_write_node,
+ :local_variable_and_write_node, :local_variable_or_write_node,
+ :local_variable_operator_write_node
+ true
+ when :call_node
+ node.receiver && node.opening_loc.nil?
+ when :string_node, :interpolated_string_node, :x_string_node, :interpolated_x_string_node
+ node.heredoc?
+ else
+ false
+ end
+ end
+
+ private def visit_ternary_node_flat(node, truthy, falsy)
+ visit(node.predicate)
+ text(" ?")
+ indent do
+ breakable_space
+ visit(truthy)
+ text(" :")
+ breakable_space
+ visit(falsy)
+ end
+ end
+
+ private def visit_ternary_node_break(node, truthy, falsy)
+ group do
+ text("if ")
+ align(3) { visit(node.predicate) }
+ end
+
+ indent do
+ breakable_space
+ visit(truthy)
+ end
+
+ breakable_space
+ text("else")
+
+ indent do
+ breakable_space
+ visit(falsy)
+ end
+
+ breakable_space
+ text("end")
+ end
+
+ # Visit an if node in the break form.
+ private def visit_if_node_break(node)
+ statements = node.statements
+ subsequent = node.subsequent
+
+ group do
+ visit_location(node.if_keyword_loc)
+ text(" ")
+ align(3) { visit(node.predicate) }
+ end
+
+ if subsequent
+ visit_body(statements, [], false)
+ breakable_space
+ visit(subsequent)
+ else
+ visit_body(statements, node.end_keyword_loc&.comments || [], false)
+ end
+
+ breakable_space
+ text("end")
+ end
+
+ # Visit an if node in the flat form.
+ private def visit_if_node_flat(node)
+ visit(node.statements)
+ text(" if ")
+ visit(node.predicate)
+ end
+
+ # 1i
+ def visit_imaginary_node(node)
+ text(node.slice)
+ end
+
+ # { foo: }
+ # ^^^^
+ def visit_implicit_node(node)
+ # Nothing, because it represents implicit syntax.
+ end
+
+ # foo { |bar,| }
+ # ^
+ def visit_implicit_rest_node(_node)
+ text(",")
+ end
+
+ # case foo; in bar; end
+ # ^^^^^^^^^^^^^^^^^^^^^
+ def visit_in_node(node)
+ statements = node.statements
+
+ group do
+ text("in ")
+ align(3) { visit(node.pattern) }
+
+ if statements
+ indent do
+ breakable_force
+ visit(statements)
+ end
+ end
+ end
+ end
+
+ # foo[bar] += baz
+ # ^^^^^^^^^^^^^^^
+ def visit_index_operator_write_node(node)
+ arguments = [*node.arguments, *node.block]
+
+ visit_write(node.binary_operator_loc, node.value) do
+ group do
+ visit(node.receiver)
+ visit_location(node.opening_loc)
+
+ if arguments.any?
+ indent do
+ breakable_empty
+ seplist(arguments) { |argument| visit(argument) }
+ end
+ end
+
+ breakable_empty
+ text("]")
+ end
+ end
+ end
+
+ # foo[bar] &&= baz
+ # ^^^^^^^^^^^^^^^^
+ def visit_index_and_write_node(node)
+ arguments = [*node.arguments, *node.block]
+
+ visit_write(node.operator_loc, node.value) do
+ group do
+ visit(node.receiver)
+ visit_location(node.opening_loc)
+
+ if arguments.any?
+ indent do
+ breakable_empty
+ seplist(arguments) { |argument| visit(argument) }
+ end
+ end
+
+ breakable_empty
+ text("]")
+ end
+ end
+ end
+
+ # foo[bar] ||= baz
+ # ^^^^^^^^^^^^^^^^
+ def visit_index_or_write_node(node)
+ arguments = [*node.arguments, *node.block]
+
+ visit_write(node.operator_loc, node.value) do
+ group do
+ visit(node.receiver)
+ visit_location(node.opening_loc)
+
+ if arguments.any?
+ indent do
+ breakable_empty
+ seplist(arguments) { |argument| visit(argument) }
+ end
+ end
+
+ breakable_empty
+ text("]")
+ end
+ end
+ end
+
+ # foo[bar], = 1
+ # ^^^^^^^^
+ def visit_index_target_node(node)
+ group do
+ visit(node.receiver)
+ visit_location(node.opening_loc)
+
+ if (arguments = (node.arguments&.arguments || [])).any?
+ indent do
+ breakable_empty
+ seplist(arguments) { |argument| visit(argument) }
+ end
+ end
+
+ breakable_empty
+ text("]")
+ end
+ end
+
+ # @foo
+ # ^^^^
+ def visit_instance_variable_read_node(node)
+ text(node.name.name)
+ end
+
+ # @foo = 1
+ # ^^^^^^^^
+ #
+ # @foo, @bar = 1
+ # ^^^^ ^^^^
+ def visit_instance_variable_write_node(node)
+ visit_write(node.operator_loc, node.value) { text(node.name.name) }
+ end
+
+ # @foo += bar
+ # ^^^^^^^^^^^
+ def visit_instance_variable_operator_write_node(node)
+ visit_write(node.binary_operator_loc, node.value) { text(node.name.name) }
+ end
+
+ # @foo &&= bar
+ # ^^^^^^^^^^^^
+ def visit_instance_variable_and_write_node(node)
+ visit_write(node.operator_loc, node.value) { text(node.name.name) }
+ end
+
+ # @foo ||= bar
+ # ^^^^^^^^^^^^
+ def visit_instance_variable_or_write_node(node)
+ visit_write(node.operator_loc, node.value) { text(node.name.name) }
+ end
+
+ # @foo, = bar
+ # ^^^^
+ def visit_instance_variable_target_node(node)
+ text(node.name.name)
+ end
+
+ # 1
+ # ^
+ def visit_integer_node(node)
+ slice = node.slice
+
+ if slice.match?(/^[1-9]\d{4,}$/)
+ # 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 = (slice.length + 2) % 3
+ text(" #{slice}"[index..].scan(/.../).join("_").strip)
+ else
+ text(slice)
+ end
+ end
+
+ # if /foo #{bar}/ then end
+ # ^^^^^^^^^^^^
+ def visit_interpolated_match_last_line_node(node)
+ visit_regular_expression_node_parts(node, node.parts)
+ end
+
+ # /foo #{bar}/
+ # ^^^^^^^^^^^^
+ def visit_interpolated_regular_expression_node(node)
+ visit_regular_expression_node_parts(node, node.parts)
+ end
+
+ # "foo #{bar}"
+ # ^^^^^^^^^^^^
+ def visit_interpolated_string_node(node)
+ parts = node.parts
+
+ if node.heredoc?
+ # First, if this interpolated string was expressed as a heredoc, then
+ # we'll maintain that formatting and print it out again as a heredoc.
+ visit_heredoc(node, parts)
+ elsif parts.length > 1 && independent_string?(parts[0]) && independent_string?(parts[1])
+ # Next, we'll check if this string is composed of multiple parts that
+ # have their own opening. If it is, then this actually represents string
+ # concatenation and not a single string literal. In this case we'll
+ # format each on their own with appropriate spacing.
+ group do
+ visit(parts[0])
+ if_break { text(" \\") }
+ indent do
+ breakable_space
+ seplist(
+ parts[1..],
+ -> do
+ if_break { text(" \\") }
+ breakable_space
+ end
+ ) { |part| visit(part) }
+ end
+ end
+ else
+ # Finally, if it's a regular interpolated string, we'll forward this on
+ # to our generic string node formatter.
+ visit_string_node_parts(node, node.parts)
+ end
+ end
+
+ private def independent_string?(node)
+ case node.type
+ when :string_node
+ !node.opening_loc.nil?
+ when :interpolated_string_node
+ !node.opening_loc.nil?
+ else
+ false
+ end
+ end
+
+ # :"foo #{bar}"
+ # ^^^^^^^^^^^^^
+ def visit_interpolated_symbol_node(node)
+ opening = node.opening
+ parts = node.parts
+
+ # First, we'll check if we don't have an opening. If we don't, then this
+ # is inside of a %I literal and we should just print the parts as-is.
+ return group { parts.each { |part| visit(part) } } if opening.nil?
+
+ # 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 = stack[-2]
+ hash_key = parent.is_a?(AssocNode) && parent.key == node
+
+ # Here we determine the quotes to use for an interpolated symbol. It's
+ # bound by a lot of rules because it could be in many different contexts
+ # with many different kinds of escaping.
+ opening_quote, closing_quote =
+ if opening.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(opening[2])
+ pattern = /[\n#{Regexp.escape(matching)}'"]/
+
+ # This check is to ensure we don't find a matching quote inside of the
+ # symbol that would be confusing.
+ matched = parts.any? { |part| part.is_a?(StringNode) && part.content.match?(pattern) }
+
+ if matched
+ [opening, matching]
+ elsif quotes_locked?(parts)
+ ["#{":" unless hash_key}'", "'"]
+ else
+ ["#{":" unless hash_key}#{preferred_quote}", preferred_quote]
+ end
+ elsif quotes_locked?(parts)
+ if hash_key
+ if opening.start_with?(":")
+ [opening[1..], "#{opening[1..]}:"]
+ else
+ [opening, node.closing]
+ end
+ else
+ [opening, node.closing]
+ end
+ else
+ [hash_key ? preferred_quote : ":#{preferred_quote}", preferred_quote]
+ end
+
+ group do
+ text(opening_quote)
+ parts.each do |part|
+ if part.is_a?(StringNode)
+ value = quotes_normalize(part.content, closing_quote)
+ first = true
+
+ value.each_line(chomp: true) do |line|
+ if first
+ first = false
+ else
+ breakable_return
+ end
+
+ text(line)
+ end
+
+ breakable_return if value.end_with?("\n")
+ else
+ visit(part)
+ end
+ end
+ text(closing_quote)
+ end
+ end
+
+ # `foo #{bar}`
+ # ^^^^^^^^^^^^
+ def visit_interpolated_x_string_node(node)
+ if node.heredoc?
+ visit_heredoc(node, node.parts)
+ else
+ group do
+ text(node.opening)
+ node.parts.each { |part| visit(part) }
+ text(node.closing)
+ end
+ end
+ end
+
+ # it
+ # ^^
+ def visit_it_local_variable_read_node(_node)
+ text("it")
+ end
+
+ # foo(bar: baz)
+ # ^^^^^^^^
+ def visit_keyword_hash_node(node)
+ elements = node.elements
+
+ case stack[-2]&.type
+ when :break_node, :next_node, :return_node
+ visit_keyword_hash_node_layout(node) { |element| visit(element) }
+ else
+ if elements.any? { |element| element.value.is_a?(ImplicitNode) }
+ visit_keyword_hash_node_layout(node) { |element| visit(element) }
+ elsif elements.all? { |element|
+ !element.is_a?(AssocNode) || element.operator_loc.nil? ||
+ element.key.is_a?(InterpolatedSymbolNode) ||
+ (
+ element.key.is_a?(SymbolNode) && (value = element.key.value) &&
+ value.match?(/^[_A-Za-z]/) &&
+ !value.end_with?("=")
+ )
+ }
+ visit_keyword_hash_node_layout(node) do |element|
+ group { visit_assoc_node_label(element) }
+ end
+ else
+ visit_keyword_hash_node_layout(node) do |element|
+ group { visit_assoc_node_rocket(element) }
+ end
+ end
+ end
+ end
+
+ # -> { it }
+ # ^^^^^^^^^
+ def visit_it_parameters_node(_node)
+ raise "Visiting ItParametersNode is not supported."
+ end
+
+ # Visit a keyword hash node and yield out each plain association element for
+ # formatting by the caller.
+ private def visit_keyword_hash_node_layout(node)
+ seplist(node.elements) do |element|
+ if element.is_a?(AssocNode)
+ yield element
+ else
+ visit(element)
+ end
+ end
+ end
+
+ # def foo(**bar); end
+ # ^^^^^
+ #
+ # def foo(**); end
+ # ^^
+ def visit_keyword_rest_parameter_node(node)
+ name = node.name
+
+ text("**")
+ text(name.name) if name
+ end
+
+ # -> {}
+ def visit_lambda_node(node)
+ parameters = node.parameters
+ body = node.body
+ closing_comments = node.closing_loc.comments
+
+ group do
+ text("->")
+ visit(parameters) if parameters.is_a?(BlockParametersNode)
+
+ if body || closing_comments.any?
+ text(" ")
+ if_break do
+ text("do")
+ node.opening_loc.comments.each { |comment| visit_comment(comment) }
+
+ indent do
+ if body
+ breakable_space
+ visit(body)
+ end
+
+ closing_comments.each do |comment|
+ breakable_force
+
+ if comment.is_a?(InlineComment)
+ text(comment.location.slice)
+ else
+ trim
+ text(comment.location.slice.rstrip)
+ end
+ end
+ end
+
+ breakable_space
+ text("end")
+ end.if_flat do
+ if body
+ text("{ ")
+ visit(body)
+ text(" }")
+ else
+ text(" {}")
+ end
+ end
+ else
+ text(" {}")
+ end
+ end
+ end
+
+ # foo
+ # ^^^
+ def visit_local_variable_read_node(node)
+ text(node.name.name)
+ end
+
+ # foo = 1
+ # ^^^^^^^
+ #
+ # foo, bar = 1
+ # ^^^ ^^^
+ def visit_local_variable_write_node(node)
+ visit_write(node.operator_loc, node.value) { text(node.name.name) }
+ end
+
+ # foo += bar
+ # ^^^^^^^^^^
+ def visit_local_variable_operator_write_node(node)
+ visit_write(node.binary_operator_loc, node.value) { text(node.name.name) }
+ end
+
+ # foo &&= bar
+ # ^^^^^^^^^^^
+ def visit_local_variable_and_write_node(node)
+ visit_write(node.operator_loc, node.value) { text(node.name.name) }
+ end
+
+ # foo ||= bar
+ # ^^^^^^^^^^^
+ def visit_local_variable_or_write_node(node)
+ visit_write(node.operator_loc, node.value) { text(node.name.name) }
+ end
+
+ # foo, = bar
+ # ^^^
+ def visit_local_variable_target_node(node)
+ text(node.name.name)
+ end
+
+ # if /foo/ then end
+ # ^^^^^
+ def visit_match_last_line_node(node)
+ visit_regular_expression_node_parts(
+ node,
+ [bare_string(node.send(:source), node.location.copy, node.content_loc)]
+ )
+ end
+
+ # foo in bar
+ # ^^^^^^^^^^
+ def visit_match_predicate_node(node)
+ pattern = node.pattern
+
+ group do
+ visit(node.value)
+ text(" in")
+
+ case pattern.type
+ when :array_pattern_node, :hash_pattern_node, :find_pattern_node
+ text(" ")
+ visit(pattern)
+ else
+ indent do
+ breakable_space
+ visit(pattern)
+ end
+ end
+ end
+ end
+
+ # foo => bar
+ # ^^^^^^^^^^
+ def visit_match_required_node(node)
+ pattern = node.pattern
+
+ group do
+ visit(node.value)
+ text(" =>")
+
+ case pattern.type
+ when :array_pattern_node, :hash_pattern_node, :find_pattern_node
+ text(" ")
+ visit(pattern)
+ else
+ indent do
+ breakable_space
+ visit(pattern)
+ end
+ end
+ end
+ end
+
+ # /(?foo)/ =~ bar
+ # ^^^^^^^^^^^^^^^^^^^^
+ def visit_match_write_node(node)
+ visit(node.call)
+ end
+
+ # A node that is missing from the syntax tree. This is only used in the
+ # case of a syntax error. We'll format it as empty.
+ def visit_missing_node(node)
+ end
+
+ # module Foo; end
+ # ^^^^^^^^^^^^^^^
+ def visit_module_node(node)
+ module_keyword_loc = node.module_keyword_loc
+
+ group do
+ group do
+ visit_location(module_keyword_loc)
+
+ if module_keyword_loc.comments.any?
+ indent do
+ breakable_space
+ visit(node.constant_path)
+ end
+ else
+ text(" ")
+ visit(node.constant_path)
+ end
+ end
+
+ visit_body(node.body, node.end_keyword_loc.comments)
+ breakable_force
+ text("end")
+ end
+ end
+
+ # foo, bar = baz
+ # ^^^^^^^^
+ def visit_multi_target_node(node)
+ targets = [*node.lefts, *node.rest, *node.rights]
+ implicit_rest = targets.pop if targets.last.is_a?(ImplicitRestNode)
+ lparen_loc = node.lparen_loc
+
+ group do
+ if lparen_loc && node.rparen_loc
+ visit_location(lparen_loc)
+ indent do
+ breakable_empty
+ seplist(targets) { |target| visit(target) }
+ visit(implicit_rest) if implicit_rest
+ end
+ breakable_empty
+ text(")")
+ else
+ seplist(targets) { |target| visit(target) }
+ visit(implicit_rest) if implicit_rest
+ end
+ end
+ end
+
+ # foo, bar = baz
+ # ^^^^^^^^^^^^^^
+ def visit_multi_write_node(node)
+ targets = [*node.lefts, *node.rest, *node.rights]
+ implicit_rest = targets.pop if targets.last.is_a?(ImplicitRestNode)
+ lparen_loc = node.lparen_loc
+
+ visit_write(node.operator_loc, node.value) do
+ group do
+ if lparen_loc && node.rparen_loc
+ visit_location(lparen_loc)
+ indent do
+ breakable_empty
+ seplist(targets) { |target| visit(target) }
+ visit(implicit_rest) if implicit_rest
+ end
+ breakable_empty
+ text(")")
+ else
+ seplist(targets) { |target| visit(target) }
+ visit(implicit_rest) if implicit_rest
+ end
+ end
+ end
+ end
+
+ # next
+ # ^^^^
+ #
+ # next foo
+ # ^^^^^^^^
+ def visit_next_node(node)
+ visit_jump("next", node.arguments)
+ end
+
+ # nil
+ # ^^^
+ def visit_nil_node(_node)
+ text("nil")
+ end
+
+ # def foo(**nil); end
+ # ^^^^^
+ def visit_no_keywords_parameter_node(node)
+ group do
+ visit_location(node.operator_loc)
+ align(2) do
+ breakable_empty
+ text("nil")
+ end
+ end
+ end
+
+ # -> { _1 + _2 }
+ # ^^^^^^^^^^^^^^
+ def visit_numbered_parameters_node(_node)
+ raise "Visiting NumberedParametersNode is not supported."
+ end
+
+ # $1
+ # ^^
+ def visit_numbered_reference_read_node(node)
+ text(node.slice)
+ end
+
+ # def foo(bar: baz); end
+ # ^^^^^^^^
+ def visit_optional_keyword_parameter_node(node)
+ group do
+ text("#{node.name.name}:")
+ indent do
+ breakable_space
+ visit(node.value)
+ end
+ end
+ end
+
+ # def foo(bar = 1); end
+ # ^^^^^^^
+ def visit_optional_parameter_node(node)
+ group do
+ text("#{node.name.name} ")
+ visit_location(node.operator_loc)
+ indent do
+ breakable_space
+ visit(node.value)
+ end
+ end
+ end
+
+ # a or b
+ # ^^^^^^
+ def visit_or_node(node)
+ visit_binary(node.left, node.operator_loc, node.right)
+ end
+
+ # def foo(bar, *baz); end
+ # ^^^^^^^^^
+ def visit_parameters_node(node)
+ parameters = node.compact_child_nodes
+ implicit_rest = parameters.pop if parameters.last.is_a?(ImplicitRestNode)
+
+ align(0) do
+ seplist(parameters) { |parameter| visit(parameter) }
+ visit(implicit_rest) if implicit_rest
+ end
+ end
+
+ # ()
+ # ^^
+ #
+ # (1)
+ # ^^^
+ def visit_parentheses_node(node)
+ group do
+ visit_location(node.opening_loc)
+ visit_body(node.body, node.closing_loc.comments, false)
+ breakable_empty
+ text(")")
+ end
+ end
+
+ # foo => ^(bar)
+ # ^^^^^^
+ def visit_pinned_expression_node(node)
+ group do
+ text("^")
+ visit_location(node.lparen_loc)
+ visit_body(node.expression, node.rparen_loc.comments, false)
+ breakable_empty
+ text(")")
+ end
+ end
+
+ # foo = 1 and bar => ^foo
+ # ^^^^
+ def visit_pinned_variable_node(node)
+ visit_prefix(node.operator_loc, node.variable)
+ end
+
+ # END {}
+ def visit_post_execution_node(node)
+ statements = node.statements
+ closing_comments = node.closing_loc.comments
+
+ group do
+ text("END ")
+ visit_location(node.opening_loc)
+
+ if statements || closing_comments.any?
+ indent do
+ if statements
+ breakable_space
+ visit(statements)
+ end
+
+ closing_comments.each do |comment|
+ breakable_empty
+ text(comment.location.slice)
+ end
+ end
+
+ breakable_space
+ end
+
+ text("}")
+ end
+ end
+
+ # BEGIN {}
+ def visit_pre_execution_node(node)
+ statements = node.statements
+ closing_comments = node.closing_loc.comments
+
+ group do
+ text("BEGIN ")
+ visit_location(node.opening_loc)
+
+ if statements || closing_comments.any?
+ indent do
+ if statements
+ breakable_space
+ visit(statements)
+ end
+
+ closing_comments.each do |comment|
+ breakable_empty
+ text(comment.location.slice)
+ end
+ end
+
+ breakable_space
+ end
+
+ text("}")
+ end
+ end
+
+ # The top-level program node.
+ def visit_program_node(node)
+ visit(node.statements)
+ seplist(node.location.comments, -> { breakable_force }) do |comment|
+ text(comment.location.slice.rstrip)
+ end
+ break_parent
+ end
+
+ # 0..5
+ # ^^^^
+ def visit_range_node(node)
+ left = node.left
+ right = node.right
+
+ group do
+ visit(left) if left
+ visit_location(node.operator_loc)
+ visit(right) if right
+ end
+ end
+
+ # 1r
+ # ^^
+ def visit_rational_node(node)
+ text(node.slice)
+ end
+
+ # redo
+ # ^^^^
+ def visit_redo_node(_node)
+ text("redo")
+ end
+
+ # /foo/
+ # ^^^^^
+ def visit_regular_expression_node(node)
+ visit_regular_expression_node_parts(
+ node,
+ [bare_string(node.send(:source), node.location.copy, node.content_loc)]
+ )
+ end
+
+ # def foo(bar:); end
+ # ^^^^
+ def visit_required_keyword_parameter_node(node)
+ text("#{node.name.name}:")
+ end
+
+ # def foo(bar); end
+ # ^^^
+ def visit_required_parameter_node(node)
+ text(node.name.name)
+ end
+
+ # foo rescue bar
+ # ^^^^^^^^^^^^^^
+ def visit_rescue_modifier_node(node)
+ group do
+ text("begin")
+ indent do
+ breakable_force
+ visit(node.expression)
+ end
+ breakable_force
+ visit_location(node.keyword_loc)
+ text(" StandardError")
+ indent do
+ breakable_force
+ visit(node.rescue_expression)
+ end
+ breakable_force
+ text("end")
+ end
+ end
+
+ # begin; rescue; end
+ # ^^^^^^^
+ def visit_rescue_node(node)
+ exceptions = node.exceptions
+ operator_loc = node.operator_loc
+ reference = node.reference
+
+ statements = node.statements
+ subsequent = node.subsequent
+
+ group do
+ group do
+ visit_location(node.keyword_loc)
+
+ if exceptions.any?
+ text(" ")
+ align(7) { seplist(exceptions) { |exception| visit(exception) } }
+ elsif reference.nil?
+ text(" StandardError")
+ end
+
+ if reference
+ text(" ")
+ visit_location(operator_loc)
+
+ if operator_loc.comments.any?
+ indent do
+ breakable_space
+ visit(reference)
+ end
+ else
+ text(" ")
+ visit(reference)
+ end
+ end
+ end
+
+ if statements
+ indent do
+ breakable_force
+ visit(statements)
+ end
+ end
+
+ if subsequent
+ breakable_force
+ visit(subsequent)
+ end
+ end
+ end
+
+ # def foo(*bar); end
+ # ^^^^
+ #
+ # def foo(*); end
+ # ^
+ def visit_rest_parameter_node(node)
+ name = node.name
+
+ if name
+ group do
+ visit_location(node.operator_loc)
+ align(1) do
+ breakable_empty
+ text(node.name.name)
+ end
+ end
+ else
+ visit_location(node.operator_loc)
+ end
+ end
+
+ # retry
+ # ^^^^^
+ def visit_retry_node(_node)
+ text("retry")
+ end
+
+ # return
+ # ^^^^^^
+ #
+ # return 1
+ # ^^^^^^^^
+ def visit_return_node(node)
+ visit_jump("return", node.arguments)
+ end
+
+ # self
+ # ^^^^
+ def visit_self_node(_node)
+ text("self")
+ end
+
+ # A shareable constant.
+ def visit_shareable_constant_node(node)
+ visit(node.write)
+ end
+
+ # class << self; end
+ # ^^^^^^^^^^^^^^^^^^
+ def visit_singleton_class_node(node)
+ operator_loc = node.operator_loc
+
+ group do
+ group do
+ text("class ")
+ visit_location(operator_loc)
+
+ if operator_loc.comments.any?
+ indent do
+ breakable_space
+ visit(node.expression)
+ end
+ else
+ text(" ")
+ visit(node.expression)
+ end
+ end
+
+ visit_body(node.body, node.end_keyword_loc.comments)
+ breakable_force
+ text("end")
+ end
+ end
+
+ # __ENCODING__
+ # ^^^^^^^^^^^^
+ def visit_source_encoding_node(_node)
+ text("__ENCODING__")
+ end
+
+ # __FILE__
+ # ^^^^^^^^
+ def visit_source_file_node(_node)
+ text("__FILE__")
+ end
+
+ # __LINE__
+ # ^^^^^^^^
+ def visit_source_line_node(_node)
+ text("__LINE__")
+ end
+
+ # foo(*bar)
+ # ^^^^
+ #
+ # def foo((bar, *baz)); end
+ # ^^^^
+ #
+ # def foo(*); bar(*); end
+ # ^
+ def visit_splat_node(node)
+ expression = node.expression
+ operator_loc = node.operator_loc
+
+ if expression
+ group do
+ text("*")
+ operator_loc.comments.each { |comment| visit_comment(comment) }
+
+ align(1) { visit(expression) }
+ end
+ else
+ text("*")
+ operator_loc.comments.each { |comment| visit_comment(comment) }
+ end
+ end
+
+ # A list of statements.
+ def visit_statements_node(node)
+ parent = stack[-2]
+
+ previous_line = nil
+ previous_access_control = false
+
+ node.body.each do |statement|
+ if previous_line.nil?
+ visit(statement)
+ elsif ((statement.location.start_line - previous_line) > 1) || previous_access_control ||
+ access_control?(statement)
+ breakable_force
+ breakable_force
+ visit(statement)
+ elsif (statement.location.start_line != previous_line) ||
+ !parent.is_a?(EmbeddedStatementsNode)
+ breakable_force
+ visit(statement)
+ else
+ text("; ")
+ visit(statement)
+ end
+
+ previous_line = statement.heredoc_end_line
+ previous_access_control = access_control?(statement)
+ end
+ end
+
+ # "foo"
+ # ^^^^^
+ def visit_string_node(node)
+ if node.heredoc?
+ visit_heredoc(node, [node])
+ else
+ opening = node.opening
+ content = node.content
+
+ if !opening
+ text(content)
+ elsif opening == "?"
+ if content.length == 1
+ text(preferred_quote)
+ text(content == preferred_quote ? "\\#{preferred_quote}" : content)
+ text(preferred_quote)
+ else
+ text("?#{content}")
+ end
+ else
+ visit_string_node_parts(node, [node])
+ end
+ end
+ end
+
+ # super(foo)
+ # ^^^^^^^^^^
+ def visit_super_node(node)
+ arguments = [*node.arguments]
+ block = node.block
+
+ if block.is_a?(BlockArgumentNode)
+ arguments << block
+ block = nil
+ end
+
+ group do
+ text("super")
+
+ if node.lparen_loc && node.rparen_loc
+ text("(")
+
+ if arguments.any?
+ indent do
+ breakable_empty
+ seplist(arguments) { |argument| visit(argument) }
+ end
+ breakable_empty
+ end
+
+ text(")")
+ elsif arguments.any?
+ text(" ")
+ align(6) { seplist(arguments) { |argument| visit(argument) } }
+ end
+
+ if block
+ text(" ")
+ visit(block)
+ end
+ end
+ end
+
+ # :foo
+ # ^^^^
+ def visit_symbol_node(node)
+ text(node.slice)
+ end
+
+ # true
+ # ^^^^
+ def visit_true_node(_node)
+ text("true")
+ end
+
+ # undef foo
+ # ^^^^^^^^^
+ def visit_undef_node(node)
+ group do
+ text("undef ")
+ align(6) do
+ seplist(node.names) do |name|
+ if name.is_a?(SymbolNode)
+ text(name.value)
+
+ if (comment = name.location.comments.first)
+ visit_comment(comment)
+ end
+ else
+ visit(name)
+ end
+ end
+ end
+ end
+ end
+
+ # unless foo; bar end
+ # ^^^^^^^^^^^^^^^^^^^
+ #
+ # bar unless foo
+ # ^^^^^^^^^^^^^^
+ def visit_unless_node(node)
+ statements = node.statements
+ else_clause = node.else_clause
+
+ if !statements || else_clause || contains_conditional?(statements.body.first)
+ group do
+ visit_unless_node_break(node)
+ break_parent
+ end
+ elsif contains_write?(node.predicate) || contains_write?(statements)
+ if node.end_keyword_loc
+ group do
+ visit_unless_node_break(node)
+ break_parent
+ end
+ else
+ group { visit_unless_node_flat(node) }
+ end
+ else
+ group do
+ if_break { visit_unless_node_break(node) }.if_flat do
+ ensure_parentheses { visit_unless_node_flat(node) }
+ end
+ end
+ end
+ end
+
+ # Visit an unless node in the break form.
+ private def visit_unless_node_break(node)
+ statements = node.statements
+ else_clause = node.else_clause
+
+ group do
+ visit_location(node.keyword_loc)
+ text(" ")
+ align(3) { visit(node.predicate) }
+ end
+
+ if else_clause
+ visit_body(statements, [], false)
+ breakable_space
+ visit(else_clause)
+ else
+ visit_body(statements, node.end_keyword_loc&.comments || [], false)
+ end
+
+ breakable_space
+ text("end")
+ end
+
+ # Visit an unless node in the flat form.
+ private def visit_unless_node_flat(node)
+ visit(node.statements)
+ text(" unless ")
+ visit(node.predicate)
+ end
+
+ # until foo; bar end
+ # ^^^^^^^^^^^^^^^^^
+ #
+ # bar until foo
+ # ^^^^^^^^^^^^^
+ def visit_until_node(node)
+ statements = node.statements
+ closing_loc = node.closing_loc
+
+ if node.begin_modifier?
+ group { visit_until_node_flat(node) }
+ elsif statements.nil? || node.keyword_loc.comments.any? || closing_loc&.comments&.any?
+ group do
+ visit_until_node_break(node)
+ break_parent
+ end
+ elsif contains_write?(node.predicate) || contains_write?(statements)
+ if closing_loc
+ group do
+ visit_until_node_break(node)
+ break_parent
+ end
+ else
+ group { visit_until_node_flat(node) }
+ end
+ else
+ group { if_break { visit_until_node_break(node) }.if_flat { visit_until_node_flat(node) } }
+ end
+ end
+
+ # Visit an until node in the break form.
+ private def visit_until_node_break(node)
+ visit_location(node.keyword_loc)
+ text(" ")
+ align(6) { visit(node.predicate) }
+ visit_body(node.statements, node.closing_loc&.comments || [], false)
+ breakable_space
+ text("end")
+ end
+
+ # Visit an until node in the flat form.
+ private def visit_until_node_flat(node)
+ ensure_parentheses do
+ visit(node.statements)
+ text(" until ")
+ visit(node.predicate)
+ end
+ end
+
+ # case foo; when bar; end
+ # ^^^^^^^^^^^^^
+ def visit_when_node(node)
+ conditions = node.conditions
+ statements = node.statements
+
+ group do
+ group do
+ text("when ")
+ align(5) do
+ seplist(conditions, -> { group { comma_breakable } }) { |condition| visit(condition) }
+
+ # Very special case here. If you're inside of a when clause and the
+ # last condition is an endless range, then you are forced to use the
+ # "then" keyword to make it parse properly.
+ last = conditions.last
+ text(" then") if last.is_a?(RangeNode) && last.right.nil?
+ end
+ end
+
+ if statements
+ indent do
+ breakable_force
+ visit(statements)
+ end
+ end
+ end
+ end
+
+ # while foo; bar end
+ # ^^^^^^^^^^^^^^^^^^
+ #
+ # bar while foo
+ # ^^^^^^^^^^^^^
+ def visit_while_node(node)
+ statements = node.statements
+ closing_loc = node.closing_loc
+
+ if node.begin_modifier?
+ group { visit_while_node_flat(node) }
+ elsif statements.nil? || node.keyword_loc.comments.any? || closing_loc&.comments&.any?
+ group do
+ visit_while_node_break(node)
+ break_parent
+ end
+ elsif contains_write?(node.predicate) || contains_write?(statements)
+ if closing_loc
+ group do
+ visit_while_node_break(node)
+ break_parent
+ end
+ else
+ group { visit_while_node_flat(node) }
+ end
+ else
+ group { if_break { visit_while_node_break(node) }.if_flat { visit_while_node_flat(node) } }
+ end
+ end
+
+ # Visit a while node in the flat form.
+ private def visit_while_node_flat(node)
+ ensure_parentheses do
+ visit(node.statements)
+ text(" while ")
+ visit(node.predicate)
+ end
+ end
+
+ # Visit a while node in the break form.
+ private def visit_while_node_break(node)
+ visit_location(node.keyword_loc)
+ text(" ")
+ align(6) { visit(node.predicate) }
+ visit_body(node.statements, node.closing_loc&.comments || [], false)
+ breakable_space
+ text("end")
+ end
+
+ # `foo`
+ # ^^^^^
+ def visit_x_string_node(node)
+ if node.heredoc?
+ visit_heredoc(node, [node])
+ else
+ text("`#{node.content}`")
+ end
+ end
+
+ # yield
+ # ^^^^^
+ #
+ # yield 1
+ # ^^^^^^^
+ def visit_yield_node(node)
+ arguments = node.arguments
+
+ if arguments.nil?
+ text("yield")
+ else
+ lparen_loc = node.lparen_loc
+ rparen_loc = node.rparen_loc
+
+ group do
+ text("yield")
+
+ if lparen_loc
+ visit_location(lparen_loc)
+ else
+ if_break { text("(") }.if_flat { text(" ") }
+ end
+
+ indent do
+ breakable_empty
+ visit(arguments)
+ end
+ breakable_empty
+
+ if rparen_loc
+ visit_location(rparen_loc)
+ else
+ if_break { text(")") }
+ end
+ end
+ end
+ end
+
+ private
+
+ # --------------------------------------------------------------------------
+ # Helper methods
+ # --------------------------------------------------------------------------
+
+ # Returns whether or not the given statement is an access control statement.
+ # Truthfully, we can't actually tell this for sure without performing method
+ # lookup, but we assume none of these methods are overridden.
+ def access_control?(statement)
+ statement.is_a?(CallNode) && statement.variable_call? &&
+ %i[private protected public].include?(statement.name)
+ end
+
+ # There are times when it is useful to create string nodes so that
+ # non-interpolated nodes can be formatted as if they were their interpolated
+ # counterparts with a single part.
+ def bare_string(source, location, content_loc)
+ StringNode.new(source, 0, location, 0, nil, content_loc, 0, "")
+ end
+
+ # (source, node_id, location, flags, opening_loc, content_loc, closing_loc, unescaped)
+
+ # True if the given node contains a conditional expression.
+ def contains_conditional?(node)
+ case node.type
+ when :if_node, :unless_node
+ true
+ else
+ false
+ end
+ end
+
+ # True if the given node contains a write expression.
+ def contains_write?(node)
+ case node.type
+ when :call_and_write_node, :call_or_write_node, :call_operator_write_node,
+ :class_variable_write_node, :class_variable_and_write_node,
+ :class_variable_or_write_node, :class_variable_operator_write_node, :constant_write_node,
+ :constant_and_write_node, :constant_or_write_node, :constant_operator_write_node,
+ :constant_path_write_node, :constant_path_and_write_node, :constant_path_or_write_node,
+ :constant_path_operator_write_node, :global_variable_write_node,
+ :global_variable_and_write_node, :global_variable_or_write_node,
+ :global_variable_operator_write_node, :instance_variable_write_node,
+ :instance_variable_and_write_node, :instance_variable_or_write_node,
+ :instance_variable_operator_write_node, :local_variable_write_node,
+ :local_variable_and_write_node, :local_variable_or_write_node,
+ :local_variable_operator_write_node, :multi_write_node
+ true
+ when :class_node, :module_node, :singleton_class_node
+ false
+ else
+ node.compact_child_nodes.any? { |child| contains_write?(child) }
+ 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.
+ def ensure_parentheses
+ case stack[-2]&.type
+ when :arguments_node, :assoc_node, :call_node, :call_and_write_node, :call_or_write_node,
+ :call_operator_write_node, :class_variable_write_node, :class_variable_and_write_node,
+ :class_variable_or_write_node, :class_variable_operator_write_node, :constant_write_node,
+ :constant_and_write_node, :constant_or_write_node, :constant_operator_write_node,
+ :constant_path_write_node, :constant_path_and_write_node, :constant_path_or_write_node,
+ :constant_path_operator_write_node, :global_variable_write_node,
+ :global_variable_and_write_node, :global_variable_or_write_node,
+ :global_variable_operator_write_node, :instance_variable_write_node,
+ :instance_variable_and_write_node, :instance_variable_or_write_node,
+ :instance_variable_operator_write_node, :local_variable_write_node,
+ :local_variable_and_write_node, :local_variable_or_write_node,
+ :local_variable_operator_write_node, :multi_write_node
+ text("(")
+ yield
+ text(")")
+ else
+ yield
+ end
+ end
+
+ # Returns whether or not the given node should be indented when it is
+ # printed as the value of a write expression.
+ def indent_write?(node)
+ case node.type
+ when :array_node
+ node.opening_loc.nil?
+ when :hash_node, :lambda_node
+ false
+ when :string_node, :x_string_node, :interpolated_string_node, :interpolated_x_string_node
+ !node.heredoc?
+ when :call_node
+ node.receiver.nil? || indent_write?(node.receiver)
+ when :interpolated_symbol_node
+ node.opening_loc.nil? || !node.opening.start_with?("%s")
+ else
+ true
+ end
+ end
+
+ # If there is some part of the 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 quotes_locked?(parts)
+ parts.any? do |node|
+ !node.is_a?(StringNode) || node.content.match?(/\\|#[@${]|#{preferred_quote}/)
+ end
+ end
+
+ # Find the matching closing quote for the given opening quote.
+ def quotes_matching(quote)
+ case quote
+ when "("
+ ")"
+ when "["
+ "]"
+ when "{"
+ "}"
+ when "<"
+ ">"
+ else
+ quote
+ end
+ end
+
+ # Escape and unescape single and double quotes as needed to be able to
+ # enclose +content+ with +enclosing+.
+ def quotes_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
+
+ # --------------------------------------------------------------------------
+ # Visit helpers
+ # --------------------------------------------------------------------------
+
+ # Visit a node and format it, including any comments that are found around
+ # it that are attached to its location.
+ def visit(node)
+ stack << node
+
+ ignore = false
+ previous_line = nil
+
+ node.location.leading_comments.each do |comment|
+ slice = comment.slice
+
+ if previous_line
+ if (comment.location.start_line - previous_line) > 1
+ breakable_force
+ breakable_force
+ else
+ breakable_force
+ end
+ end
+
+ if comment.is_a?(InlineComment)
+ text(slice)
+ previous_line = comment.location.end_line
+ else
+ trim
+ text(slice.rstrip)
+ previous_line = comment.location.end_line - 1
+ end
+
+ ignore ||= slice.include?("stree-ignore")
+ end
+
+ if previous_line
+ if (node.location.start_line - previous_line) > 1
+ breakable_force
+ breakable_force
+ else
+ breakable_force
+ end
+ end
+
+ doc =
+ if ignore
+ # 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.
+ align(0) do
+ slice = node.slice
+ seplist(slice.each_line(chomp: true), -> { breakable_return }) do |line|
+ text(line)
+ end
+ breakable_return if slice.end_with?("\n")
+ end
+ else
+ node.accept(self)
+ end
+
+ node.location.trailing_comments.each { |comment| visit_comment(comment) }
+
+ stack.pop
+ doc
+ end
+
+ # Visit a binary expression, and format it with the given left and right
+ # nodes, and the given operator location.
+ def visit_binary(left, operator_loc, right)
+ group do
+ visit(left)
+ text(" ")
+ visit_location(operator_loc)
+ indent do
+ breakable_space
+ visit(right)
+ end
+ end
+ end
+
+ # Visit the body of a node, and format it with the given comments.
+ def visit_body(body, comments, force_break = true)
+ break_parent if force_break && (body || comments.any?)
+
+ indent do
+ if body
+ breakable_empty if !body.is_a?(BeginNode) || !body.statements.nil?
+ visit(body)
+ end
+
+ comments.each do |comment|
+ if comment.is_a?(InlineComment)
+ breakable_force
+ text(comment.location.slice)
+ else
+ breakable_force
+ trim
+ text(comment.location.slice.rstrip)
+ end
+ end
+ end
+ end
+
+ # Visit a comment and print it out.
+ def visit_comment(comment)
+ if !comment.is_a?(InlineComment)
+ breakable_force
+ trim
+ text(comment.location.slice.rstrip)
+ breakable_force
+ elsif comment.trailing?
+ line_suffix(COMMENT_PRIORITY) do
+ text(" ")
+ text(comment.location.slice)
+ break_parent
+ end
+ else
+ breakable_space
+ text(comment.location.slice)
+ break_parent
+ end
+ end
+
+ # Visit a set of elements within a collection, along with the comments that
+ # may be present within them.
+ def visit_elements(elements, comments)
+ indent do
+ if elements.any?
+ breakable_empty
+ seplist(elements) { |element| visit(element) }
+ if_break { text(",") } if trailing_comma
+ end
+
+ comments.each do |comment|
+ breakable_force
+ text(comment.location.slice)
+ end
+ end
+ end
+
+ # Visit a set of elements within a collection, along with the comments that
+ # may be present within them. Additionally add a space before the start and
+ # after the end of the collection.
+ def visit_elements_spaced(elements, comments)
+ indent do
+ if elements.any?
+ breakable_space
+ seplist(elements) { |element| visit(element) }
+ end
+
+ comments.each do |comment|
+ breakable_force
+ text(comment.slice)
+ end
+ end
+ end
+
+ # Visit a heredoc node, and format it with the given parts.
+ def visit_heredoc(node, parts)
+ # If the heredoc is indented, then we're going to need to reintroduce the
+ # indentation to the parts of the heredoc.
+ indent = parts.first.is_a?(StringNode) ? "" : parts.first.location.start_line_slice
+ opening = node.opening
+
+ if opening[2] == "~"
+ parts.each do |part|
+ if part.is_a?(StringNode) && !part.content.start_with?("\n")
+ indent = part.content[/\A[ \t]*/].delete_prefix(part.unescaped[/\A[ \t]*/])
+ break
+ end
+ end
+ end
+
+ group do
+ text(opening)
+ line_suffix(HEREDOC_PRIORITY) do
+ group do
+ target << BREAKABLE_RETURN
+
+ previous_newline = true
+ parts.each do |part|
+ case part.type
+ when :string_node, :x_string_node
+ value = part.content
+ seplist(value.each_line(chomp: true), -> { target << BREAKABLE_RETURN }) do |line|
+ text(line)
+ end
+
+ if (previous_newline = value.end_with?("\n"))
+ target << BREAKABLE_RETURN
+ end
+ else
+ text(indent) if previous_newline
+ visit(part)
+ previous_newline = false
+ end
+ end
+
+ text(node.closing.chomp)
+ end
+ end
+ end
+ end
+
+ # Visit a jump expression, which consists of a keyword followed by an
+ # optional set of arguments.
+ def visit_jump(keyword, arguments)
+ if !arguments
+ text(keyword)
+ elsif arguments.arguments.length == 1
+ argument = arguments.arguments.first
+
+ case argument.type
+ when :parentheses_node
+ body = argument.body
+
+ if body.is_a?(StatementsNode) && body.body.length == 1
+ case (first = body.body.first).type
+ when :class_variable_read_node, :constant_read_node, :false_node, :float_node,
+ :global_variable_read_node, :imaginary_node, :instance_variable_read_node,
+ :integer_node, :local_variable_read_node, :nil_node, :rational_node, :self_node,
+ :true_node
+ text("#{keyword} ")
+ visit(first)
+ when :array_node
+ if first.elements.length > 1
+ group do
+ text(keyword)
+ if_break { text(" [") }.if_flat { text(" ") }
+
+ indent do
+ breakable_empty
+ seplist(first.elements) { |element| visit(element) }
+ end
+
+ if_break do
+ breakable_empty
+ text("]")
+ end
+ end
+ else
+ text("#{keyword} ")
+ visit(first)
+ end
+ else
+ group do
+ text(keyword)
+ visit(argument)
+ end
+ end
+ else
+ group do
+ text(keyword)
+ visit(argument)
+ end
+ end
+ when :class_variable_read_node, :constant_read_node, :false_node, :float_node,
+ :global_variable_read_node, :imaginary_node, :instance_variable_read_node,
+ :integer_node, :local_variable_read_node, :nil_node, :rational_node, :self_node,
+ :true_node
+ text("#{keyword} ")
+ visit(argument)
+ when :array_node
+ if argument.elements.length > 1
+ group do
+ text(keyword)
+ if_break { text(" [") }.if_flat { text(" ") }
+
+ indent do
+ breakable_empty
+ seplist(argument.elements) { |element| visit(element) }
+ end
+
+ if_break do
+ breakable_empty
+ text("]")
+ end
+ end
+ else
+ text("#{keyword} ")
+ visit(argument)
+ end
+ else
+ group do
+ text(keyword)
+ if_break { text("(") }.if_flat { text(" ") }
+
+ indent do
+ breakable_empty
+ visit(argument)
+ end
+
+ if_break do
+ breakable_empty
+ text(")")
+ end
+ end
+ end
+ else
+ group do
+ text(keyword)
+ if_break { text(" [") }.if_flat { text(" ") }
+
+ indent do
+ breakable_empty
+ visit(arguments)
+ end
+
+ if_break do
+ breakable_empty
+ text("]")
+ end
+ end
+ end
+ end
+
+ # Print out a slice of the given location, and handle any attached trailing
+ # comments that may be present.
+ def visit_location(location, value = location.slice)
+ location.leading_comments.each do |comment|
+ if comment.is_a?(InlineComment)
+ text(comment.location.slice)
+ else
+ breakable_force
+ trim
+ text(comment.location.slice.rstrip)
+ end
+ breakable_force
+ end
+
+ text(value)
+ location.trailing_comments.each { |comment| visit_comment(comment) }
+ end
+
+ # Visit a prefix expression, which consists of a single operator prefixing
+ # a nested expression.
+ def visit_prefix(operator_loc, value)
+ if value
+ group do
+ visit_location(operator_loc)
+ align(operator_loc.length) { visit(value) }
+ end
+ else
+ visit_location(operator_loc)
+ end
+ end
+
+ # Visit the parts of a regular expression-like node.
+ def visit_regular_expression_node_parts(node, parts)
+ # 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/
+ ambiguous =
+ (
+ (part = parts.first) && part.is_a?(StringNode) && part.content.start_with?(" ", "=") &&
+ stack[0...-1].reverse_each.any? do |parent|
+ parent.is_a?(CallNode) && parent.arguments && parent.opening_loc.nil?
+ end
+ )
+
+ braces =
+ (ambiguous || parts.any? { |part| part.is_a?(StringNode) && part.content.include?("/") })
+
+ if braces && parts.any? { |part| part.is_a?(StringNode) && part.content.match?(/[{}]/) }
+ group do
+ text(node.opening)
+ parts.each { |part| visit(part) }
+ text(node.closing)
+ end
+ elsif braces
+ group do
+ text("%r{")
+
+ parts.each do |part|
+ if part.is_a?(StringNode)
+ seplist(part.content.each_line(chomp: true), -> { breakable_return }) do |line|
+ text(line.gsub(%r{(?(line_suffix) do
+ [-line_suffix.last.priority, -line_suffixes.index(line_suffix)]
+ end
+
+ # This is a linear stack instead of a mutually recursive call defined on
+ # the individual doc nodes for efficiency.
+ while (indent, mode, doc = commands.pop)
+ case doc.type
+ when :string
+ buffer << doc
+ position += doc.length
+ when :group
+ if mode == MODE_FLAT && !should_remeasure
+ next_mode = doc.break ? MODE_BREAK : MODE_FLAT
+ commands.concat(doc.contents.reverse.map { |part| [indent, next_mode, part] })
+ else
+ should_remeasure = false
+
+ if doc.break
+ commands.concat(doc.contents.reverse.map { |part| [indent, MODE_BREAK, part] })
+ else
+ next_commands = doc.contents.reverse.map { |part| [indent, MODE_FLAT, part] }
+
+ if fits?(next_commands, commands, print_width - position)
+ commands.concat(next_commands)
+ else
+ next_commands.each { |command| command[1] = MODE_BREAK }
+ commands.concat(next_commands)
+ end
+ end
+ end
+ when :breakable
+ if mode == MODE_FLAT
+ if doc.force
+ # This line was forced into the output even if we were in flat mode,
+ # so we need to tell the next group that no matter what, it needs to
+ # remeasure because the previous measurement didn't accurately
+ # capture the entire expression (this is necessary for nested
+ # groups).
+ should_remeasure = true
+ else
+ buffer << doc.separator
+ position += doc.width
+ next
+ end
+ end
+
+ # If there are any commands in the line suffix buffer, then we're going
+ # to flush them now, as we are about to add a newline.
+ if line_suffixes.any?
+ commands << [indent, mode, doc]
+
+ line_suffixes
+ .sort_by(&line_suffix_sort)
+ .each do |(indent, mode, doc)|
+ commands += doc.contents.reverse.map { |part| [indent, mode, part] }
+ end
+
+ line_suffixes.clear
+ next
+ end
+
+ if !doc.indent
+ buffer << "\n"
+ position = 0
+ else
+ position -= trim!(buffer)
+ buffer << "\n"
+ buffer << " " * indent
+ position = indent
+ end
+ when :indent
+ next_indent = indent + 2
+ commands.concat(doc.contents.reverse.map { |part| [next_indent, mode, part] })
+ when :align
+ next_indent = indent + doc.indent
+ commands.concat(doc.contents.reverse.map { |part| [next_indent, mode, part] })
+ when :trim
+ position -= trim!(buffer)
+ when :if_break
+ if mode == MODE_BREAK && doc.break_contents.any?
+ commands.concat(doc.break_contents.reverse.map { |part| [indent, mode, part] })
+ elsif mode == MODE_FLAT && doc.flat_contents.any?
+ commands.concat(doc.flat_contents.reverse.map { |part| [indent, mode, part] })
+ end
+ when :line_suffix
+ line_suffixes << [indent, mode, doc]
+ when :break_parent
+ # do nothing
+ else
+ # Special case where the user has defined some way to get an extra doc
+ # node that we don't explicitly support into the list. In this case
+ # we're going to assume it's 0-width and just append it to the output
+ # buffer.
+ #
+ # This is useful behavior for putting marker nodes into the list so that
+ # you can know how things are getting mapped before they get printed.
+ buffer << doc
+ end
+
+ if commands.empty? && line_suffixes.any?
+ line_suffixes
+ .sort_by(&line_suffix_sort)
+ .each do |(indent, mode, doc)|
+ commands.concat(doc.contents.reverse.map { |part| [indent, mode, part] })
+ end
+
+ line_suffixes.clear
+ end
+ end
+ end
+
+ # This method returns a boolean as to whether or not the remaining commands
+ # fit onto the remaining space on the current line. If we finish printing
+ # all of the commands or if we hit a newline, then we return true. Otherwise
+ # if we continue printing past the remaining space, we return false.
+ def fits?(next_commands, rest_commands, remaining)
+ # This is the index in the remaining commands that we've handled so far.
+ # We reverse through the commands and add them to the stack if we've run
+ # out of nodes to handle.
+ rest_index = rest_commands.length
+
+ # This is our stack of commands, very similar to the commands list in the
+ # print method.
+ commands = [*next_commands]
+
+ # This is our output buffer, really only necessary to keep track of
+ # because we could encounter a Trim doc node that would actually add
+ # remaining space.
+ fit_buffer = []
+
+ while remaining >= 0
+ if commands.empty?
+ return true if rest_index == 0
+
+ rest_index -= 1
+ commands << rest_commands[rest_index]
+ next
+ end
+
+ indent, mode, doc = commands.pop
+
+ case doc.type
+ when :string
+ fit_buffer << doc
+ remaining -= doc.length
+ when :group
+ next_mode = doc.break ? MODE_BREAK : mode
+ commands += doc.contents.reverse.map { |part| [indent, next_mode, part] }
+ when :breakable
+ if mode == MODE_FLAT && !doc.force
+ fit_buffer << doc.separator
+ remaining -= doc.width
+ next
+ end
+
+ return true
+ when :indent
+ next_indent = indent + 2
+ commands += doc.contents.reverse.map { |part| [next_indent, mode, part] }
+ when :align
+ next_indent = indent + doc.indent
+ commands += doc.contents.reverse.map { |part| [next_indent, mode, part] }
+ when :trim
+ remaining += trim!(fit_buffer)
+ when :if_break
+ if mode == MODE_BREAK && doc.break_contents.any?
+ commands += doc.break_contents.reverse.map { |part| [indent, mode, part] }
+ elsif mode == MODE_FLAT && doc.flat_contents.any?
+ commands += doc.flat_contents.reverse.map { |part| [indent, mode, part] }
+ end
+ end
+ end
+
+ false
+ end
+
+ def trim!(buffer)
+ return 0 if buffer.empty?
+
+ trimmed = 0
+
+ while buffer.any? && buffer.last.is_a?(String) && buffer.last.match?(/\A[\t ]*\z/)
+ trimmed += buffer.pop.length
+ end
+
+ if buffer.any? && buffer.last.is_a?(String) && !buffer.last.frozen?
+ length = buffer.last.length
+ buffer.last.gsub!(/[\t ]*\z/, "")
+ trimmed += length - buffer.last.length
+ end
+
+ trimmed
+ end
+
+ # --------------------------------------------------------------------------
+ # Helper node builders
+ # --------------------------------------------------------------------------
+
+ # This method calculates the position of the text relative to the current
+ # indentation level when the doc has been printed. It's useful for
+ # determining how to align text to doc nodes that are already built into the
+ # tree.
+ def last_position(node)
+ queue = [node]
+ width = 0
+
+ while (doc = queue.shift)
+ case doc.type
+ when :string
+ width += doc.length
+ when :group, :indent, :align
+ queue = doc.contents + queue
+ when :breakable
+ width = 0
+ when :if_break
+ queue = doc.break_contents + queue
+ end
+ end
+
+ width
+ end
+
+ # This method will remove any breakables from the list of contents so that
+ # no newlines are present in the output. If a newline is being forced into
+ # the output, the replace value will be used.
+ def remove_breaks(node, replace = "; ")
+ queue = [node]
+
+ while (doc = queue.shift)
+ case doc.type
+ when :align, :indent
+ doc.contents.map! { |child| remove_breaks_with(child, replace) }
+ queue.concat(doc.contents)
+ when :group
+ doc.unbreak!
+ doc.contents.map! { |child| remove_breaks_with(child, replace) }
+ queue.concat(doc.contents)
+ when :if_break
+ doc.flat_contents.map! { |child| remove_breaks_with(child, replace) }
+ queue.concat(doc.flat_contents)
+ end
+ end
+ end
+
+ # Remove breaks from a subtree with the given replacement string.
+ def remove_breaks_with(doc, replace)
+ case doc.type
+ when :breakable
+ doc.force ? replace : doc.separator
+ when :if_break
+ Align.new(0, doc.flat_contents)
+ else
+ doc
+ end
+ end
+
+ # Adds a separated list.
+ # The list is separated by comma with breakable space, by default.
+ #
+ # #seplist iterates the +list+ using +each+.
+ # It yields each object to the block given for #seplist.
+ # The procedure +separator_proc+ is called between each yields.
+ #
+ # If the iteration is zero times, +separator_proc+ is not called at all.
+ #
+ # If +separator_proc+ is nil or not given,
+ # +lambda { comma_breakable }+ is used.
+ #
+ # For example, following 3 code fragments has similar effect.
+ #
+ # q.seplist([1,2,3]) {|v| xxx v }
+ #
+ # q.seplist([1,2,3], lambda { q.comma_breakable }, :each) {|v| xxx v }
+ #
+ # xxx 1
+ # q.comma_breakable
+ # xxx 2
+ # q.comma_breakable
+ # xxx 3
+ def seplist(list, sep = nil)
+ first = true
+
+ list.each do |v|
+ if first
+ first = false
+ elsif sep
+ sep.call
+ else
+ comma_breakable
+ end
+
+ yield(v)
+ end
+ end
+
+ # --------------------------------------------------------------------------
+ # Markers node builders
+ # --------------------------------------------------------------------------
+
+ # This says "you can break a line here if necessary", and a +width+\-column
+ # text +separator+ is inserted if a line is not broken at the point.
+ #
+ # If +separator+ is not specified, ' ' is used.
+ #
+ # If +width+ is not specified, +separator.length+ is used. You will have to
+ # specify this when +separator+ is a multibyte character, for example.
+ #
+ # By default, if the surrounding group is broken and a newline is inserted,
+ # the printer will indent the subsequent line up to the current level of
+ # indentation. You can disable this behavior with the +indent+ argument if
+ # that's not desired (rare).
+ #
+ # By default, when you insert a Breakable into the print tree, it only
+ # breaks the surrounding group when the group's contents cannot fit onto the
+ # remaining space of the current line. You can force it to break the
+ # surrounding group instead if you always want the newline with the +force+
+ # argument.
+ #
+ # There are a few circumstances where you'll want to force the newline into
+ # the output but no insert a break parent (because you don't want to
+ # necessarily force the groups to break unless they need to). In this case
+ # you can pass `force: :skip_break_parent` to this method and it will not
+ # insert a break parent.
+
+ # The vast majority of breakable calls you receive while formatting are a
+ # space in flat mode and a newline in break mode. Since this is so common,
+ # we have a method here to skip past unnecessary calculation.
+ def breakable_space
+ target << BREAKABLE_SPACE
+ end
+
+ # Another very common breakable call you receive while formatting is an
+ # empty string in flat mode and a newline in break mode. Similar to
+ # breakable_space, this is here for avoid unnecessary calculation.
+ def breakable_empty
+ target << BREAKABLE_EMPTY
+ end
+
+ # The final of the very common breakable calls you receive while formatting
+ # is the normal breakable space but with the addition of the break_parent.
+ def breakable_force
+ target << BREAKABLE_FORCE
+ break_parent
+ end
+
+ # This is the same shortcut as breakable_force, except that it doesn't
+ # indent the next line. This is necessary if you're trying to preserve some
+ # custom formatting like a multi-line string.
+ def breakable_return
+ target << BREAKABLE_RETURN
+ break_parent
+ end
+
+ # This inserts a BreakParent node into the print tree which forces the
+ # surrounding and all parent group nodes to break.
+ def break_parent
+ doc = BREAK_PARENT
+ target << doc
+
+ groups.reverse_each do |group|
+ break if group.break
+ group.break!
+ end
+ end
+
+ # A convenience method which is same as follows:
+ #
+ # text(",")
+ # breakable
+ def comma_breakable
+ text(",")
+ breakable_space
+ end
+
+ # This inserts a Trim node into the print tree which, when printed, will
+ # clear all whitespace at the end of the output buffer. This is useful for
+ # the rare case where you need to delete printed indentation and force the
+ # next node to start at the beginning of the line.
+ def trim
+ target << TRIM
+ end
+
+ # --------------------------------------------------------------------------
+ # Container node builders
+ # --------------------------------------------------------------------------
+
+ # Increases left margin after newline with +indent+ for line breaks added in
+ # the block.
+ def align(indent)
+ contents = []
+ doc = Align.new(indent, contents)
+ target << doc
+
+ with_target(contents) { yield }
+ doc
+ end
+
+ # Groups line break hints added in the block. The line break hints are all
+ # to be used or not.
+ #
+ # If +indent+ is specified, the method call is regarded as nested by
+ # align(indent) { ... }.
+ #
+ # If +open_object+ is specified, text(open_object, open_width) is
+ # called before grouping. If +close_object+ is specified,
+ # text(close_object, close_width) is called after grouping.
+ def group
+ contents = []
+ doc = Group.new(contents)
+
+ groups << doc
+ target << doc
+
+ with_target(contents) { yield }
+ groups.pop
+
+ doc
+ end
+
+ # Group if the predicate is true, otherwise just yield the block.
+ def group_if(predicate)
+ if predicate
+ group { yield }
+ else
+ yield
+ end
+ end
+
+ # A small DSL-like object used for specifying the alternative contents to be
+ # printed if the surrounding group doesn't break for an IfBreak node.
+ class IfBreakBuilder
+ attr_reader :q, :flat_contents
+
+ def initialize(q, flat_contents)
+ @q = q
+ @flat_contents = flat_contents
+ end
+
+ def if_flat
+ q.with_target(flat_contents) { yield }
+ end
+ end
+
+ # When we already know that groups are broken, we don't actually need to
+ # track the flat versions of the contents. So this builder version is
+ # effectively a no-op, but we need it to maintain the same API. The only
+ # thing this can impact is that if there's a forced break in the flat
+ # contents, then we need to propagate that break up the whole tree.
+ class IfFlatIgnore
+ attr_reader :q
+
+ def initialize(q)
+ @q = q
+ end
+
+ def if_flat
+ contents = []
+ group = Group.new(contents)
+
+ q.with_target(contents) { yield }
+ q.break_parent if group.break
+ end
+ end
+
+ # Inserts an IfBreak node with the contents of the block being added to its
+ # list of nodes that should be printed if the surrounding node breaks. If it
+ # doesn't, then you can specify the contents to be printed with the #if_flat
+ # method used on the return object from this method. For example,
+ #
+ # q.if_break { q.text('do') }.if_flat { q.text('{') }
+ #
+ # In the example above, if the surrounding group is broken it will print
+ # 'do' and if it is not it will print '{'.
+ def if_break
+ break_contents = []
+ flat_contents = []
+
+ doc = IfBreak.new(break_contents, flat_contents)
+ target << doc
+
+ with_target(break_contents) { yield }
+
+ if groups.last.break
+ IfFlatIgnore.new(self)
+ else
+ IfBreakBuilder.new(self, flat_contents)
+ end
+ end
+
+ # This is similar to if_break in that it also inserts an IfBreak node into
+ # the print tree, however it's starting from the flat contents, and cannot
+ # be used to build the break contents.
+ def if_flat
+ if groups.last.break
+ contents = []
+ group = Group.new(contents)
+
+ with_target(contents) { yield }
+ break_parent if group.break
+ else
+ flat_contents = []
+ doc = IfBreak.new(break_contents, flat_contents)
+ target << doc
+
+ with_target(flat_contents) { yield }
+ doc
+ end
+ end
+
+ # Very similar to the #nest method, this indents the nested content by one
+ # level by inserting an Indent node into the print tree. The contents of the
+ # node are determined by the block.
+ def indent
+ contents = []
+ doc = Indent.new(contents)
+ target << doc
+
+ with_target(contents) { yield }
+ doc
+ end
+
+ # Inserts a LineSuffix node into the print tree. The contents of the node
+ # are determined by the block.
+ def line_suffix(priority)
+ contents = []
+ doc = LineSuffix.new(priority, contents)
+ target << doc
+
+ with_target(contents) { yield }
+ doc
+ end
+
+ # Push a value onto the output target.
+ def text(value)
+ target << value
+ end
+ end
+end
diff --git a/lib/syntax_tree/formatter.rb b/lib/syntax_tree/formatter.rb
deleted file mode 100644
index 2b229885..00000000
--- a/lib/syntax_tree/formatter.rb
+++ /dev/null
@@ -1,228 +0,0 @@
-# frozen_string_literal: true
-
-module SyntaxTree
- # A slightly enhanced PP that knows how to format recursively including
- # comments.
- class Formatter < PrettierPrint
- # Unfortunately, Gem::Version.new is not ractor-safe because it performs
- # global caching using a class variable. This works around that by just
- # setting the instance variables directly.
- class SemanticVersion < ::Gem::Version
- def initialize(version)
- @version = version
- @segments = nil
- end
- end
-
- # We want to minimize as much as possible the number of options that are
- # available in syntax tree. For the most part, if users want non-default
- # formatting, they should override the format methods on the specific nodes
- # themselves. However, because of some history with prettier and the fact
- # that folks have become entrenched in their ways, we decided to provide a
- # small amount of configurability.
- class Options
- attr_reader :quote,
- :trailing_comma,
- :disable_auto_ternary,
- :target_ruby_version
-
- def initialize(
- quote: :default,
- trailing_comma: :default,
- disable_auto_ternary: :default,
- target_ruby_version: :default
- )
- @quote =
- if quote == :default
- # We ship with a single quotes plugin that will define this
- # constant. That constant is responsible for determining the default
- # quote style. If it's defined, we default to single quotes,
- # otherwise we default to double quotes.
- defined?(SINGLE_QUOTES) ? "'" : "\""
- else
- quote
- end
-
- @trailing_comma =
- if trailing_comma == :default
- # We ship with a trailing comma plugin that will define this
- # constant. That constant is responsible for determining the default
- # trailing comma value. If it's defined, then we default to true.
- # Otherwise we default to false.
- defined?(TRAILING_COMMA)
- else
- trailing_comma
- end
-
- @disable_auto_ternary =
- if disable_auto_ternary == :default
- # We ship with a disable ternary plugin that will define this
- # constant. That constant is responsible for determining the default
- # disable ternary value. If it's defined, then we default to true.
- # Otherwise we default to false.
- defined?(DISABLE_AUTO_TERNARY)
- else
- disable_auto_ternary
- end
-
- @target_ruby_version =
- if target_ruby_version == :default
- # The default target Ruby version is the current version of Ruby.
- # This is really only used for very niche cases, and it shouldn't be
- # used by most users.
- SemanticVersion.new(RUBY_VERSION)
- else
- target_ruby_version
- end
- end
- end
-
- COMMENT_PRIORITY = 1
- HEREDOC_PRIORITY = 2
-
- attr_reader :source, :stack
-
- # These options are overridden in plugins to we need to make sure they are
- # available here.
- attr_reader :quote,
- :trailing_comma,
- :disable_auto_ternary,
- :target_ruby_version
-
- alias trailing_comma? trailing_comma
- alias disable_auto_ternary? disable_auto_ternary
-
- def initialize(source, *args, options: Options.new)
- super(*args)
-
- @source = source
- @stack = []
-
- # Memoizing these values to make access faster.
- @quote = options.quote
- @trailing_comma = options.trailing_comma
- @disable_auto_ternary = options.disable_auto_ternary
- @target_ruby_version = options.target_ruby_version
- end
-
- def self.format(source, node, base_indentation = 0)
- q = new(source, [])
- q.format(node)
- q.flush(base_indentation)
- q.output.join
- 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?
- trailing = []
- last_leading = nil
-
- # First, we're going to print all of the comments that were found before
- # the node. We'll also gather up any trailing comments that we find.
- node.comments.each do |comment|
- if comment.leading?
- comment.format(self)
- breakable(force: true)
- last_leading = comment
- else
- trailing << comment
- end
- 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.
- doc =
- if last_leading&.ignore?
- range = source[node.start_char...node.end_char]
- first = true
-
- range.each_line(chomp: true) do |line|
- if first
- first = false
- else
- breakable_return
- end
-
- text(line)
- end
-
- breakable_return if range.end_with?("\n")
- else
- node.format(self)
- end
-
- # Print all comments that were found after the node.
- trailing.each do |comment|
- line_suffix(priority: COMMENT_PRIORITY) do
- comment.inline? ? text(" ") : breakable
- 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 grandparent
- stack[-3]
- end
-
- def parent
- stack[-2]
- end
-
- def parents
- stack[0...-1].reverse_each
- end
-
- # This is a simplified version of prettyprint's group. It doesn't provide
- # any of the more advanced options because we don't need them and they take
- # up expensive computation time.
- def group
- contents = []
- doc = Group.new(0, contents: contents)
-
- groups << doc
- target << doc
-
- with_target(contents) { yield }
- groups.pop
- doc
- end
-
- # A similar version to the super, except that it calls back into the
- # separator proc with the instance of `self`.
- def seplist(list, sep = nil, iter_method = :each)
- first = true
- list.__send__(iter_method) do |*v|
- if first
- first = false
- elsif sep
- sep.call(self)
- else
- comma_breakable
- end
- yield(*v)
- end
- end
-
- # This is a much simplified version of prettyprint's text. It avoids
- # calculating width by pushing the string directly onto the target.
- def text(string)
- target << string
- end
- end
-end
diff --git a/lib/syntax_tree/language_server.rb b/lib/syntax_tree/language_server.rb
deleted file mode 100644
index 7d838a0c..00000000
--- a/lib/syntax_tree/language_server.rb
+++ /dev/null
@@ -1,163 +0,0 @@
-# frozen_string_literal: true
-
-require "cgi"
-require "json"
-require "pp"
-require "uri"
-
-module SyntaxTree
- # Syntax Tree additionally ships with a language server conforming to the
- # language server protocol. It can be invoked through the CLI by running:
- #
- # stree lsp
- #
- class LanguageServer
- # This is a small module that effectively mirrors pattern matching. We're
- # using it so that we can support truffleruby without having to ignore the
- # language server.
- module Request
- # Represents a hash pattern.
- class Shape
- attr_reader :values
-
- def initialize(values)
- @values = values
- end
-
- def ===(other)
- values.all? do |key, value|
- value == :any ? other.key?(key) : value === other[key]
- end
- end
- end
-
- # Represents an array pattern.
- class Tuple
- attr_reader :values
-
- def initialize(values)
- @values = values
- end
-
- def ===(other)
- values.each_with_index.all? { |value, index| value === other[index] }
- end
- end
-
- def self.[](value)
- case value
- when Array
- Tuple.new(value.map { |child| self[child] })
- when Hash
- Shape.new(value.transform_values { |child| self[child] })
- else
- value
- end
- end
- end
-
- attr_reader :input, :output, :print_width
-
- def initialize(
- input: $stdin,
- output: $stdout,
- print_width: DEFAULT_PRINT_WIDTH,
- ignore_files: []
- )
- @input = input.binmode
- @output = output.binmode
- @print_width = print_width
- @ignore_files = ignore_files
- end
-
- # rubocop:disable Layout/LineLength
- def run
- store =
- Hash.new do |hash, uri|
- filepath = CGI.unescape(URI.parse(uri).path)
- File.exist?(filepath) ? (hash[uri] = File.read(filepath)) : nil
- 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)
-
- # stree-ignore
- case request
- when Request[method: "initialize", id: :any]
- store.clear
- write(id: request[:id], result: { capabilities: capabilities })
- when Request[method: "initialized"]
- # ignored
- when Request[method: "shutdown"] # tolerate missing ID to be a good citizen
- store.clear
- write(id: request[:id], result: {})
- return
- when Request[method: "textDocument/didChange", params: { textDocument: { uri: :any }, contentChanges: [{ text: :any }] }]
- store[request.dig(:params, :textDocument, :uri)] = request.dig(:params, :contentChanges, 0, :text)
- when Request[method: "textDocument/didOpen", params: { textDocument: { uri: :any, text: :any } }]
- store[request.dig(:params, :textDocument, :uri)] = request.dig(:params, :textDocument, :text)
- when Request[method: "textDocument/didClose", params: { textDocument: { uri: :any } }]
- store.delete(request.dig(:params, :textDocument, :uri))
- when Request[method: "textDocument/formatting", id: :any, params: { textDocument: { uri: :any } }]
- uri = request.dig(:params, :textDocument, :uri)
- filepath = uri.split("///").last
- ignore = @ignore_files.any? do |glob|
- File.fnmatch(glob, filepath)
- end
- contents = store[uri]
- write(id: request[:id], result: contents && !ignore ? format(contents, uri.split(".").last) : nil)
- when Request[method: %r{\$/.+}]
- # ignored
- when Request[method: "textDocument/documentColor", params: { textDocument: { uri: :any } }]
- # ignored
- else
- raise ArgumentError, "Unhandled: #{request}"
- end
- end
- end
- # rubocop:enable Layout/LineLength
-
- private
-
- def capabilities
- {
- documentFormattingProvider: true,
- textDocumentSync: {
- change: 1,
- openClose: true
- }
- }
- end
-
- def format(source, extension)
- text = SyntaxTree::HANDLERS[".#{extension}"].format(source, print_width)
-
- [
- {
- range: {
- start: {
- line: 0,
- character: 0
- },
- end: {
- line: source.lines.size + 1,
- character: 0
- }
- },
- newText: text
- }
- ]
- rescue Parser::ParseError
- # If there is a parse error, then we're not going to return any formatting
- # changes for this source.
- nil
- 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/lsp.rb b/lib/syntax_tree/lsp.rb
new file mode 100644
index 00000000..e38e8166
--- /dev/null
+++ b/lib/syntax_tree/lsp.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+require "cgi/escape"
+require "json"
+require "uri"
+
+module SyntaxTree
+ # A language server conforming to the language server protocol. It can be
+ # invoked through the CLI by running:
+ #
+ # stree lsp
+ #
+ # rubocop:disable Layout/LineLength
+ class LSP
+ def initialize(input = $stdin, output = $stdout, options: SyntaxTree.options, ignore_files: [])
+ @input = input.binmode
+ @output = output.binmode.tap { |io| io.sync = true }
+
+ @options = options
+ @ignore_files = ignore_files
+ end
+
+ def run
+ store =
+ Hash.new do |hash, uri|
+ filepath = CGI.unescape(URI.parse(uri).path)
+ hash[uri] = File.read(filepath) if File.exist?(filepath)
+ end
+
+ # stree-ignore
+ while (headers = @input.gets("\r\n\r\n"))
+ body = @input.read(Integer(headers[/Content-Length: (\d+)/i, 1]))
+
+ case (request = JSON.parse(body, symbolize_names: true))[:method].to_sym
+ when :"textDocument/didChange"
+ request => { params: { textDocument: { uri: %r{\A.+//(.+\..+?)\z} => uri }, contentChanges: [{ text: }] } }
+ store[uri] = text unless ignored?($1)
+ when :"textDocument/didOpen"
+ request => { params: { textDocument: { uri: %r{\A.+//(.+\..+?)\z} => uri, text: } } }
+ store[uri] = text unless ignored?($1)
+ when :"textDocument/didClose"
+ request => { params: { textDocument: { uri: } } }
+ store.delete(uri)
+ when :"textDocument/formatting"
+ request => { params: { textDocument: { uri: %r{\A.+//(.+(\..+?))\z} => uri } } }
+ filepath = $1
+ extension = $2
+
+ write(
+ request[:id],
+ if (source = store[uri]) && !ignored?(filepath)
+ begin
+ [
+ {
+ newText: SyntaxTree.handler_for(extension).format(source, @options),
+ range: {
+ start: { line: 0, character: 0 },
+ end: { line: source.count("\n") + 1, character: 0 }
+ }
+ }
+ ]
+ rescue ParseError
+ end
+ end
+ )
+ when :initialize
+ store.clear
+
+ write(
+ request[:id],
+ {
+ capabilities: {
+ documentFormattingProvider: true,
+ textDocumentSync: { change: 1, openClose: true }
+ }
+ }
+ )
+ when :shutdown
+ store.clear
+
+ write(request[:id], {}) and break
+ when :initialized, :"textDocument/documentColor"
+ # ignored
+ else
+ unless request[:method].start_with?("$/")
+ raise ArgumentError, "Unknown method: #{request[:method]}"
+ end
+ end
+ end
+ end
+
+ private
+
+ def ignored?(filepath)
+ @ignore_files.any? { |pattern| File.fnmatch(pattern, filepath) }
+ end
+
+ def write(id, result)
+ response = { id: id, result: result, jsonrpc: "2.0" }.to_json
+ @output.print("Content-Length: #{response.bytesize}\r\n\r\n#{response}")
+ end
+ end
+ # rubocop:enable Layout/LineLength
+end
diff --git a/lib/syntax_tree/node.rb b/lib/syntax_tree/node.rb
deleted file mode 100644
index 495dc7b0..00000000
--- a/lib/syntax_tree/node.rb
+++ /dev/null
@@ -1,12380 +0,0 @@
-# frozen_string_literal: true
-
-module SyntaxTree
- # Represents the location of a node in the tree from the source code.
- class Location
- attr_reader :start_line,
- :start_char,
- :start_column,
- :end_line,
- :end_char,
- :end_column
-
- def initialize(
- start_line:,
- start_char:,
- start_column:,
- end_line:,
- end_char:,
- end_column:
- )
- @start_line = start_line
- @start_char = start_char
- @start_column = start_column
- @end_line = end_line
- @end_char = end_char
- @end_column = end_column
- end
-
- def lines
- start_line..end_line
- end
-
- def ==(other)
- other.is_a?(Location) && start_line == other.start_line &&
- start_char == other.start_char && end_line == other.end_line &&
- end_char == other.end_char
- end
-
- def to(other)
- Location.new(
- start_line: start_line,
- start_char: start_char,
- start_column: start_column,
- end_line: [end_line, other.end_line].max,
- end_char: other.end_char,
- end_column: other.end_column
- )
- end
-
- def deconstruct
- [start_line, start_char, start_column, end_line, end_char, end_column]
- end
-
- def deconstruct_keys(_keys)
- {
- start_line: start_line,
- start_char: start_char,
- start_column: start_column,
- end_line: end_line,
- end_char: end_char,
- end_column: end_column
- }
- end
-
- def self.token(line:, char:, column:, size:)
- new(
- start_line: line,
- start_char: char,
- start_column: column,
- end_line: line,
- end_char: char + size,
- end_column: column + size
- )
- end
-
- def self.fixed(line:, char:, column:)
- new(
- start_line: line,
- start_char: char,
- start_column: column,
- end_line: line,
- end_char: char,
- end_column: column
- )
- end
-
- # A convenience method that is typically used when you don't care about the
- # location of a node, but need to create a Location instance to pass to a
- # constructor.
- def self.default
- new(
- start_line: 1,
- start_char: 0,
- start_column: 0,
- end_line: 1,
- end_char: 0,
- end_column: 0
- )
- end
- end
-
- # This is the parent node of all of the syntax tree nodes. It's pretty much
- # exclusively here to make it easier to operate with the tree in cases where
- # you're trying to monkey-patch or strictly type.
- class Node
- # [Location] the location of this node
- attr_reader :location
-
- def accept(visitor)
- raise NotImplementedError
- end
-
- def child_nodes
- raise NotImplementedError
- end
-
- def deconstruct
- raise NotImplementedError
- end
-
- def deconstruct_keys(keys)
- raise NotImplementedError
- end
-
- def format(q)
- raise NotImplementedError
- end
-
- def start_char
- location.start_char
- end
-
- def end_char
- location.end_char
- end
- end
-
- # When we're implementing the === operator for a node, we oftentimes need to
- # compare two arrays. We want to skip over the === definition of array and use
- # our own here, so we do that using this module.
- module ArrayMatch
- def self.call(left, right)
- left.length === right.length &&
- left
- .zip(right)
- .all? { |left_value, right_value| left_value === right_value }
- end
- end
-
- # BEGINBlock represents the use of the +BEGIN+ keyword, which hooks into the
- # lifecycle of the interpreter. Whatever is inside the block will get executed
- # when the program starts.
- #
- # BEGIN {
- # }
- #
- # Interestingly, the BEGIN keyword doesn't allow the do and end keywords for
- # the block. Only braces are permitted.
- class BEGINBlock < Node
- # [LBrace] the left brace that is seen after the keyword
- attr_reader :lbrace
-
- # [Statements] the expressions to be executed
- attr_reader :statements
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(lbrace:, statements:, location:)
- @lbrace = lbrace
- @statements = statements
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_BEGIN(self)
- end
-
- def child_nodes
- [lbrace, statements]
- end
-
- def copy(lbrace: nil, statements: nil, location: nil)
- node =
- BEGINBlock.new(
- lbrace: lbrace || self.lbrace,
- statements: statements || self.statements,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- lbrace: lbrace,
- statements: statements,
- location: location,
- comments: comments
- }
- end
-
- def format(q)
- q.group do
- q.text("BEGIN ")
- q.format(lbrace)
- q.indent do
- q.breakable_space
- q.format(statements)
- end
- q.breakable_space
- q.text("}")
- end
- end
-
- def ===(other)
- other.is_a?(BEGINBlock) && lbrace === other.lbrace &&
- statements === other.statements
- end
- end
-
- # CHAR irepresents a single codepoint in the script encoding.
- #
- # ?a
- #
- # In the example above, the CHAR node represents the string literal "a". You
- # can use control characters with this as well, as in ?\C-a.
- class CHAR < Node
- # [String] the value of the character literal
- attr_reader :value
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:)
- @value = value
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_CHAR(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(value: nil, location: nil)
- node =
- CHAR.new(
- value: value || self.value,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location, comments: comments }
- end
-
- def format(q)
- if value.length != 2
- q.text(value)
- else
- q.text(q.quote)
- q.text(value[1] == q.quote ? "\\#{q.quote}" : value[1])
- q.text(q.quote)
- end
- end
-
- def ===(other)
- other.is_a?(CHAR) && value === other.value
- end
- end
-
- # ENDBlock represents the use of the +END+ keyword, which hooks into the
- # lifecycle of the interpreter. Whatever is inside the block will get executed
- # when the program ends.
- #
- # END {
- # }
- #
- # Interestingly, the END keyword doesn't allow the do and end keywords for the
- # block. Only braces are permitted.
- class ENDBlock < Node
- # [LBrace] the left brace that is seen after the keyword
- attr_reader :lbrace
-
- # [Statements] the expressions to be executed
- attr_reader :statements
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(lbrace:, statements:, location:)
- @lbrace = lbrace
- @statements = statements
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_END(self)
- end
-
- def child_nodes
- [lbrace, statements]
- end
-
- def copy(lbrace: nil, statements: nil, location: nil)
- node =
- ENDBlock.new(
- lbrace: lbrace || self.lbrace,
- statements: statements || self.statements,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- lbrace: lbrace,
- statements: statements,
- location: location,
- comments: comments
- }
- end
-
- def format(q)
- q.group do
- q.text("END ")
- q.format(lbrace)
- q.indent do
- q.breakable_space
- q.format(statements)
- end
- q.breakable_space
- q.text("}")
- end
- end
-
- def ===(other)
- other.is_a?(ENDBlock) && lbrace === other.lbrace &&
- statements === other.statements
- end
- end
-
- # EndContent represents the use of __END__ syntax, which allows individual
- # scripts to keep content after the main ruby code that can be read through
- # the DATA constant.
- #
- # puts DATA.read
- #
- # __END__
- # some other content that is not executed by the program
- #
- class EndContent < Node
- # [String] the content after the script
- attr_reader :value
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:)
- @value = value
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit___end__(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(value: nil, location: nil)
- node =
- EndContent.new(
- value: value || self.value,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location, comments: comments }
- end
-
- def format(q)
- q.text("__END__")
- q.breakable_force
-
- first = true
- value.each_line(chomp: true) do |line|
- if first
- first = false
- else
- q.breakable_return
- end
-
- q.text(line)
- end
-
- q.breakable_return if value.end_with?("\n")
- end
-
- def ===(other)
- other.is_a?(EndContent) && value === other.value
- end
- end
-
- # Alias represents the use of the +alias+ keyword with regular arguments (not
- # global variables). The +alias+ keyword is used to make a method respond to
- # another name as well as the current one.
- #
- # alias aliased_name name
- #
- # For the example above, in the current context you can now call aliased_name
- # and it will execute the name method. When you're aliasing two methods, you
- # can either provide bare words (like the example above) or you can provide
- # symbols (note that this includes dynamic symbols like
- # :"left-#{middle}-right").
- class AliasNode < Node
- # Formats an argument to the alias keyword. For symbol literals it uses the
- # value of the symbol directly to look like bare words.
- class AliasArgumentFormatter
- # [Backref | DynaSymbol | GVar | SymbolLiteral] the argument being passed
- # to alias
- attr_reader :argument
-
- def initialize(argument)
- @argument = argument
- end
-
- def comments
- if argument.is_a?(SymbolLiteral)
- argument.comments + argument.value.comments
- else
- argument.comments
- end
- end
-
- def format(q)
- if argument.is_a?(SymbolLiteral)
- q.format(argument.value)
- else
- q.format(argument)
- end
- end
- end
-
- # [DynaSymbol | GVar | SymbolLiteral] the new name of the method
- attr_reader :left
-
- # [Backref | DynaSymbol | GVar | SymbolLiteral] the old name of the method
- attr_reader :right
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(left:, right:, location:)
- @left = left
- @right = right
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_alias(self)
- end
-
- def child_nodes
- [left, right]
- end
-
- def copy(left: nil, right: nil, location: nil)
- node =
- AliasNode.new(
- left: left || self.left,
- right: right || self.right,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { left: left, right: right, location: location, comments: comments }
- end
-
- def format(q)
- keyword = "alias "
- left_argument = AliasArgumentFormatter.new(left)
-
- q.group do
- q.text(keyword)
- q.format(left_argument, stackable: false)
- q.group do
- q.nest(keyword.length) do
- left_argument.comments.any? ? q.breakable_force : q.breakable_space
- q.format(AliasArgumentFormatter.new(right), stackable: false)
- end
- end
- end
- end
-
- def ===(other)
- other.is_a?(AliasNode) && left === other.left && right === other.right
- end
- end
-
- # ARef represents when you're pulling a value out of a collection at a
- # specific index. Put another way, it's any time you're calling the method
- # #[].
- #
- # collection[index]
- #
- # The nodes usually contains two children, the collection and the index. In
- # some cases, you don't necessarily have the second child node, because you
- # can call procs with a pretty esoteric syntax. In the following example, you
- # wouldn't have a second child node:
- #
- # collection[]
- #
- class ARef < Node
- # [Node] the value being indexed
- attr_reader :collection
-
- # [nil | Args] the value being passed within the brackets
- attr_reader :index
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(collection:, index:, location:)
- @collection = collection
- @index = index
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_aref(self)
- end
-
- def child_nodes
- [collection, index]
- end
-
- def copy(collection: nil, index: nil, location: nil)
- node =
- ARef.new(
- collection: collection || self.collection,
- index: index || self.index,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- collection: collection,
- index: index,
- location: location,
- comments: comments
- }
- end
-
- def format(q)
- q.group do
- q.format(collection)
- q.text("[")
-
- if index
- q.indent do
- q.breakable_empty
- q.format(index)
- end
- q.breakable_empty
- end
-
- q.text("]")
- end
- end
-
- def ===(other)
- other.is_a?(ARef) && collection === other.collection &&
- index === other.index
- end
- end
-
- # ARefField represents assigning values into collections at specific indices.
- # Put another way, it's any time you're calling the method #[]=. The
- # ARefField node itself is just the left side of the assignment, and they're
- # always wrapped in assign nodes.
- #
- # collection[index] = value
- #
- class ARefField < Node
- # [Node] the value being indexed
- attr_reader :collection
-
- # [nil | Args] the value being passed within the brackets
- attr_reader :index
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(collection:, index:, location:)
- @collection = collection
- @index = index
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_aref_field(self)
- end
-
- def child_nodes
- [collection, index]
- end
-
- def copy(collection: nil, index: nil, location: nil)
- node =
- ARefField.new(
- collection: collection || self.collection,
- index: index || self.index,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- collection: collection,
- index: index,
- location: location,
- comments: comments
- }
- end
-
- def format(q)
- q.group do
- q.format(collection)
- q.text("[")
-
- if index
- q.indent do
- q.breakable_empty
- q.format(index)
- end
- q.breakable_empty
- end
-
- q.text("]")
- end
- end
-
- def ===(other)
- other.is_a?(ARefField) && collection === other.collection &&
- index === other.index
- end
- end
-
- # ArgParen represents wrapping arguments to a method inside a set of
- # parentheses.
- #
- # method(argument)
- #
- # In the example above, there would be an ArgParen node around the Args node
- # that represents the set of arguments being sent to the method method. The
- # argument child node can be +nil+ if no arguments were passed, as in:
- #
- # method()
- #
- class ArgParen < Node
- # [nil | Args | ArgsForward] the arguments inside the
- # parentheses
- attr_reader :arguments
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(arguments:, location:)
- @arguments = arguments
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_arg_paren(self)
- end
-
- def child_nodes
- [arguments]
- end
-
- def copy(arguments: nil, location: nil)
- node =
- ArgParen.new(
- arguments: arguments || self.arguments,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { arguments: arguments, location: location, comments: comments }
- end
-
- def format(q)
- unless arguments
- q.text("()")
- return
- end
-
- q.text("(")
- q.group do
- q.indent do
- q.breakable_empty
- q.format(arguments)
- q.if_break { q.text(",") } if q.trailing_comma? && trailing_comma?
- end
- q.breakable_empty
- end
- q.text(")")
- end
-
- def ===(other)
- other.is_a?(ArgParen) && arguments === other.arguments
- end
-
- def arity
- arguments&.arity || 0
- end
-
- private
-
- def trailing_comma?
- arguments = self.arguments
- return false unless arguments.is_a?(Args)
-
- parts = arguments.parts
- if parts.last.is_a?(ArgBlock)
- # If the last argument is a block, then we can't put a trailing comma
- # after it without resulting in a syntax error.
- false
- elsif (parts.length == 1) && (part = parts.first) &&
- (part.is_a?(Command) || part.is_a?(CommandCall))
- # If the only argument is a command or command call, then a trailing
- # comma would be parsed as part of that expression instead of on this
- # one, so we don't want to add a trailing comma.
- false
- else
- # Otherwise, we should be okay to add a trailing comma.
- true
- end
- end
- end
-
- # Args represents a list of arguments being passed to a method call or array
- # literal.
- #
- # method(first, second, third)
- #
- class Args < Node
- # [Array[ Node ]] the arguments that this node wraps
- attr_reader :parts
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(parts:, location:)
- @parts = parts
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_args(self)
- end
-
- def child_nodes
- parts
- end
-
- def copy(parts: nil, location: nil)
- node =
- Args.new(
- parts: parts || self.parts,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { parts: parts, location: location, comments: comments }
- end
-
- def format(q)
- q.seplist(parts) { |part| q.format(part) }
- end
-
- def ===(other)
- other.is_a?(Args) && ArrayMatch.call(parts, other.parts)
- end
-
- def arity
- parts.sum do |part|
- case part
- when ArgStar, ArgsForward
- Float::INFINITY
- when BareAssocHash
- part.assocs.sum do |assoc|
- assoc.is_a?(AssocSplat) ? Float::INFINITY : 1
- end
- else
- 1
- end
- end
- end
- end
-
- # ArgBlock represents using a block operator on an expression.
- #
- # method(&expression)
- #
- class ArgBlock < Node
- # [nil | Node] the expression being turned into a block
- attr_reader :value
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:)
- @value = value
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_arg_block(self)
- end
-
- def child_nodes
- [value]
- end
-
- def copy(value: nil, location: nil)
- node =
- ArgBlock.new(
- value: value || self.value,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location, comments: comments }
- end
-
- def format(q)
- q.text("&")
- q.format(value) if value
- end
-
- def ===(other)
- other.is_a?(ArgBlock) && value === other.value
- end
- end
-
- # Star represents using a splat operator on an expression.
- #
- # method(*arguments)
- #
- class ArgStar < Node
- # [nil | Node] the expression being splatted
- attr_reader :value
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:)
- @value = value
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_arg_star(self)
- end
-
- def child_nodes
- [value]
- end
-
- def copy(value: nil, location: nil)
- node =
- ArgStar.new(
- value: value || self.value,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location, comments: comments }
- end
-
- def format(q)
- q.text("*")
- q.format(value) if value
- end
-
- def ===(other)
- other.is_a?(ArgStar) && value === other.value
- end
- end
-
- # ArgsForward represents forwarding all kinds of arguments onto another method
- # call.
- #
- # def request(method, path, **headers, &block); end
- #
- # def get(...)
- # request(:GET, ...)
- # end
- #
- # def post(...)
- # request(:POST, ...)
- # end
- #
- # In the example above, both the get and post methods are forwarding all of
- # their arguments (positional, keyword, and block) on to the request method.
- # The ArgsForward node appears in both the caller (the request method calls)
- # and the callee (the get and post definitions).
- class ArgsForward < Node
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(location:)
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_args_forward(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(location: nil)
- node = ArgsForward.new(location: location || self.location)
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { location: location, comments: comments }
- end
-
- def format(q)
- q.text("...")
- end
-
- def ===(other)
- other.is_a?(ArgsForward)
- end
-
- def arity
- Float::INFINITY
- end
- end
-
- # ArrayLiteral represents an array literal, which can optionally contain
- # elements.
- #
- # []
- # [one, two, three]
- #
- class ArrayLiteral < Node
- # It's very common to use seplist with ->(q) { q.breakable_space }. We wrap
- # that pattern into an object to cut down on having to create a bunch of
- # lambdas all over the place.
- class BreakableSpaceSeparator
- def call(q)
- q.breakable_space
- end
- end
-
- BREAKABLE_SPACE_SEPARATOR = BreakableSpaceSeparator.new.freeze
-
- # Formats an array of multiple simple string literals into the %w syntax.
- class QWordsFormatter
- # [Args] the contents of the array
- attr_reader :contents
-
- def initialize(contents)
- @contents = contents
- end
-
- def format(q)
- q.text("%w[")
- q.group do
- q.indent do
- q.breakable_empty
- q.seplist(contents.parts, BREAKABLE_SPACE_SEPARATOR) do |part|
- if part.is_a?(StringLiteral)
- q.format(part.parts.first)
- else
- q.text(part.value[1..])
- end
- end
- end
- q.breakable_empty
- end
- q.text("]")
- end
- end
-
- # Formats an array of multiple simple symbol literals into the %i syntax.
- class QSymbolsFormatter
- # [Args] the contents of the array
- attr_reader :contents
-
- def initialize(contents)
- @contents = contents
- end
-
- def format(q)
- q.text("%i[")
- q.group do
- q.indent do
- q.breakable_empty
- q.seplist(contents.parts, BREAKABLE_SPACE_SEPARATOR) do |part|
- q.format(part.value)
- end
- end
- q.breakable_empty
- end
- q.text("]")
- end
- end
-
- # This is a special formatter used if the array literal contains no values
- # but _does_ contain comments. In this case we do some special formatting to
- # make sure the comments gets indented properly.
- class EmptyWithCommentsFormatter
- # [LBracket] the opening bracket
- attr_reader :lbracket
-
- def initialize(lbracket)
- @lbracket = lbracket
- end
-
- def format(q)
- q.group do
- q.text("[")
- q.indent do
- lbracket.comments.each do |comment|
- q.breakable_force
- comment.format(q)
- end
- end
- q.breakable_force
- q.text("]")
- end
- end
- end
-
- # [nil | LBracket | QSymbolsBeg | QWordsBeg | SymbolsBeg | WordsBeg] the
- # bracket that opens this array
- attr_reader :lbracket
-
- # [nil | Args] the contents of the array
- attr_reader :contents
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(lbracket:, contents:, location:)
- @lbracket = lbracket
- @contents = contents
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_array(self)
- end
-
- def child_nodes
- [lbracket, contents]
- end
-
- def copy(lbracket: nil, contents: nil, location: nil)
- node =
- ArrayLiteral.new(
- lbracket: lbracket || self.lbracket,
- contents: contents || self.contents,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- lbracket: lbracket,
- contents: contents,
- location: location,
- comments: comments
- }
- end
-
- def format(q)
- lbracket = self.lbracket
- contents = self.contents
-
- if lbracket.is_a?(LBracket) && lbracket.comments.empty? && contents &&
- contents.comments.empty? && contents.parts.length > 1
- if qwords?
- QWordsFormatter.new(contents).format(q)
- return
- end
-
- if qsymbols?
- QSymbolsFormatter.new(contents).format(q)
- return
- end
- end
-
- if empty_with_comments?
- EmptyWithCommentsFormatter.new(lbracket).format(q)
- return
- end
-
- q.group do
- q.format(lbracket)
-
- if contents
- q.indent do
- q.breakable_empty
- q.format(contents)
- q.if_break { q.text(",") } if q.trailing_comma?
- end
- end
-
- q.breakable_empty
- q.text("]")
- end
- end
-
- def ===(other)
- other.is_a?(ArrayLiteral) && lbracket === other.lbracket &&
- contents === other.contents
- end
-
- private
-
- def qwords?
- contents.parts.all? do |part|
- case part
- when StringLiteral
- part.comments.empty? && part.parts.length == 1 &&
- part.parts.first.is_a?(TStringContent) &&
- !part.parts.first.value.match?(/[\s\[\]\\]/)
- when CHAR
- !part.value.match?(/[\[\]\\]/)
- else
- false
- end
- end
- end
-
- def qsymbols?
- contents.parts.all? do |part|
- part.is_a?(SymbolLiteral) && part.comments.empty?
- end
- end
-
- # If we have an empty array that contains only comments, then we're going
- # to do some special printing to ensure they get indented correctly.
- def empty_with_comments?
- contents.nil? && lbracket.comments.any? &&
- lbracket.comments.none?(&:inline?)
- end
- end
-
- # AryPtn represents matching against an array pattern using the Ruby 2.7+
- # pattern matching syntax. It’s one of the more complicated nodes, because
- # the four parameters that it accepts can almost all be nil.
- #
- # case [1, 2, 3]
- # in [Integer, Integer]
- # "matched"
- # in Container[Integer, Integer]
- # "matched"
- # in [Integer, *, Integer]
- # "matched"
- # end
- #
- # An AryPtn node is created with four parameters: an optional constant
- # wrapper, an array of positional matches, an optional splat with identifier,
- # and an optional array of positional matches that occur after the splat.
- # All of the in clauses above would create an AryPtn node.
- class AryPtn < Node
- # Formats the optional splat of an array pattern.
- class RestFormatter
- # [VarField] the identifier that represents the remaining positionals
- attr_reader :value
-
- def initialize(value)
- @value = value
- end
-
- def comments
- value.comments
- end
-
- def format(q)
- q.text("*")
- q.format(value)
- end
- end
-
- # [nil | VarRef | ConstPathRef] the optional constant wrapper
- attr_reader :constant
-
- # [Array[ Node ]] the regular positional arguments that this array
- # pattern is matching against
- attr_reader :requireds
-
- # [nil | VarField] the optional starred identifier that grabs up a list of
- # positional arguments
- attr_reader :rest
-
- # [Array[ Node ]] the list of positional arguments occurring after the
- # optional star if there is one
- attr_reader :posts
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(constant:, requireds:, rest:, posts:, location:)
- @constant = constant
- @requireds = requireds
- @rest = rest
- @posts = posts
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_aryptn(self)
- end
-
- def child_nodes
- [constant, *requireds, rest, *posts]
- end
-
- def copy(
- constant: nil,
- requireds: nil,
- rest: nil,
- posts: nil,
- location: nil
- )
- node =
- AryPtn.new(
- constant: constant || self.constant,
- requireds: requireds || self.requireds,
- rest: rest || self.rest,
- posts: posts || self.posts,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- constant: constant,
- requireds: requireds,
- rest: rest,
- posts: posts,
- location: location,
- comments: comments
- }
- end
-
- def format(q)
- q.group do
- q.format(constant) if constant
- q.text("[")
- q.indent do
- q.breakable_empty
-
- parts = [*requireds]
- parts << RestFormatter.new(rest) if rest
- parts += posts
-
- q.seplist(parts) { |part| q.format(part) }
- end
- q.breakable_empty
- q.text("]")
- end
- end
-
- def ===(other)
- other.is_a?(AryPtn) && constant === other.constant &&
- ArrayMatch.call(requireds, other.requireds) && rest === other.rest &&
- ArrayMatch.call(posts, other.posts)
- end
- end
-
- # Determins if the following value should be indented or not.
- module AssignFormatting
- def self.skip_indent?(value)
- case value
- when ArrayLiteral, HashLiteral, Heredoc, Lambda, QSymbols, QWords,
- Symbols, Words
- true
- when CallNode
- skip_indent?(value.receiver)
- when DynaSymbol
- value.quote.start_with?("%s")
- else
- false
- end
- end
- end
-
- # Assign represents assigning something to a variable or constant. Generally,
- # the left side of the assignment is going to be any node that ends with the
- # name "Field".
- #
- # variable = value
- #
- class Assign < Node
- # [ARefField | ConstPathField | Field | TopConstField | VarField] the target
- # to assign the result of the expression to
- attr_reader :target
-
- # [Node] the expression to be assigned
- attr_reader :value
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(target:, value:, location:)
- @target = target
- @value = value
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_assign(self)
- end
-
- def child_nodes
- [target, value]
- end
-
- def copy(target: nil, value: nil, location: nil)
- node =
- Assign.new(
- target: target || self.target,
- value: value || self.value,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { target: target, value: value, location: location, comments: comments }
- end
-
- def format(q)
- q.group do
- q.format(target)
- q.text(" =")
-
- if skip_indent?
- q.text(" ")
- q.format(value)
- else
- q.indent do
- q.breakable_space
- q.format(value)
- end
- end
- end
- end
-
- def ===(other)
- other.is_a?(Assign) && target === other.target && value === other.value
- end
-
- private
-
- def skip_indent?
- target.comments.empty? &&
- (target.is_a?(ARefField) || AssignFormatting.skip_indent?(value))
- end
- end
-
- # Assoc represents a key-value pair within a hash. It is a child node of
- # either an AssocListFromArgs or a BareAssocHash.
- #
- # { key1: value1, key2: value2 }
- #
- # In the above example, the would be two Assoc nodes.
- class Assoc < Node
- # [Node] the key of this pair
- attr_reader :key
-
- # [nil | Node] the value of this pair
- attr_reader :value
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(key:, value:, location:)
- @key = key
- @value = value
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_assoc(self)
- end
-
- def child_nodes
- [key, value]
- end
-
- def copy(key: nil, value: nil, location: nil)
- node =
- Assoc.new(
- key: key || self.key,
- value: value || self.value,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { key: key, value: value, location: location, comments: comments }
- end
-
- def format(q)
- if value.is_a?(HashLiteral)
- format_contents(q)
- else
- q.group { format_contents(q) }
- end
- end
-
- def ===(other)
- other.is_a?(Assoc) && key === other.key && value === other.value
- end
-
- private
-
- def format_contents(q)
- (q.parent || HashKeyFormatter::Identity.new).format_key(q, key)
- return unless value
-
- if key.comments.empty? && AssignFormatting.skip_indent?(value)
- q.text(" ")
- q.format(value)
- else
- q.indent do
- q.breakable_space
- q.format(value)
- end
- end
- end
- end
-
- # AssocSplat represents double-splatting a value into a hash (either a hash
- # literal or a bare hash in a method call).
- #
- # { **pairs }
- #
- class AssocSplat < Node
- # [nil | Node] the expression that is being splatted
- attr_reader :value
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:)
- @value = value
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_assoc_splat(self)
- end
-
- def child_nodes
- [value]
- end
-
- def copy(value: nil, location: nil)
- node =
- AssocSplat.new(
- value: value || self.value,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location, comments: comments }
- end
-
- def format(q)
- q.text("**")
- q.format(value) if value
- end
-
- def ===(other)
- other.is_a?(AssocSplat) && value === other.value
- end
- end
-
- # Backref represents a global variable referencing a matched value. It comes
- # in the form of a $ followed by a positive integer.
- #
- # $1
- #
- class Backref < Node
- # [String] the name of the global backreference variable
- attr_reader :value
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:)
- @value = value
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_backref(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(value: nil, location: nil)
- node =
- Backref.new(
- value: value || self.value,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location, comments: comments }
- end
-
- def format(q)
- q.text(value)
- end
-
- def ===(other)
- other.is_a?(Backref) && value === other.value
- end
- end
-
- # Backtick represents the use of the ` operator. It's usually found being used
- # for an XStringLiteral, but could also be found as the name of a method being
- # defined.
- class Backtick < Node
- # [String] the backtick in the string
- attr_reader :value
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:)
- @value = value
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_backtick(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(value: nil, location: nil)
- node =
- Backtick.new(
- value: value || self.value,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location, comments: comments }
- end
-
- def format(q)
- q.text(value)
- end
-
- def ===(other)
- other.is_a?(Backtick) && value === other.value
- end
- end
-
- # This module is responsible for formatting the assocs contained within a
- # hash or bare hash. It first determines if every key in the hash can use
- # labels. If it can, it uses labels. Otherwise it uses hash rockets.
- module HashKeyFormatter
- # Formats the keys of a hash literal using labels.
- class Labels
- LABEL = /\A[A-Za-z_](\w*[\w!?])?\z/.freeze
-
- def format_key(q, key)
- case key
- when Label
- q.format(key)
- when SymbolLiteral
- q.format(key.value)
- q.text(":")
- when DynaSymbol
- parts = key.parts
-
- if parts.length == 1 && (part = parts.first) &&
- part.is_a?(TStringContent) && part.value.match?(LABEL)
- q.format(part)
- q.text(":")
- else
- q.format(key)
- q.text(":")
- end
- end
- end
- end
-
- # Formats the keys of a hash literal using hash rockets.
- class Rockets
- def format_key(q, key)
- case key
- when Label
- q.text(":#{key.value.chomp(":")}")
- when DynaSymbol
- q.text(":")
- q.format(key)
- else
- q.format(key)
- end
-
- q.text(" =>")
- end
- end
-
- # When formatting a single assoc node without the context of the parent
- # hash, this formatter is used. It uses whatever is present in the node,
- # because there is nothing to be consistent with.
- class Identity
- def format_key(q, key)
- if key.is_a?(Label)
- q.format(key)
- else
- q.format(key)
- q.text(" =>")
- end
- end
- end
-
- class << self
- def for(container)
- (assocs = container.assocs).each_with_index do |assoc, index|
- if assoc.is_a?(AssocSplat)
- # Splat nodes do not impact the formatting choice.
- elsif assoc.value.nil?
- # If the value is nil, then it has been omitted. In this case we
- # have to match the existing formatting because standardizing would
- # potentially break the code. For example:
- #
- # { first:, "second" => "value" }
- #
- return Identity.new
- else
- # Otherwise, we need to check the type of the key. If it's a label
- # or dynamic symbol, we can use labels. If it's a symbol literal
- # then it needs to match a certain pattern to be used as a label. If
- # it's anything else, then we need to use hash rockets.
- case assoc.key
- when Label, DynaSymbol
- # Here labels can be used.
- when SymbolLiteral
- # When attempting to convert a hash rocket into a hash label,
- # you need to take care because only certain patterns are
- # allowed. Ruby source says that they have to match keyword
- # arguments to methods, but don't specify what that is. After
- # some experimentation, it looks like it's:
- value = assoc.key.value.value
-
- if !value.match?(/^[_A-Za-z]/) || value.end_with?("=")
- if omitted_value?(assocs[(index + 1)..])
- return Identity.new
- else
- return Rockets.new
- end
- end
- else
- if omitted_value?(assocs[(index + 1)..])
- return Identity.new
- else
- return Rockets.new
- end
- end
- end
- end
-
- Labels.new
- end
-
- private
-
- def omitted_value?(assocs)
- assocs.any? { |assoc| !assoc.is_a?(AssocSplat) && assoc.value.nil? }
- end
- end
- end
-
- # BareAssocHash represents a hash of contents being passed as a method
- # argument (and therefore has omitted braces). It's very similar to an
- # AssocListFromArgs node.
- #
- # method(key1: value1, key2: value2)
- #
- class BareAssocHash < Node
- # [Array[ Assoc | AssocSplat ]]
- attr_reader :assocs
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(assocs:, location:)
- @assocs = assocs
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_bare_assoc_hash(self)
- end
-
- def child_nodes
- assocs
- end
-
- def copy(assocs: nil, location: nil)
- node =
- BareAssocHash.new(
- assocs: assocs || self.assocs,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { assocs: assocs, location: location, comments: comments }
- end
-
- def format(q)
- q.seplist(assocs) { |assoc| q.format(assoc) }
- end
-
- def ===(other)
- other.is_a?(BareAssocHash) && ArrayMatch.call(assocs, other.assocs)
- end
-
- def format_key(q, key)
- @key_formatter ||=
- case q.parents.take(3).last
- when Break, Next, ReturnNode
- HashKeyFormatter::Identity.new
- else
- HashKeyFormatter.for(self)
- end
-
- @key_formatter.format_key(q, key)
- end
- end
-
- # Begin represents a begin..end chain.
- #
- # begin
- # value
- # end
- #
- class Begin < Node
- # [BodyStmt] the bodystmt that contains the contents of this begin block
- attr_reader :bodystmt
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(bodystmt:, location:)
- @bodystmt = bodystmt
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_begin(self)
- end
-
- def child_nodes
- [bodystmt]
- end
-
- def copy(bodystmt: nil, location: nil)
- node =
- Begin.new(
- bodystmt: bodystmt || self.bodystmt,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { bodystmt: bodystmt, location: location, comments: comments }
- end
-
- def format(q)
- q.text("begin")
-
- unless bodystmt.empty?
- q.indent do
- q.breakable_force unless bodystmt.statements.empty?
- q.format(bodystmt)
- end
- end
-
- q.breakable_force
- q.text("end")
- end
-
- def ===(other)
- other.is_a?(Begin) && bodystmt === other.bodystmt
- end
- end
-
- # PinnedBegin represents a pinning a nested statement within pattern matching.
- #
- # case value
- # in ^(statement)
- # end
- #
- class PinnedBegin < Node
- # [Node] the expression being pinned
- attr_reader :statement
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(statement:, location:)
- @statement = statement
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_pinned_begin(self)
- end
-
- def child_nodes
- [statement]
- end
-
- def copy(statement: nil, location: nil)
- node =
- PinnedBegin.new(
- statement: statement || self.statement,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { statement: statement, location: location, comments: comments }
- end
-
- def format(q)
- q.group do
- q.text("^(")
- q.nest(1) do
- q.indent do
- q.breakable_empty
- q.format(statement)
- end
- q.breakable_empty
- q.text(")")
- end
- end
- end
-
- def ===(other)
- other.is_a?(PinnedBegin) && statement === other.statement
- end
- end
-
- # Binary represents any expression that involves two sub-expressions with an
- # operator in between. This can be something that looks like a mathematical
- # operation:
- #
- # 1 + 1
- #
- # but can also be something like pushing a value onto an array:
- #
- # array << value
- #
- class Binary < Node
- # Since Binary's operator is a symbol, it's better to use the `name` method
- # than to allocate a new string every time. This is a tiny performance
- # optimization, but enough that it shows up in the profiler. Adding this in
- # for older Ruby versions.
- unless :+.respond_to?(:name)
- using Module.new {
- refine Symbol do
- def name
- to_s.freeze
- end
- end
- }
- end
-
- # [Node] the left-hand side of the expression
- attr_reader :left
-
- # [Symbol] the operator used between the two expressions
- attr_reader :operator
-
- # [Node] the right-hand side of the expression
- attr_reader :right
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(left:, operator:, right:, location:)
- @left = left
- @operator = operator
- @right = right
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_binary(self)
- end
-
- def child_nodes
- [left, right]
- end
-
- def copy(left: nil, operator: nil, right: nil, location: nil)
- node =
- Binary.new(
- left: left || self.left,
- operator: operator || self.operator,
- right: right || self.right,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- left: left,
- operator: operator,
- right: right,
- location: location,
- comments: comments
- }
- end
-
- def format(q)
- left = self.left
- power = operator == :**
-
- q.group do
- q.group { q.format(left) }
- q.text(" ") unless power
-
- if operator != :<<
- q.group do
- q.text(operator.name)
- q.indent do
- power ? q.breakable_empty : q.breakable_space
- q.format(right)
- end
- end
- elsif left.is_a?(Binary) && left.operator == :<<
- q.group do
- q.text(operator.name)
- q.indent do
- power ? q.breakable_empty : q.breakable_space
- q.format(right)
- end
- end
- else
- q.text("<< ")
- q.format(right)
- end
- end
- end
-
- def ===(other)
- other.is_a?(Binary) && left === other.left &&
- operator === other.operator && right === other.right
- end
- end
-
- # BlockVar represents the parameters being declared for a block. Effectively
- # this node is everything contained within the pipes. This includes all of the
- # various parameter types, as well as block-local variable declarations.
- #
- # method do |positional, optional = value, keyword:, █ local|
- # end
- #
- class BlockVar < Node
- # [Params] the parameters being declared with the block
- attr_reader :params
-
- # [Array[ Ident ]] the list of block-local variable declarations
- attr_reader :locals
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(params:, locals:, location:)
- @params = params
- @locals = locals
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_block_var(self)
- end
-
- def child_nodes
- [params, *locals]
- end
-
- def copy(params: nil, locals: nil, location: nil)
- node =
- BlockVar.new(
- params: params || self.params,
- locals: locals || self.locals,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { params: params, locals: locals, location: location, comments: comments }
- end
-
- # Within the pipes of the block declaration, we don't want any spaces. So
- # we'll separate the parameters with a comma and space but no breakables.
- class Separator
- def call(q)
- q.text(", ")
- end
- end
-
- # We'll keep a single instance of this separator around for all block vars
- # to cut down on allocations.
- SEPARATOR = Separator.new.freeze
-
- def format(q)
- q.text("|")
- q.group do
- q.remove_breaks(q.format(params))
-
- if locals.any?
- q.text("; ")
- q.seplist(locals, SEPARATOR) { |local| q.format(local) }
- end
- end
- q.text("|")
- end
-
- def ===(other)
- other.is_a?(BlockVar) && params === other.params &&
- ArrayMatch.call(locals, other.locals)
- end
-
- # When a single required parameter is declared for a block, it gets
- # automatically expanded if the values being yielded into it are an array.
- def arg0?
- params.requireds.length == 1 && params.optionals.empty? &&
- params.rest.nil? && params.posts.empty? && params.keywords.empty? &&
- params.keyword_rest.nil? && params.block.nil?
- end
- end
-
- # BlockArg represents declaring a block parameter on a method definition.
- #
- # def method(&block); end
- #
- class BlockArg < Node
- # [nil | Ident] the name of the block argument
- attr_reader :name
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(name:, location:)
- @name = name
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_blockarg(self)
- end
-
- def child_nodes
- [name]
- end
-
- def copy(name: nil, location: nil)
- node =
- BlockArg.new(
- name: name || self.name,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { name: name, location: location, comments: comments }
- end
-
- def format(q)
- q.text("&")
- q.format(name) if name
- end
-
- def ===(other)
- other.is_a?(BlockArg) && name === other.name
- end
- end
-
- # bodystmt can't actually determine its bounds appropriately because it
- # doesn't necessarily know where it started. So the parent node needs to
- # report back down into this one where it goes.
- class BodyStmt < Node
- # [Statements] the list of statements inside the begin clause
- attr_reader :statements
-
- # [nil | Rescue] the optional rescue chain attached to the begin clause
- attr_reader :rescue_clause
-
- # [nil | Kw] the optional else keyword
- attr_reader :else_keyword
-
- # [nil | Statements] the optional set of statements inside the else clause
- attr_reader :else_clause
-
- # [nil | Ensure] the optional ensure clause
- attr_reader :ensure_clause
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(
- statements:,
- rescue_clause:,
- else_keyword:,
- else_clause:,
- ensure_clause:,
- location:
- )
- @statements = statements
- @rescue_clause = rescue_clause
- @else_keyword = else_keyword
- @else_clause = else_clause
- @ensure_clause = ensure_clause
- @location = location
- @comments = []
- end
-
- def bind(parser, start_char, start_column, end_char, end_column)
- rescue_clause = self.rescue_clause
-
- @location =
- Location.new(
- start_line: location.start_line,
- start_char: start_char,
- start_column: start_column,
- end_line: location.end_line,
- end_char: end_char,
- end_column: end_column
- )
-
- # Here we're going to determine the bounds for the statements
- consequent = rescue_clause || else_clause || ensure_clause
- statements.bind(
- parser,
- start_char,
- start_column,
- consequent ? consequent.location.start_char : end_char,
- consequent ? consequent.location.start_column : end_column
- )
-
- # Next we're going to determine the rescue clause if there is one
- if rescue_clause
- consequent = else_clause || ensure_clause
-
- rescue_clause.bind_end(
- consequent ? consequent.location.start_char : end_char,
- consequent ? consequent.location.start_column : end_column
- )
- end
- end
-
- def empty?
- statements.empty? && !rescue_clause && !else_clause && !ensure_clause
- end
-
- def accept(visitor)
- visitor.visit_bodystmt(self)
- end
-
- def child_nodes
- [statements, rescue_clause, else_keyword, else_clause, ensure_clause]
- end
-
- def copy(
- statements: nil,
- rescue_clause: nil,
- else_keyword: nil,
- else_clause: nil,
- ensure_clause: nil,
- location: nil
- )
- node =
- BodyStmt.new(
- statements: statements || self.statements,
- rescue_clause: rescue_clause || self.rescue_clause,
- else_keyword: else_keyword || self.else_keyword,
- else_clause: else_clause || self.else_clause,
- ensure_clause: ensure_clause || self.ensure_clause,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- statements: statements,
- rescue_clause: rescue_clause,
- else_keyword: else_keyword,
- else_clause: else_clause,
- ensure_clause: ensure_clause,
- location: location,
- comments: comments
- }
- end
-
- def format(q)
- q.group do
- q.format(statements) unless statements.empty?
-
- if rescue_clause
- q.nest(-2) do
- q.breakable_force
- q.format(rescue_clause)
- end
- end
-
- if else_clause
- q.nest(-2) do
- q.breakable_force
- q.format(else_keyword)
- end
-
- unless else_clause.empty?
- q.breakable_force
- q.format(else_clause)
- end
- end
-
- if ensure_clause
- q.nest(-2) do
- q.breakable_force
- q.format(ensure_clause)
- end
- end
- end
- end
-
- def ===(other)
- other.is_a?(BodyStmt) && statements === other.statements &&
- rescue_clause === other.rescue_clause &&
- else_keyword === other.else_keyword &&
- else_clause === other.else_clause &&
- ensure_clause === other.ensure_clause
- end
- end
-
- # Formats either a Break, Next, or Return node.
- class FlowControlFormatter
- # [String] the keyword to print
- attr_reader :keyword
-
- # [Break | Next | Return] the node being formatted
- attr_reader :node
-
- def initialize(keyword, node)
- @keyword = keyword
- @node = node
- end
-
- def format(q)
- # If there are no arguments associated with this flow control, then we can
- # safely just print the keyword and return.
- if node.arguments.nil?
- q.text(keyword)
- return
- end
-
- q.group do
- q.text(keyword)
-
- parts = node.arguments.parts
- length = parts.length
-
- if length == 0
- # Here there are no arguments at all, so we're not going to print
- # anything. This would be like if we had:
- #
- # break
- #
- elsif length >= 2
- # If there are multiple arguments, format them all. If the line is
- # going to break into multiple, then use brackets to start and end the
- # expression.
- format_arguments(q, " [", "]")
- else
- # If we get here, then we're formatting a single argument to the flow
- # control keyword.
- part = parts.first
-
- case part
- when Paren
- statements = part.contents.body
-
- if statements.length == 1
- statement = statements.first
-
- if statement.is_a?(ArrayLiteral)
- contents = statement.contents
-
- if contents && contents.parts.length >= 2
- # Here we have a single argument that is a set of parentheses
- # wrapping an array literal that has at least 2 elements.
- # We're going to print the contents of the array directly.
- # This would be like if we had:
- #
- # break([1, 2, 3])
- #
- # which we will print as:
- #
- # break 1, 2, 3
- #
- q.text(" ")
- format_array_contents(q, statement)
- else
- # Here we have a single argument that is a set of parentheses
- # wrapping an array literal that has 0 or 1 elements. We're
- # going to skip the parentheses but print the array itself.
- # This would be like if we had:
- #
- # break([1])
- #
- # which we will print as:
- #
- # break [1]
- #
- q.text(" ")
- q.format(statement)
- end
- elsif skip_parens?(statement)
- # Here we have a single argument that is a set of parentheses
- # that themselves contain a single statement. That statement is
- # a simple value that we can skip the parentheses for. This
- # would be like if we had:
- #
- # break(1)
- #
- # which we will print as:
- #
- # break 1
- #
- q.text(" ")
- q.format(statement)
- else
- # Here we have a single argument that is a set of parentheses.
- # We're going to print the parentheses themselves as if they
- # were the set of arguments. This would be like if we had:
- #
- # break(foo.bar)
- #
- q.format(part)
- end
- else
- q.format(part)
- end
- when ArrayLiteral
- contents = part.contents
-
- if contents && contents.parts.length >= 2
- # Here there is a single argument that is an array literal with at
- # least two elements. We skip directly into the array literal's
- # elements in order to print the contents. This would be like if
- # we had:
- #
- # break [1, 2, 3]
- #
- # which we will print as:
- #
- # break 1, 2, 3
- #
- q.text(" ")
- format_array_contents(q, part)
- else
- # Here there is a single argument that is an array literal with 0
- # or 1 elements. In this case we're going to print the array as it
- # is because skipping the brackets would change the remaining.
- # This would be like if we had:
- #
- # break []
- # break [1]
- #
- q.text(" ")
- q.format(part)
- end
- else
- # Here there is a single argument that hasn't matched one of our
- # previous cases. We're going to print the argument as it is. This
- # would be like if we had:
- #
- # break foo
- #
- format_arguments(q, "(", ")")
- end
- end
- end
- end
-
- private
-
- def format_array_contents(q, array)
- q.if_break { q.text("[") }
- q.indent do
- q.breakable_empty
- q.format(array.contents)
- end
- q.breakable_empty
- q.if_break { q.text("]") }
- end
-
- def format_arguments(q, opening, closing)
- q.if_break { q.text(opening) }
- q.indent do
- q.breakable_space
- q.format(node.arguments)
- end
- q.breakable_empty
- q.if_break { q.text(closing) }
- end
-
- def skip_parens?(node)
- case node
- when FloatLiteral, Imaginary, Int, RationalLiteral
- true
- when VarRef
- case node.value
- when Const, CVar, GVar, IVar, Kw
- true
- else
- false
- end
- else
- false
- end
- end
- end
-
- # Break represents using the +break+ keyword.
- #
- # break
- #
- # It can also optionally accept arguments, as in:
- #
- # break 1
- #
- class Break < Node
- # [Args] the arguments being sent to the keyword
- attr_reader :arguments
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(arguments:, location:)
- @arguments = arguments
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_break(self)
- end
-
- def child_nodes
- [arguments]
- end
-
- def copy(arguments: nil, location: nil)
- node =
- Break.new(
- arguments: arguments || self.arguments,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { arguments: arguments, location: location, comments: comments }
- end
-
- def format(q)
- FlowControlFormatter.new("break", self).format(q)
- end
-
- def ===(other)
- other.is_a?(Break) && arguments === other.arguments
- end
- end
-
- # Wraps a call operator (which can be a string literal :: or an Op node or a
- # Period node) and formats it when called.
- class CallOperatorFormatter
- # [:"::" | Op | Period] the operator being formatted
- attr_reader :operator
-
- def initialize(operator)
- @operator = operator
- end
-
- def comments
- operator == :"::" ? [] : operator.comments
- end
-
- def format(q)
- case operator
- when :"::"
- q.text(".")
- when Op
- operator.value == "::" ? q.text(".") : operator.format(q)
- else
- operator.format(q)
- end
- end
- end
-
- # This is probably the most complicated formatter in this file. It's
- # responsible for formatting chains of method calls, with or without arguments
- # or blocks. In general, we want to go from something like
- #
- # foo.bar.baz
- #
- # to
- #
- # foo
- # .bar
- # .baz
- #
- # Of course there are a lot of caveats to that, including trailing operators
- # when necessary, where comments are places, how blocks are aligned, etc.
- class CallChainFormatter
- # [CallNode | MethodAddBlock] the top of the call chain
- attr_reader :node
-
- def initialize(node)
- @node = node
- end
-
- def format(q)
- children = [node]
- threshold = 3
-
- # First, walk down the chain until we get to the point where we're not
- # longer at a chainable node.
- loop do
- case (child = children.last)
- when CallNode
- case (receiver = child.receiver)
- when CallNode
- if receiver.receiver.nil?
- break
- else
- children << receiver
- end
- when MethodAddBlock
- if (call = receiver.call).is_a?(CallNode) && !call.receiver.nil?
- children << receiver
- else
- break
- end
- else
- break
- end
- when MethodAddBlock
- if (call = child.call).is_a?(CallNode) && !call.receiver.nil?
- children << call
- else
- break
- end
- else
- break
- end
- end
-
- # Here, we have very specialized behavior where if we're within a sig
- # block, then we're going to assume we're creating a Sorbet type
- # signature. In that case, we really want the threshold to be lowered so
- # that we create method chains off of any two method calls within the
- # block. For more details, see
- # https://github.com/prettier/plugin-ruby/issues/863.
- parents = q.parents.take(4)
- if (parent = parents[2])
- # If we're at a block with the `do` keywords, then we want to go one
- # more level up. This is because do blocks have BodyStmt nodes instead
- # of just Statements nodes.
- parent = parents[3] if parent.is_a?(BlockNode) && parent.keywords?
-
- if parent.is_a?(MethodAddBlock) &&
- (call = parent.call).is_a?(CallNode) && call.message.value == "sig"
- threshold = 2
- end
- end
-
- if children.length >= threshold
- q.group do
- q
- .if_break { format_chain(q, children) }
- .if_flat { node.format_contents(q) }
- end
- else
- node.format_contents(q)
- end
- end
-
- def format_chain(q, children)
- # We're going to have some specialized behavior for if it's an entire
- # chain of calls without arguments except for the last one. This is common
- # enough in Ruby source code that it's worth the extra complexity here.
- empty_except_last =
- children
- .drop(1)
- .all? { |child| child.is_a?(CallNode) && child.arguments.nil? }
-
- # Here, we're going to add all of the children onto the stack of the
- # formatter so it's as if we had descending normally into them. This is
- # necessary so they can check their parents as normal.
- q.stack.concat(children)
- q.format(children.last.receiver) if children.last.receiver
-
- q.group do
- if attach_directly?(children.last)
- format_child(q, children.pop)
- q.stack.pop
- end
-
- q.indent do
- # We track another variable that checks if you need to move the
- # operator to the previous line in case there are trailing comments
- # and a trailing operator.
- skip_operator = false
-
- while (child = children.pop)
- if child.is_a?(CallNode)
- if (receiver = child.receiver).is_a?(CallNode) &&
- (receiver.message != :call) &&
- (receiver.message.value == "where") &&
- (child.message != :call && child.message.value == "not")
- # This is very specialized behavior wherein we group
- # .where.not calls together because it looks better. For more
- # information, see
- # https://github.com/prettier/plugin-ruby/issues/862.
- else
- # If we're at a Call node and not a MethodAddBlock node in the
- # chain then we're going to add a newline so it indents
- # properly.
- q.breakable_empty
- end
- end
-
- format_child(
- q,
- child,
- skip_comments: children.empty?,
- skip_operator: skip_operator,
- skip_attached: empty_except_last && children.empty?
- )
-
- # If the parent call node has a comment on the message then we need
- # to print the operator trailing in order to keep it working.
- last_child = children.last
- if last_child.is_a?(CallNode) && last_child.message != :call &&
- (
- (last_child.message.comments.any? && last_child.operator) ||
- (last_child.operator && last_child.operator.comments.any?)
- )
- q.format(CallOperatorFormatter.new(last_child.operator))
- skip_operator = true
- else
- skip_operator = false
- end
-
- # Pop off the formatter's stack so that it aligns with what would
- # have happened if we had been formatting normally.
- q.stack.pop
- end
- end
- end
-
- if empty_except_last
- case node
- when CallNode
- node.format_arguments(q)
- when MethodAddBlock
- q.format(node.block)
- end
- end
- end
-
- def self.chained?(node)
- return false if ENV["STREE_FAST_FORMAT"]
-
- case node
- when CallNode
- !node.receiver.nil?
- when MethodAddBlock
- call = node.call
- call.is_a?(CallNode) && !call.receiver.nil?
- else
- false
- end
- end
-
- private
-
- # For certain nodes, we want to attach directly to the end and don't
- # want to indent the first call. So we'll pop off the first children and
- # format it separately here.
- def attach_directly?(node)
- case node.receiver
- when ArrayLiteral, HashLiteral, Heredoc, IfNode, UnlessNode,
- XStringLiteral
- true
- else
- false
- end
- end
-
- def format_child(
- q,
- child,
- skip_comments: false,
- skip_operator: false,
- skip_attached: false
- )
- # First, format the actual contents of the child.
- case child
- when CallNode
- q.group do
- if !skip_operator && child.operator
- q.format(CallOperatorFormatter.new(child.operator))
- end
- q.format(child.message) if child.message != :call
- child.format_arguments(q) unless skip_attached
- end
- when MethodAddBlock
- q.format(child.block) unless skip_attached
- end
-
- # If there are any comments on this node then we need to explicitly print
- # them out here since we're bypassing the normal comment printing.
- if child.comments.any? && !skip_comments
- child.comments.each do |comment|
- comment.inline? ? q.text(" ") : q.breakable_space
- comment.format(q)
- end
-
- q.break_parent
- end
- end
- end
-
- # CallNode represents a method call.
- #
- # receiver.message
- #
- class CallNode < Node
- # [nil | Node] the receiver of the method call
- attr_reader :receiver
-
- # [nil | :"::" | Op | Period] the operator being used to send the message
- attr_reader :operator
-
- # [:call | Backtick | Const | Ident | Op] the message being sent
- attr_reader :message
-
- # [nil | ArgParen | Args] the arguments to the method call
- attr_reader :arguments
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(receiver:, operator:, message:, arguments:, location:)
- @receiver = receiver
- @operator = operator
- @message = message
- @arguments = arguments
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_call(self)
- end
-
- def child_nodes
- [
- receiver,
- (operator if operator != :"::"),
- (message if message != :call),
- arguments
- ]
- end
-
- def copy(
- receiver: nil,
- operator: nil,
- message: nil,
- arguments: nil,
- location: nil
- )
- node =
- CallNode.new(
- receiver: receiver || self.receiver,
- operator: operator || self.operator,
- message: message || self.message,
- arguments: arguments || self.arguments,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- receiver: receiver,
- operator: operator,
- message: message,
- arguments: arguments,
- location: location,
- comments: comments
- }
- end
-
- def format(q)
- if receiver
- # If we're at the top of a call chain, then we're going to do some
- # specialized printing in case we can print it nicely. We _only_ do this
- # at the top of the chain to avoid weird recursion issues.
- if CallChainFormatter.chained?(receiver) &&
- !CallChainFormatter.chained?(q.parent)
- q.group do
- q
- .if_break { CallChainFormatter.new(self).format(q) }
- .if_flat { format_contents(q) }
- end
- else
- format_contents(q)
- end
- else
- q.format(message)
-
- # Note that this explicitly leaves parentheses in place even if they are
- # empty. There are two reasons we would need to do this. The first is if
- # we're calling something that looks like a constant, as in:
- #
- # Foo()
- #
- # In this case if we remove the parentheses then this becomes a constant
- # reference and not a method call. The second is if we're calling a
- # method that is the same name as a local variable that is in scope, as
- # in:
- #
- # foo = foo()
- #
- # In this case we have to keep the parentheses or else it treats this
- # like assigning nil to the local variable. Note that we could attempt
- # to be smarter about this by tracking the local variables that are in
- # scope, but for now it's simpler and more efficient to just leave the
- # parentheses in place.
- q.format(arguments) if arguments
- end
- end
-
- def ===(other)
- other.is_a?(CallNode) && receiver === other.receiver &&
- operator === other.operator && message === other.message &&
- arguments === other.arguments
- end
-
- # Print out the arguments to this call. If there are no arguments, then do
- # nothing.
- def format_arguments(q)
- case arguments
- when ArgParen
- q.format(arguments)
- when Args
- q.text(" ")
- q.format(arguments)
- end
- end
-
- def format_contents(q)
- call_operator = CallOperatorFormatter.new(operator)
-
- q.group do
- q.format(receiver)
-
- # If there are trailing comments on the call operator, then we need to
- # use the trailing form as opposed to the leading form.
- q.format(call_operator) if call_operator.comments.any?
-
- q.group do
- q.indent do
- if receiver.comments.any? || call_operator.comments.any?
- q.breakable_force
- end
-
- if call_operator.comments.empty?
- q.format(call_operator, stackable: false)
- end
-
- q.format(message) if message != :call
- end
-
- format_arguments(q)
- end
- end
- end
-
- def arity
- arguments&.arity || 0
- end
- end
-
- # Case represents the beginning of a case chain.
- #
- # case value
- # when 1
- # "one"
- # when 2
- # "two"
- # else
- # "number"
- # end
- #
- class Case < Node
- # [Kw] the keyword that opens this expression
- attr_reader :keyword
-
- # [nil | Node] optional value being switched on
- attr_reader :value
-
- # [In | When] the next clause in the chain
- attr_reader :consequent
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(keyword:, value:, consequent:, location:)
- @keyword = keyword
- @value = value
- @consequent = consequent
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_case(self)
- end
-
- def child_nodes
- [keyword, value, consequent]
- end
-
- def copy(keyword: nil, value: nil, consequent: nil, location: nil)
- node =
- Case.new(
- keyword: keyword || self.keyword,
- value: value || self.value,
- consequent: consequent || self.consequent,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- keyword: keyword,
- value: value,
- consequent: consequent,
- location: location,
- comments: comments
- }
- end
-
- def format(q)
- q.group do
- q.format(keyword)
-
- if value
- q.text(" ")
- q.format(value)
- end
-
- q.breakable_force
- q.format(consequent)
- q.breakable_force
-
- q.text("end")
- end
- end
-
- def ===(other)
- other.is_a?(Case) && keyword === other.keyword && value === other.value &&
- consequent === other.consequent
- end
- end
-
- # RAssign represents a single-line pattern match.
- #
- # value in pattern
- # value => pattern
- #
- class RAssign < Node
- # [Node] the left-hand expression
- attr_reader :value
-
- # [Kw | Op] the operator being used to match against the pattern, which is
- # either => or in
- attr_reader :operator
-
- # [Node] the pattern on the right-hand side of the expression
- attr_reader :pattern
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, operator:, pattern:, location:)
- @value = value
- @operator = operator
- @pattern = pattern
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_rassign(self)
- end
-
- def child_nodes
- [value, operator, pattern]
- end
-
- def copy(value: nil, operator: nil, pattern: nil, location: nil)
- node =
- RAssign.new(
- value: value || self.value,
- operator: operator || self.operator,
- pattern: pattern || self.pattern,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- value: value,
- operator: operator,
- pattern: pattern,
- location: location,
- comments: comments
- }
- end
-
- def format(q)
- q.group do
- q.format(value)
- q.text(" ")
- q.format(operator)
-
- case pattern
- when AryPtn, FndPtn, HshPtn
- q.text(" ")
- q.format(pattern)
- else
- q.group do
- q.indent do
- q.breakable_space
- q.format(pattern)
- end
- end
- end
- end
- end
-
- def ===(other)
- other.is_a?(RAssign) && value === other.value &&
- operator === other.operator && pattern === other.pattern
- end
- end
-
- # Class represents defining a class using the +class+ keyword.
- #
- # class Container
- # end
- #
- # Classes can have path names as their class name in case it's being nested
- # under a namespace, as in:
- #
- # class Namespace::Container
- # end
- #
- # Classes can also be defined as a top-level path, in the case that it's
- # already in a namespace but you want to define it at the top-level instead,
- # as in:
- #
- # module OtherNamespace
- # class ::Namespace::Container
- # end
- # end
- #
- # All of these declarations can also have an optional superclass reference, as
- # in:
- #
- # class Child < Parent
- # end
- #
- # That superclass can actually be any Ruby expression, it doesn't necessarily
- # need to be a constant, as in:
- #
- # class Child < method
- # end
- #
- class ClassDeclaration < Node
- # [ConstPathRef | ConstRef | TopConstRef] the name of the class being
- # defined
- attr_reader :constant
-
- # [nil | Node] the optional superclass declaration
- attr_reader :superclass
-
- # [BodyStmt] the expressions to execute within the context of the class
- attr_reader :bodystmt
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(constant:, superclass:, bodystmt:, location:)
- @constant = constant
- @superclass = superclass
- @bodystmt = bodystmt
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_class(self)
- end
-
- def child_nodes
- [constant, superclass, bodystmt]
- end
-
- def copy(constant: nil, superclass: nil, bodystmt: nil, location: nil)
- node =
- ClassDeclaration.new(
- constant: constant || self.constant,
- superclass: superclass || self.superclass,
- bodystmt: bodystmt || self.bodystmt,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- constant: constant,
- superclass: superclass,
- bodystmt: bodystmt,
- location: location,
- comments: comments
- }
- end
-
- def format(q)
- if bodystmt.empty?
- q.group do
- format_declaration(q)
- q.breakable_force
- q.text("end")
- end
- else
- q.group do
- format_declaration(q)
-
- q.indent do
- q.breakable_force
- q.format(bodystmt)
- end
-
- q.breakable_force
- q.text("end")
- end
- end
- end
-
- def ===(other)
- other.is_a?(ClassDeclaration) && constant === other.constant &&
- superclass === other.superclass && bodystmt === other.bodystmt
- end
-
- private
-
- def format_declaration(q)
- q.group do
- q.text("class ")
- q.format(constant)
-
- if superclass
- q.text(" < ")
- q.format(superclass)
- end
- end
- end
- end
-
- # Comma represents the use of the , operator.
- class Comma < Node
- # [String] the comma in the string
- attr_reader :value
-
- def initialize(value:, location:)
- @value = value
- @location = location
- end
-
- def accept(visitor)
- visitor.visit_comma(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(value: nil, location: nil)
- Comma.new(value: value || self.value, location: location || self.location)
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location }
- end
-
- def ===(other)
- other.is_a?(Comma) && value === other.value
- end
- end
-
- # Command represents a method call with arguments and no parentheses. Note
- # that Command nodes only happen when there is no explicit receiver for this
- # method.
- #
- # method argument
- #
- class Command < Node
- # [Const | Ident] the message being sent to the implicit receiver
- attr_reader :message
-
- # [Args] the arguments being sent with the message
- attr_reader :arguments
-
- # [nil | BlockNode] the optional block being passed to the method
- attr_reader :block
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(message:, arguments:, block:, location:)
- @message = message
- @arguments = arguments
- @block = block
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_command(self)
- end
-
- def child_nodes
- [message, arguments, block]
- end
-
- def copy(message: nil, arguments: nil, block: nil, location: nil)
- node =
- Command.new(
- message: message || self.message,
- arguments: arguments || self.arguments,
- block: block || self.block,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- message: message,
- arguments: arguments,
- block: block,
- location: location,
- comments: comments
- }
- end
-
- def format(q)
- q.group do
- q.format(message)
- align(q, self) { q.format(arguments) }
- end
-
- q.format(block) if block
- end
-
- def ===(other)
- other.is_a?(Command) && message === other.message &&
- arguments === other.arguments && block === other.block
- end
-
- def arity
- arguments.arity
- end
-
- private
-
- def align(q, node, &block)
- arguments = node.arguments
-
- if arguments.is_a?(Args)
- parts = arguments.parts
-
- if parts.size == 1
- part = parts.first
-
- case part
- when DefNode
- q.text(" ")
- yield
- when IfOp
- q.if_flat { q.text(" ") }
- yield
- when Command
- align(q, part, &block)
- else
- q.text(" ")
- q.nest(message.value.length + 1) { yield }
- end
- else
- q.text(" ")
- q.nest(message.value.length + 1) { yield }
- end
- else
- q.text(" ")
- q.nest(message.value.length + 1) { yield }
- end
- end
- end
-
- # CommandCall represents a method call on an object with arguments and no
- # parentheses.
- #
- # object.method argument
- #
- class CommandCall < Node
- # [nil | Node] the receiver of the message
- attr_reader :receiver
-
- # [nil | :"::" | Op | Period] the operator used to send the message
- attr_reader :operator
-
- # [:call | Const | Ident | Op] the message being send
- attr_reader :message
-
- # [nil | Args | ArgParen] the arguments going along with the message
- attr_reader :arguments
-
- # [nil | BlockNode] the block associated with this method call
- attr_reader :block
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(
- receiver:,
- operator:,
- message:,
- arguments:,
- block:,
- location:
- )
- @receiver = receiver
- @operator = operator
- @message = message
- @arguments = arguments
- @block = block
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_command_call(self)
- end
-
- def child_nodes
- [receiver, message, arguments, block]
- end
-
- def copy(
- receiver: nil,
- operator: nil,
- message: nil,
- arguments: nil,
- block: nil,
- location: nil
- )
- node =
- CommandCall.new(
- receiver: receiver || self.receiver,
- operator: operator || self.operator,
- message: message || self.message,
- arguments: arguments || self.arguments,
- block: block || self.block,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- receiver: receiver,
- operator: operator,
- message: message,
- arguments: arguments,
- block: block,
- location: location,
- comments: comments
- }
- end
-
- def format(q)
- message = self.message
- arguments = self.arguments
- block = self.block
-
- q.group do
- doc =
- q.nest(0) do
- q.format(receiver)
-
- # If there are leading comments on the message then we know we have
- # a newline in the source that is forcing these things apart. In
- # this case we will have to use a trailing operator.
- if message != :call && message.comments.any?(&:leading?)
- q.format(CallOperatorFormatter.new(operator), stackable: false)
- q.indent do
- q.breakable_empty
- q.format(message)
- end
- else
- q.format(CallOperatorFormatter.new(operator), stackable: false)
- q.format(message)
- end
- end
-
- # Format the arguments for this command call here. If there are no
- # arguments, then print nothing.
- if arguments
- parts = arguments.parts
-
- if parts.length == 1 && parts.first.is_a?(IfOp)
- q.if_flat { q.text(" ") }
- q.format(arguments)
- else
- q.text(" ")
- q.nest(argument_alignment(q, doc)) { q.format(arguments) }
- end
- end
- end
-
- q.format(block) if block
- end
-
- def ===(other)
- other.is_a?(CommandCall) && receiver === other.receiver &&
- operator === other.operator && message === other.message &&
- arguments === other.arguments && block === other.block
- end
-
- def arity
- arguments&.arity || 0
- end
-
- private
-
- def argument_alignment(q, doc)
- # Very special handling case for rspec matchers. In general with rspec
- # matchers you expect to see something like:
- #
- # expect(foo).to receive(:bar).with(
- # 'one',
- # 'two',
- # 'three',
- # 'four',
- # 'five'
- # )
- #
- # In this case the arguments are aligned to the left side as opposed to
- # being aligned with the `receive` call.
- if %w[to not_to to_not].include?(message.value)
- 0
- else
- width = q.last_position(doc) + 1
- width > (q.maxwidth / 2) ? 0 : width
- end
- end
- end
-
- # Comment represents a comment in the source.
- #
- # # comment
- #
- class Comment < Node
- # [String] the contents of the comment
- attr_reader :value
-
- # [boolean] whether or not there is code on the same line as this comment.
- # If there is, then inline will be true.
- attr_reader :inline
- alias inline? inline
-
- def initialize(value:, inline:, location:)
- @value = value
- @inline = inline
- @location = location
-
- @leading = false
- @trailing = false
- end
-
- def leading!
- @leading = true
- end
-
- def leading?
- @leading
- end
-
- def trailing!
- @trailing = true
- end
-
- def trailing?
- @trailing
- end
-
- def ignore?
- value.match?(/\A#\s*stree-ignore\s*\z/)
- end
-
- def comments
- []
- end
-
- def accept(visitor)
- visitor.visit_comment(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(value: nil, inline: nil, location: nil)
- Comment.new(
- value: value || self.value,
- inline: inline || self.inline,
- location: location || self.location
- )
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, inline: inline, location: location }
- end
-
- def format(q)
- q.text(value)
- end
-
- def ===(other)
- other.is_a?(Comment) && value === other.value && inline === other.inline
- end
- end
-
- # Const represents a literal value that _looks_ like a constant. This could
- # actually be a reference to a constant:
- #
- # Constant
- #
- # It could also be something that looks like a constant in another context, as
- # in a method call to a capitalized method:
- #
- # object.Constant
- #
- # or a symbol that starts with a capital letter:
- #
- # :Constant
- #
- class Const < Node
- # [String] the name of the constant
- attr_reader :value
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:)
- @value = value
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_const(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(value: nil, location: nil)
- node =
- Const.new(
- value: value || self.value,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location, comments: comments }
- end
-
- def format(q)
- q.text(value)
- end
-
- def ===(other)
- other.is_a?(Const) && value === other.value
- end
- end
-
- # ConstPathField represents the child node of some kind of assignment. It
- # represents when you're assigning to a constant that is being referenced as
- # a child of another variable.
- #
- # object::Const = value
- #
- class ConstPathField < Node
- # [Node] the source of the constant
- attr_reader :parent
-
- # [Const] the constant itself
- attr_reader :constant
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(parent:, constant:, location:)
- @parent = parent
- @constant = constant
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_const_path_field(self)
- end
-
- def child_nodes
- [parent, constant]
- end
-
- def copy(parent: nil, constant: nil, location: nil)
- node =
- ConstPathField.new(
- parent: parent || self.parent,
- constant: constant || self.constant,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- parent: parent,
- constant: constant,
- location: location,
- comments: comments
- }
- end
-
- def format(q)
- q.format(parent)
- q.text("::")
- q.format(constant)
- end
-
- def ===(other)
- other.is_a?(ConstPathField) && parent === other.parent &&
- constant === other.constant
- end
- end
-
- # ConstPathRef represents referencing a constant by a path.
- #
- # object::Const
- #
- class ConstPathRef < Node
- # [Node] the source of the constant
- attr_reader :parent
-
- # [Const] the constant itself
- attr_reader :constant
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(parent:, constant:, location:)
- @parent = parent
- @constant = constant
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_const_path_ref(self)
- end
-
- def child_nodes
- [parent, constant]
- end
-
- def copy(parent: nil, constant: nil, location: nil)
- node =
- ConstPathRef.new(
- parent: parent || self.parent,
- constant: constant || self.constant,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- parent: parent,
- constant: constant,
- location: location,
- comments: comments
- }
- end
-
- def format(q)
- q.format(parent)
- q.text("::")
- q.format(constant)
- end
-
- def ===(other)
- other.is_a?(ConstPathRef) && parent === other.parent &&
- constant === other.constant
- end
- end
-
- # ConstRef represents the name of the constant being used in a class or module
- # declaration.
- #
- # class Container
- # end
- #
- class ConstRef < Node
- # [Const] the constant itself
- attr_reader :constant
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(constant:, location:)
- @constant = constant
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_const_ref(self)
- end
-
- def child_nodes
- [constant]
- end
-
- def copy(constant: nil, location: nil)
- node =
- ConstRef.new(
- constant: constant || self.constant,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { constant: constant, location: location, comments: comments }
- end
-
- def format(q)
- q.format(constant)
- end
-
- def ===(other)
- other.is_a?(ConstRef) && constant === other.constant
- end
- end
-
- # CVar represents the use of a class variable.
- #
- # @@variable
- #
- class CVar < Node
- # [String] the name of the class variable
- attr_reader :value
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:)
- @value = value
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_cvar(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(value: nil, location: nil)
- node =
- CVar.new(
- value: value || self.value,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location, comments: comments }
- end
-
- def format(q)
- q.text(value)
- end
-
- def ===(other)
- other.is_a?(CVar) && value === other.value
- end
- end
-
- # Def represents defining a regular method on the current self object.
- #
- # def method(param) result end
- # def object.method(param) result end
- #
- class DefNode < Node
- # [nil | Node] the target where the method is being defined
- attr_reader :target
-
- # [nil | Op | Period] the operator being used to declare the method
- attr_reader :operator
-
- # [Backtick | Const | Ident | Kw | Op] the name of the method
- attr_reader :name
-
- # [nil | Params | Paren] the parameter declaration for the method
- attr_reader :params
-
- # [BodyStmt | Node] the expressions to be executed by the method
- attr_reader :bodystmt
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(target:, operator:, name:, params:, bodystmt:, location:)
- @target = target
- @operator = operator
- @name = name
- @params = params
- @bodystmt = bodystmt
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_def(self)
- end
-
- def child_nodes
- [target, operator, name, params, bodystmt]
- end
-
- def copy(
- target: nil,
- operator: nil,
- name: nil,
- params: nil,
- bodystmt: nil,
- location: nil
- )
- node =
- DefNode.new(
- target: target || self.target,
- operator: operator || self.operator,
- name: name || self.name,
- params: params || self.params,
- bodystmt: bodystmt || self.bodystmt,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- target: target,
- operator: operator,
- name: name,
- params: params,
- bodystmt: bodystmt,
- location: location,
- comments: comments
- }
- end
-
- def format(q)
- params = self.params
- bodystmt = self.bodystmt
-
- q.group do
- q.group do
- q.text("def")
- q.text(" ") if target || name.comments.empty?
-
- if target
- q.format(target)
- q.format(CallOperatorFormatter.new(operator), stackable: false)
- end
-
- q.format(name)
-
- case params
- when Paren
- q.format(params)
- when Params
- q.format(params) if !params.empty? || params.comments.any?
- end
- end
-
- if endless?
- q.text(" =")
- q.group do
- q.indent do
- q.breakable_space
- q.format(bodystmt)
- end
- end
- else
- unless bodystmt.empty?
- q.indent do
- q.breakable_force
- q.format(bodystmt)
- end
- end
-
- q.breakable_force
- q.text("end")
- end
- end
- end
-
- def ===(other)
- other.is_a?(DefNode) && target === other.target &&
- operator === other.operator && name === other.name &&
- params === other.params && bodystmt === other.bodystmt
- end
-
- # Returns true if the method was found in the source in the "endless" form,
- # i.e. where the method body is defined using the `=` operator after the
- # method name and parameters.
- def endless?
- !bodystmt.is_a?(BodyStmt)
- end
-
- def arity
- params = self.params
-
- case params
- when Params
- params.arity
- when Paren
- params.contents.arity
- else
- 0..0
- end
- end
- end
-
- # Defined represents the use of the +defined?+ operator. It can be used with
- # and without parentheses.
- #
- # defined?(variable)
- #
- class Defined < Node
- # [Node] the value being sent to the keyword
- attr_reader :value
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:)
- @value = value
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_defined(self)
- end
-
- def child_nodes
- [value]
- end
-
- def copy(value: nil, location: nil)
- node =
- Defined.new(
- value: value || self.value,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location, comments: comments }
- end
-
- def format(q)
- q.text("defined?(")
- q.group do
- q.indent do
- q.breakable_empty
- q.format(value)
- end
- q.breakable_empty
- end
- q.text(")")
- end
-
- def ===(other)
- other.is_a?(Defined) && value === other.value
- end
- end
-
- # Block represents passing a block to a method call using the +do+ and +end+
- # keywords or the +{+ and +}+ operators.
- #
- # method do |value|
- # end
- #
- # method { |value| }
- #
- class BlockNode < Node
- # Formats the opening brace or keyword of a block.
- class BlockOpenFormatter
- # [String] the actual output that should be printed
- attr_reader :text
-
- # [LBrace | Keyword] the node that is being represented
- attr_reader :node
-
- def initialize(text, node)
- @text = text
- @node = node
- end
-
- def comments
- node.comments
- end
-
- def format(q)
- q.text(text)
- end
- end
-
- # [LBrace | Kw] the left brace or the do keyword that opens this block
- attr_reader :opening
-
- # [nil | BlockVar] the optional variable declaration within this block
- attr_reader :block_var
-
- # [BodyStmt | Statements] the expressions to be executed within this block
- attr_reader :bodystmt
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(opening:, block_var:, bodystmt:, location:)
- @opening = opening
- @block_var = block_var
- @bodystmt = bodystmt
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_block(self)
- end
-
- def child_nodes
- [opening, block_var, bodystmt]
- end
-
- def copy(opening: nil, block_var: nil, bodystmt: nil, location: nil)
- node =
- BlockNode.new(
- opening: opening || self.opening,
- block_var: block_var || self.block_var,
- bodystmt: bodystmt || self.bodystmt,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- opening: opening,
- block_var: block_var,
- bodystmt: bodystmt,
- location: location,
- comments: comments
- }
- end
-
- def format(q)
- # If this is nested anywhere inside of a Command or CommandCall node, then
- # we can't change which operators we're using for the bounds of the block.
- break_opening, break_closing, flat_opening, flat_closing =
- if unchangeable_bounds?(q)
- block_close = keywords? ? "end" : "}"
- [opening.value, block_close, opening.value, block_close]
- elsif forced_do_end_bounds?(q)
- %w[do end do end]
- elsif forced_brace_bounds?(q)
- %w[{ } { }]
- else
- %w[do end { }]
- end
-
- # If the receiver of this block a Command or CommandCall node, then there
- # are no parentheses around the arguments to that command, so we need to
- # break the block.
- case q.parent
- when nil, Command, CommandCall
- q.break_parent
- format_break(q, break_opening, break_closing)
- return
- end
-
- q.group do
- q
- .if_break { format_break(q, break_opening, break_closing) }
- .if_flat { format_flat(q, flat_opening, flat_closing) }
- end
- end
-
- def ===(other)
- other.is_a?(BlockNode) && opening === other.opening &&
- block_var === other.block_var && bodystmt === other.bodystmt
- end
-
- def keywords?
- opening.is_a?(Kw)
- end
-
- def arity
- case block_var
- when BlockVar
- block_var.params.arity
- else
- 0..0
- end
- end
-
- private
-
- # If this is nested anywhere inside certain nodes, then we can't change
- # which operators/keywords we're using for the bounds of the block.
- def unchangeable_bounds?(q)
- q.parents.any? do |parent|
- # If we hit a statements, then we're safe to use whatever since we
- # know for certain we're going to get split over multiple lines
- # anyway.
- case parent
- when Statements, ArgParen
- break false
- when Command, CommandCall
- true
- else
- false
- end
- end
- end
-
- # If we're a sibling of a control-flow keyword, then we're going to have to
- # use the do..end bounds.
- def forced_do_end_bounds?(q)
- case q.parent&.call
- when Break, Next, ReturnNode, Super
- true
- else
- false
- end
- end
-
- # If we're the predicate of a loop or conditional, then we're going to have
- # to go with the {..} bounds.
- def forced_brace_bounds?(q)
- previous = nil
- q.parents.any? do |parent|
- case parent
- when Paren, Statements
- # If we hit certain breakpoints then we know we're safe.
- return false
- when IfNode, IfOp, UnlessNode, WhileNode, UntilNode
- return true if parent.predicate == previous
- end
-
- previous = parent
- false
- end
- end
-
- def format_break(q, break_opening, break_closing)
- q.text(" ")
- q.format(BlockOpenFormatter.new(break_opening, opening), stackable: false)
-
- if block_var
- q.text(" ")
- q.format(block_var)
- end
-
- unless bodystmt.empty?
- q.indent do
- q.breakable_space
- q.format(bodystmt)
- end
- end
-
- q.breakable_space
- q.text(break_closing)
- end
-
- def format_flat(q, flat_opening, flat_closing)
- q.text(" ")
- q.format(BlockOpenFormatter.new(flat_opening, opening), stackable: false)
-
- if block_var
- q.breakable_space
- q.format(block_var)
- q.breakable_space
- end
-
- if bodystmt.empty?
- q.text(" ") if flat_opening == "do"
- else
- q.breakable_space unless block_var
- q.format(bodystmt)
- q.breakable_space
- end
-
- q.text(flat_closing)
- end
- end
-
- # RangeNode represents using the .. or the ... operator between two
- # expressions. Usually this is to create a range object.
- #
- # 1..2
- #
- # Sometimes this operator is used to create a flip-flop.
- #
- # if value == 5 .. value == 10
- # end
- #
- # One of the sides of the expression may be nil, but not both.
- class RangeNode < Node
- # [nil | Node] the left side of the expression
- attr_reader :left
-
- # [Op] the operator used for this range
- attr_reader :operator
-
- # [nil | Node] the right side of the expression
- attr_reader :right
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(left:, operator:, right:, location:)
- @left = left
- @operator = operator
- @right = right
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_range(self)
- end
-
- def child_nodes
- [left, right]
- end
-
- def copy(left: nil, operator: nil, right: nil, location: nil)
- node =
- RangeNode.new(
- left: left || self.left,
- operator: operator || self.operator,
- right: right || self.right,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- left: left,
- operator: operator,
- right: right,
- location: location,
- comments: comments
- }
- end
-
- def format(q)
- q.format(left) if left
-
- case q.parent
- when IfNode, UnlessNode
- q.text(" #{operator.value} ")
- else
- q.text(operator.value)
- end
-
- q.format(right) if right
- end
-
- def ===(other)
- other.is_a?(RangeNode) && left === other.left &&
- operator === other.operator && right === other.right
- end
- end
-
- # Responsible for providing information about quotes to be used for strings
- # and dynamic symbols.
- module Quotes
- # The matching pairs of quotes that can be used with % literals.
- PAIRS = { "(" => ")", "[" => "]", "{" => "}", "<" => ">" }.freeze
-
- # If there is some part of this string that matches an escape sequence or
- # that contains the interpolation pattern ("#{"), then we are locked into
- # whichever quote the user chose. (If they chose single quotes, then double
- # quoting would activate the escape sequence, and if they chose double
- # quotes, then single quotes would deactivate it.)
- def self.locked?(node, quote)
- node.parts.any? do |part|
- !part.is_a?(TStringContent) || part.value.match?(/\\|#[@${]|#{quote}/)
- end
- end
-
- # Find the matching closing quote for the given opening quote.
- def self.matching(quote)
- PAIRS.fetch(quote) { quote }
- end
-
- # Escape and unescape single and double quotes as needed to be able to
- # enclose +content+ with +enclosing+.
- def self.normalize(content, enclosing)
- return content if enclosing != "\"" && enclosing != "'"
-
- content.gsub(/\\([\s\S])|(['"])/) do
- _match, escaped, quote = Regexp.last_match.to_a
-
- if quote == enclosing
- "\\#{quote}"
- elsif quote
- quote
- else
- "\\#{escaped}"
- end
- end
- end
- end
-
- # DynaSymbol represents a symbol literal that uses quotes to dynamically
- # define its value.
- #
- # :"#{variable}"
- #
- # They can also be used as a special kind of dynamic hash key, as in:
- #
- # { "#{key}": value }
- #
- class DynaSymbol < Node
- # [Array[ StringDVar | StringEmbExpr | TStringContent ]] the parts of the
- # dynamic symbol
- attr_reader :parts
-
- # [nil | String] the quote used to delimit the dynamic symbol
- attr_reader :quote
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(parts:, quote:, location:)
- @parts = parts
- @quote = quote
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_dyna_symbol(self)
- end
-
- def child_nodes
- parts
- end
-
- def copy(parts: nil, quote: nil, location: nil)
- node =
- DynaSymbol.new(
- parts: parts || self.parts,
- quote: quote || self.quote,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { parts: parts, quote: quote, location: location, comments: comments }
- end
-
- def format(q)
- opening_quote, closing_quote = quotes(q)
-
- q.text(opening_quote)
- q.group do
- parts.each do |part|
- if part.is_a?(TStringContent)
- value = Quotes.normalize(part.value, closing_quote)
- first = true
-
- value.each_line(chomp: true) do |line|
- if first
- first = false
- else
- q.breakable_return
- end
-
- q.text(line)
- end
-
- q.breakable_return if value.end_with?("\n")
- else
- q.format(part)
- end
- end
- end
- q.text(closing_quote)
- end
-
- def ===(other)
- other.is_a?(DynaSymbol) && ArrayMatch.call(parts, other.parts) &&
- quote === other.quote
- end
-
- private
-
- # Here we determine the quotes to use for a dynamic symbol. It's bound by a
- # lot of rules because it could be in many different contexts with many
- # different kinds of escaping.
- def quotes(q)
- # If we're inside of an assoc node as the key, then it will handle
- # printing the : on its own since it could change sides.
- parent = q.parent
- hash_key = parent.is_a?(Assoc) && parent.key == self
-
- if quote.start_with?("%s")
- # Here we're going to check if there is a closing character, a new line,
- # or a quote in the content of the dyna symbol. If there is, then
- # quoting could get weird, so just bail out and stick to the original
- # quotes in the source.
- matching = Quotes.matching(quote[2])
- pattern = /[\n#{Regexp.escape(matching)}'"]/
-
- # This check is to ensure we don't find a matching quote inside of the
- # symbol that would be confusing.
- matched =
- parts.any? do |part|
- part.is_a?(TStringContent) && part.value.match?(pattern)
- end
-
- if matched
- [quote, matching]
- elsif Quotes.locked?(self, q.quote)
- ["#{":" unless hash_key}'", "'"]
- else
- ["#{":" unless hash_key}#{q.quote}", q.quote]
- end
- elsif Quotes.locked?(self, q.quote)
- if quote.start_with?(":")
- [hash_key ? quote[1..] : quote, quote[1..]]
- else
- [hash_key ? quote : ":#{quote}", quote]
- end
- else
- [hash_key ? q.quote : ":#{q.quote}", q.quote]
- end
- end
- end
-
- # Else represents the end of an +if+, +unless+, or +case+ chain.
- #
- # if variable
- # else
- # end
- #
- class Else < Node
- # [Kw] the else keyword
- attr_reader :keyword
-
- # [Statements] the expressions to be executed
- attr_reader :statements
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(keyword:, statements:, location:)
- @keyword = keyword
- @statements = statements
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_else(self)
- end
-
- def child_nodes
- [keyword, statements]
- end
-
- def copy(keyword: nil, statements: nil, location: nil)
- node =
- Else.new(
- keyword: keyword || self.keyword,
- statements: statements || self.statements,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- keyword: keyword,
- statements: statements,
- location: location,
- comments: comments
- }
- end
-
- def format(q)
- q.group do
- q.format(keyword)
-
- unless statements.empty?
- q.indent do
- q.breakable_force
- q.format(statements)
- end
- end
- end
- end
-
- def ===(other)
- other.is_a?(Else) && keyword === other.keyword &&
- statements === other.statements
- end
- end
-
- # Elsif represents another clause in an +if+ or +unless+ chain.
- #
- # if variable
- # elsif other_variable
- # end
- #
- class Elsif < Node
- # [Node] the expression to be checked
- attr_reader :predicate
-
- # [Statements] the expressions to be executed
- attr_reader :statements
-
- # [nil | Elsif | Else] the next clause in the chain
- attr_reader :consequent
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(predicate:, statements:, consequent:, location:)
- @predicate = predicate
- @statements = statements
- @consequent = consequent
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_elsif(self)
- end
-
- def child_nodes
- [predicate, statements, consequent]
- end
-
- def copy(predicate: nil, statements: nil, consequent: nil, location: nil)
- node =
- Elsif.new(
- predicate: predicate || self.predicate,
- statements: statements || self.statements,
- consequent: consequent || self.consequent,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- predicate: predicate,
- statements: statements,
- consequent: consequent,
- location: location,
- comments: comments
- }
- end
-
- def format(q)
- q.group do
- q.group do
- q.text("elsif ")
- q.nest("elsif".length - 1) { q.format(predicate) }
- end
-
- unless statements.empty?
- q.indent do
- q.breakable_force
- q.format(statements)
- end
- end
-
- if consequent
- q.group do
- q.breakable_force
- q.format(consequent)
- end
- end
- end
- end
-
- def ===(other)
- other.is_a?(Elsif) && predicate === other.predicate &&
- statements === other.statements && consequent === other.consequent
- end
- end
-
- # EmbDoc represents a multi-line comment.
- #
- # =begin
- # first line
- # second line
- # =end
- #
- class EmbDoc < Node
- # [String] the contents of the comment
- attr_reader :value
-
- def initialize(value:, location:)
- @value = value
- @location = location
-
- @leading = false
- @trailing = false
- end
-
- def leading!
- @leading = true
- end
-
- def leading?
- @leading
- end
-
- def trailing!
- @trailing = true
- end
-
- def trailing?
- @trailing
- end
-
- def inline?
- false
- end
-
- def ignore?
- false
- end
-
- def comments
- []
- end
-
- def accept(visitor)
- visitor.visit_embdoc(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(value: nil, location: nil)
- EmbDoc.new(
- value: value || self.value,
- location: location || self.location
- )
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location }
- end
-
- def format(q)
- if (q.parent.is_a?(DefNode) && q.parent.endless?) ||
- q.parent.is_a?(Statements)
- q.trim
- else
- q.breakable_return
- end
-
- q.text(value)
- end
-
- def ===(other)
- other.is_a?(EmbDoc) && value === other.value
- end
- end
-
- # EmbExprBeg represents the beginning token for using interpolation inside of
- # a parent node that accepts string content (like a string or regular
- # expression).
- #
- # "Hello, #{person}!"
- #
- class EmbExprBeg < Node
- # [String] the #{ used in the string
- attr_reader :value
-
- def initialize(value:, location:)
- @value = value
- @location = location
- end
-
- def accept(visitor)
- visitor.visit_embexpr_beg(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(value: nil, location: nil)
- EmbExprBeg.new(
- value: value || self.value,
- location: location || self.location
- )
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location }
- end
-
- def ===(other)
- other.is_a?(EmbExprBeg) && value === other.value
- end
- end
-
- # EmbExprEnd represents the ending token for using interpolation inside of a
- # parent node that accepts string content (like a string or regular
- # expression).
- #
- # "Hello, #{person}!"
- #
- class EmbExprEnd < Node
- # [String] the } used in the string
- attr_reader :value
-
- def initialize(value:, location:)
- @value = value
- @location = location
- end
-
- def accept(visitor)
- visitor.visit_embexpr_end(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(value: nil, location: nil)
- EmbExprEnd.new(
- value: value || self.value,
- location: location || self.location
- )
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location }
- end
-
- def ===(other)
- other.is_a?(EmbExprEnd) && value === other.value
- end
- end
-
- # EmbVar represents the use of shorthand interpolation for an instance, class,
- # or global variable into a parent node that accepts string content (like a
- # string or regular expression).
- #
- # "#@variable"
- #
- # In the example above, an EmbVar node represents the # because it forces
- # @variable to be interpolated.
- class EmbVar < Node
- # [String] the # used in the string
- attr_reader :value
-
- def initialize(value:, location:)
- @value = value
- @location = location
- end
-
- def accept(visitor)
- visitor.visit_embvar(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(value: nil, location: nil)
- EmbVar.new(
- value: value || self.value,
- location: location || self.location
- )
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location }
- end
-
- def ===(other)
- other.is_a?(EmbVar) && value === other.value
- end
- end
-
- # Ensure represents the use of the +ensure+ keyword and its subsequent
- # statements.
- #
- # begin
- # ensure
- # end
- #
- class Ensure < Node
- # [Kw] the ensure keyword that began this node
- attr_reader :keyword
-
- # [Statements] the expressions to be executed
- attr_reader :statements
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(keyword:, statements:, location:)
- @keyword = keyword
- @statements = statements
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_ensure(self)
- end
-
- def child_nodes
- [keyword, statements]
- end
-
- def copy(keyword: nil, statements: nil, location: nil)
- node =
- Ensure.new(
- keyword: keyword || self.keyword,
- statements: statements || self.statements,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- keyword: keyword,
- statements: statements,
- location: location,
- comments: comments
- }
- end
-
- def format(q)
- q.format(keyword)
-
- unless statements.empty?
- q.indent do
- q.breakable_force
- q.format(statements)
- end
- end
- end
-
- def ===(other)
- other.is_a?(Ensure) && keyword === other.keyword &&
- statements === other.statements
- end
- end
-
- # ExcessedComma represents a trailing comma in a list of block parameters. It
- # changes the block parameters such that they will destructure.
- #
- # [[1, 2, 3], [2, 3, 4]].each do |first, second,|
- # end
- #
- # In the above example, an ExcessedComma node would appear in the third
- # position of the Params node that is used to declare that block. The third
- # position typically represents a rest-type parameter, but in this case is
- # used to indicate that a trailing comma was used.
- class ExcessedComma < Node
- # [String] the comma
- attr_reader :value
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:)
- @value = value
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_excessed_comma(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(value: nil, location: nil)
- node =
- ExcessedComma.new(
- value: value || self.value,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location, comments: comments }
- end
-
- def format(q)
- q.text(value)
- end
-
- def ===(other)
- other.is_a?(ExcessedComma) && value === other.value
- end
- end
-
- # Field is always the child of an assignment. It represents assigning to a
- # “field” on an object.
- #
- # object.variable = value
- #
- class Field < Node
- # [Node] the parent object that owns the field being assigned
- attr_reader :parent
-
- # [:"::" | Op | Period] the operator being used for the assignment
- attr_reader :operator
-
- # [Const | Ident] the name of the field being assigned
- attr_reader :name
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(parent:, operator:, name:, location:)
- @parent = parent
- @operator = operator
- @name = name
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_field(self)
- end
-
- def child_nodes
- operator = self.operator
- [parent, (operator if operator != :"::"), name]
- end
-
- def copy(parent: nil, operator: nil, name: nil, location: nil)
- node =
- Field.new(
- parent: parent || self.parent,
- operator: operator || self.operator,
- name: name || self.name,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- parent: parent,
- operator: operator,
- name: name,
- location: location,
- comments: comments
- }
- end
-
- def format(q)
- q.group do
- q.format(parent)
- q.format(CallOperatorFormatter.new(operator), stackable: false)
- q.format(name)
- end
- end
-
- def ===(other)
- other.is_a?(Field) && parent === other.parent &&
- operator === other.operator && name === other.name
- end
- end
-
- # FloatLiteral represents a floating point number literal.
- #
- # 1.0
- #
- class FloatLiteral < Node
- # [String] the value of the floating point number literal
- attr_reader :value
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:)
- @value = value
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_float(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(value: nil, location: nil)
- node =
- FloatLiteral.new(
- value: value || self.value,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location, comments: comments }
- end
-
- def format(q)
- q.text(value)
- end
-
- def ===(other)
- other.is_a?(FloatLiteral) && value === other.value
- end
- end
-
- # FndPtn represents matching against a pattern where you find a pattern in an
- # array using the Ruby 3.0+ pattern matching syntax.
- #
- # case value
- # in [*, 7, *]
- # end
- #
- class FndPtn < Node
- # [nil | VarRef | ConstPathRef] the optional constant wrapper
- attr_reader :constant
-
- # [VarField] the splat on the left-hand side
- attr_reader :left
-
- # [Array[ Node ]] the list of positional expressions in the pattern that
- # are being matched
- attr_reader :values
-
- # [VarField] the splat on the right-hand side
- attr_reader :right
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(constant:, left:, values:, right:, location:)
- @constant = constant
- @left = left
- @values = values
- @right = right
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_fndptn(self)
- end
-
- def child_nodes
- [constant, left, *values, right]
- end
-
- def copy(constant: nil, left: nil, values: nil, right: nil, location: nil)
- node =
- FndPtn.new(
- constant: constant || self.constant,
- left: left || self.left,
- values: values || self.values,
- right: right || self.right,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- constant: constant,
- left: left,
- values: values,
- right: right,
- location: location,
- comments: comments
- }
- end
-
- def format(q)
- q.format(constant) if constant
-
- q.group do
- q.text("[")
-
- q.indent do
- q.breakable_empty
-
- q.text("*")
- q.format(left)
- q.comma_breakable
-
- q.seplist(values) { |value| q.format(value) }
- q.comma_breakable
-
- q.text("*")
- q.format(right)
- end
-
- q.breakable_empty
- q.text("]")
- end
- end
-
- def ===(other)
- other.is_a?(FndPtn) && constant === other.constant &&
- left === other.left && ArrayMatch.call(values, other.values) &&
- right === other.right
- end
- end
-
- # For represents using a +for+ loop.
- #
- # for value in list do
- # end
- #
- class For < Node
- # [MLHS | VarField] the variable declaration being used to
- # pull values out of the object being enumerated
- attr_reader :index
-
- # [Node] the object being enumerated in the loop
- attr_reader :collection
-
- # [Statements] the statements to be executed
- attr_reader :statements
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(index:, collection:, statements:, location:)
- @index = index
- @collection = collection
- @statements = statements
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_for(self)
- end
-
- def child_nodes
- [index, collection, statements]
- end
-
- def copy(index: nil, collection: nil, statements: nil, location: nil)
- node =
- For.new(
- index: index || self.index,
- collection: collection || self.collection,
- statements: statements || self.statements,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- index: index,
- collection: collection,
- statements: statements,
- location: location,
- comments: comments
- }
- end
-
- def format(q)
- q.group do
- q.text("for ")
- q.group { q.format(index) }
- q.text(" in ")
- q.format(collection)
-
- unless statements.empty?
- q.indent do
- q.breakable_force
- q.format(statements)
- end
- end
-
- q.breakable_force
- q.text("end")
- end
- end
-
- def ===(other)
- other.is_a?(For) && index === other.index &&
- collection === other.collection && statements === other.statements
- end
- end
-
- # GVar represents a global variable literal.
- #
- # $variable
- #
- class GVar < Node
- # [String] the name of the global variable
- attr_reader :value
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:)
- @value = value
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_gvar(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(value: nil, location: nil)
- node =
- GVar.new(
- value: value || self.value,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location, comments: comments }
- end
-
- def format(q)
- q.text(value)
- end
-
- def ===(other)
- other.is_a?(GVar) && value === other.value
- end
- end
-
- # HashLiteral represents a hash literal.
- #
- # { key => value }
- #
- class HashLiteral < Node
- # This is a special formatter used if the hash literal contains no values
- # but _does_ contain comments. In this case we do some special formatting to
- # make sure the comments gets indented properly.
- class EmptyWithCommentsFormatter
- # [LBrace] the opening brace
- attr_reader :lbrace
-
- def initialize(lbrace)
- @lbrace = lbrace
- end
-
- def format(q)
- q.group do
- q.text("{")
- q.indent do
- lbrace.comments.each do |comment|
- q.breakable_force
- comment.format(q)
- end
- end
- q.breakable_force
- q.text("}")
- end
- end
- end
-
- # [LBrace] the left brace that opens this hash
- attr_reader :lbrace
-
- # [Array[ Assoc | AssocSplat ]] the optional contents of the hash
- attr_reader :assocs
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(lbrace:, assocs:, location:)
- @lbrace = lbrace
- @assocs = assocs
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_hash(self)
- end
-
- def child_nodes
- [lbrace].concat(assocs)
- end
-
- def copy(lbrace: nil, assocs: nil, location: nil)
- node =
- HashLiteral.new(
- lbrace: lbrace || self.lbrace,
- assocs: assocs || self.assocs,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { lbrace: lbrace, assocs: assocs, location: location, comments: comments }
- end
-
- def format(q)
- if q.parent.is_a?(Assoc)
- format_contents(q)
- else
- q.group { format_contents(q) }
- end
- end
-
- def ===(other)
- other.is_a?(HashLiteral) && lbrace === other.lbrace &&
- ArrayMatch.call(assocs, other.assocs)
- end
-
- def format_key(q, key)
- (@key_formatter ||= HashKeyFormatter.for(self)).format_key(q, key)
- end
-
- private
-
- # If we have an empty hash that contains only comments, then we're going
- # to do some special printing to ensure they get indented correctly.
- def empty_with_comments?
- assocs.empty? && lbrace.comments.any? && lbrace.comments.none?(&:inline?)
- end
-
- def format_contents(q)
- if empty_with_comments?
- EmptyWithCommentsFormatter.new(lbrace).format(q)
- return
- end
-
- q.format(lbrace)
-
- if assocs.empty?
- q.breakable_empty
- else
- q.indent do
- q.breakable_space
- q.seplist(assocs) { |assoc| q.format(assoc) }
- q.if_break { q.text(",") } if q.trailing_comma?
- end
- q.breakable_space
- end
-
- q.text("}")
- end
- end
-
- # Heredoc represents a heredoc string literal.
- #
- # <<~DOC
- # contents
- # DOC
- #
- class Heredoc < Node
- # [HeredocBeg] the opening of the heredoc
- attr_reader :beginning
-
- # [HeredocEnd] the ending of the heredoc
- attr_reader :ending
-
- # [Integer] how far to dedent the heredoc
- attr_reader :dedent
-
- # [Array[ StringEmbExpr | StringDVar | TStringContent ]] the parts of the
- # heredoc string literal
- attr_reader :parts
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(beginning:, location:, ending: nil, dedent: 0, parts: [])
- @beginning = beginning
- @ending = ending
- @dedent = dedent
- @parts = parts
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_heredoc(self)
- end
-
- def child_nodes
- [beginning, *parts, ending]
- end
-
- def copy(beginning: nil, location: nil, ending: nil, parts: nil)
- node =
- Heredoc.new(
- beginning: beginning || self.beginning,
- location: location || self.location,
- ending: ending || self.ending,
- parts: parts || self.parts
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- beginning: beginning,
- location: location,
- ending: ending,
- parts: parts,
- comments: comments
- }
- end
-
- # This is a very specific behavior where you want to force a newline, but
- # don't want to force the break parent.
- SEPARATOR =
- PrettierPrint::Breakable.new(" ", 1, indent: false, force: true).freeze
-
- def format(q)
- q.group do
- q.format(beginning)
-
- q.line_suffix(priority: Formatter::HEREDOC_PRIORITY) do
- q.group do
- q.target << SEPARATOR
-
- parts.each do |part|
- if part.is_a?(TStringContent)
- value = part.value
- first = true
-
- value.each_line(chomp: true) do |line|
- if first
- first = false
- else
- q.target << SEPARATOR
- end
-
- q.text(line)
- end
-
- q.target << SEPARATOR if value.end_with?("\n")
- else
- q.format(part)
- end
- end
-
- q.format(ending)
- end
- end
- end
- end
-
- def ===(other)
- other.is_a?(Heredoc) && beginning === other.beginning &&
- ending === other.ending && ArrayMatch.call(parts, other.parts)
- end
- end
-
- # HeredocBeg represents the beginning declaration of a heredoc.
- #
- # <<~DOC
- # contents
- # DOC
- #
- # In the example above the HeredocBeg node represents <<~DOC.
- class HeredocBeg < Node
- # [String] the opening declaration of the heredoc
- attr_reader :value
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:)
- @value = value
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_heredoc_beg(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(value: nil, location: nil)
- node =
- HeredocBeg.new(
- value: value || self.value,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location, comments: comments }
- end
-
- def format(q)
- q.text(value)
- end
-
- def ===(other)
- other.is_a?(HeredocBeg) && value === other.value
- end
- end
-
- # HeredocEnd represents the closing declaration of a heredoc.
- #
- # <<~DOC
- # contents
- # DOC
- #
- # In the example above the HeredocEnd node represents the closing DOC.
- class HeredocEnd < Node
- # [String] the closing declaration of the heredoc
- attr_reader :value
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:)
- @value = value
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_heredoc_end(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(value: nil, location: nil)
- node =
- HeredocEnd.new(
- value: value || self.value,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location, comments: comments }
- end
-
- def format(q)
- q.text(value)
- end
-
- def ===(other)
- other.is_a?(HeredocEnd) && value === other.value
- end
- end
-
- # HshPtn represents matching against a hash pattern using the Ruby 2.7+
- # pattern matching syntax.
- #
- # case value
- # in { key: }
- # end
- #
- class HshPtn < Node
- # Formats a key-value pair in a hash pattern. The value is optional.
- class KeywordFormatter
- # [Label] the keyword being used
- attr_reader :key
-
- # [Node] the optional value for the keyword
- attr_reader :value
-
- def initialize(key, value)
- @key = key
- @value = value
- end
-
- def comments
- []
- end
-
- def format(q)
- HashKeyFormatter::Labels.new.format_key(q, key)
-
- if value
- q.text(" ")
- q.format(value)
- end
- end
- end
-
- # Formats the optional double-splat from the pattern.
- class KeywordRestFormatter
- # [VarField] the parameter that matches the remaining keywords
- attr_reader :keyword_rest
-
- def initialize(keyword_rest)
- @keyword_rest = keyword_rest
- end
-
- def comments
- []
- end
-
- def format(q)
- q.text("**")
- q.format(keyword_rest)
- end
- end
-
- # [nil | VarRef | ConstPathRef] the optional constant wrapper
- attr_reader :constant
-
- # [Array[ [DynaSymbol | Label, nil | Node] ]] the set of tuples
- # representing the keywords that should be matched against in the pattern
- attr_reader :keywords
-
- # [nil | VarField] an optional parameter to gather up all remaining keywords
- attr_reader :keyword_rest
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(constant:, keywords:, keyword_rest:, location:)
- @constant = constant
- @keywords = keywords
- @keyword_rest = keyword_rest
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_hshptn(self)
- end
-
- def child_nodes
- [constant, *keywords.flatten(1), keyword_rest]
- end
-
- def copy(constant: nil, keywords: nil, keyword_rest: nil, location: nil)
- node =
- HshPtn.new(
- constant: constant || self.constant,
- keywords: keywords || self.keywords,
- keyword_rest: keyword_rest || self.keyword_rest,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- constant: constant,
- keywords: keywords,
- keyword_rest: keyword_rest,
- location: location,
- comments: comments
- }
- end
-
- def format(q)
- parts = keywords.map { |(key, value)| KeywordFormatter.new(key, value) }
- parts << KeywordRestFormatter.new(keyword_rest) if keyword_rest
- nested = PATTERNS.include?(q.parent.class)
-
- # If there is a constant, we're going to format to have the constant name
- # first and then use brackets.
- if constant
- q.group do
- q.format(constant)
- q.text("[")
- q.indent do
- q.breakable_empty
- format_contents(q, parts, nested)
- end
- q.breakable_empty
- q.text("]")
- end
- return
- end
-
- # If there's nothing at all, then we're going to use empty braces.
- if parts.empty?
- q.text("{}")
- return
- end
-
- # If there's only one pair, then we'll just print the contents provided
- # we're not inside another pattern.
- if !nested && parts.size == 1
- format_contents(q, parts, nested)
- return
- end
-
- # Otherwise, we're going to always use braces to make it clear it's a hash
- # pattern.
- q.group do
- q.text("{")
- q.indent do
- q.breakable_space
- format_contents(q, parts, nested)
- end
-
- if q.target_ruby_version < Formatter::SemanticVersion.new("2.7.3")
- q.text(" }")
- else
- q.breakable_space
- q.text("}")
- end
- end
- end
-
- def ===(other)
- other.is_a?(HshPtn) && constant === other.constant &&
- keywords.length == other.keywords.length &&
- keywords
- .zip(other.keywords)
- .all? { |left, right| ArrayMatch.call(left, right) } &&
- keyword_rest === other.keyword_rest
- end
-
- private
-
- def format_contents(q, parts, nested)
- keyword_rest = self.keyword_rest
-
- q.group { q.seplist(parts) { |part| q.format(part, stackable: false) } }
-
- # If there isn't a constant, and there's a blank keyword_rest, then we
- # have an plain ** that needs to have a `then` after it in order to
- # parse correctly on the next parse.
- if !constant && keyword_rest && keyword_rest.value.nil? && !nested
- q.text(" then")
- end
- end
- end
-
- # The list of nodes that represent patterns inside of pattern matching so that
- # when a pattern is being printed it knows if it's nested.
- PATTERNS = [AryPtn, Binary, FndPtn, HshPtn, RAssign].freeze
-
- # Ident represents an identifier anywhere in code. It can represent a very
- # large number of things, depending on where it is in the syntax tree.
- #
- # value
- #
- class Ident < Node
- # [String] the value of the identifier
- attr_reader :value
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:)
- @value = value
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_ident(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(value: nil, location: nil)
- node =
- Ident.new(
- value: value || self.value,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location, comments: comments }
- end
-
- def format(q)
- q.text(value)
- end
-
- def ===(other)
- other.is_a?(Ident) && value === other.value
- end
- end
-
- # If the predicate of a conditional or loop contains an assignment (in which
- # case we can't know for certain that that assignment doesn't impact the
- # statements inside the conditional) then we can't use the modifier form
- # and we must use the block form.
- module ContainsAssignment
- def self.call(parent)
- queue = [parent]
-
- while (node = queue.shift)
- case node
- when Assign, MAssign, OpAssign
- return true
- else
- node.child_nodes.each { |child| queue << child if child }
- end
- end
-
- false
- end
- end
-
- # In order for an `if` or `unless` expression to be shortened to a ternary,
- # there has to be one and only one consequent clause which is an Else. Both
- # the body of the main node and the body of the Else node must have only one
- # statement, and that statement must not be on the denied list of potential
- # statements.
- module Ternaryable
- class << self
- def call(q, node)
- return false if ENV["STREE_FAST_FORMAT"] || q.disable_auto_ternary?
-
- # If this is a conditional inside of a parentheses as the only content,
- # then we don't want to transform it into a ternary. Presumably the user
- # wanted it to be an explicit conditional because there are parentheses
- # around it. So we'll just leave it in place.
- grandparent = q.grandparent
- if grandparent.is_a?(Paren) && (body = grandparent.contents.body) &&
- body.length == 1 && body.first == node
- return false
- end
-
- # Otherwise, we'll check the type of predicate. For certain nodes we
- # want to force it to not be a ternary, like if the predicate is an
- # assignment because it's hard to read.
- case node.predicate
- when Assign, Binary, Command, CommandCall, MAssign, OpAssign
- return false
- when Not
- return false unless node.predicate.parentheses?
- end
-
- # If there's no Else, then this can't be represented as a ternary.
- return false unless node.consequent.is_a?(Else)
-
- truthy_body = node.statements.body
- falsy_body = node.consequent.statements.body
-
- (truthy_body.length == 1) && ternaryable?(truthy_body.first) &&
- (falsy_body.length == 1) && ternaryable?(falsy_body.first)
- end
-
- private
-
- # Certain expressions cannot be reduced to a ternary without adding
- # parentheses around them. In this case we say they cannot be ternaried
- # and default instead to breaking them into multiple lines.
- def ternaryable?(statement)
- case statement
- when AliasNode, Assign, Break, Command, CommandCall, Defined, Heredoc,
- IfNode, IfOp, Lambda, MAssign, Next, OpAssign, RescueMod,
- ReturnNode, Super, Undef, UnlessNode, UntilNode, VoidStmt,
- WhileNode, YieldNode, ZSuper
- # This is a list of nodes that should not be allowed to be a part of a
- # ternary clause.
- false
- when Binary
- # If the user is using one of the lower precedence "and" or "or"
- # operators, then we can't use a ternary expression as it would break
- # the flow control.
- operator = statement.operator
- operator != :and && operator != :or
- else
- true
- end
- end
- end
- end
-
- # Formats an If or Unless node.
- class ConditionalFormatter
- # [String] the keyword associated with this conditional
- attr_reader :keyword
-
- # [If | Unless] the node that is being formatted
- attr_reader :node
-
- def initialize(keyword, node)
- @keyword = keyword
- @node = node
- end
-
- def format(q)
- if node.modifier?
- statement = node.statements.body[0]
-
- if ContainsAssignment.call(statement) || q.parent.is_a?(In)
- q.group { format_flat(q) }
- else
- q.group do
- q
- .if_break { format_break(q, force: false) }
- .if_flat { format_flat(q) }
- end
- end
- else
- # If we can transform this node into a ternary, then we're going to
- # print a special version that uses the ternary operator if it fits on
- # one line.
- if Ternaryable.call(q, node)
- format_ternary(q)
- return
- end
-
- # If the predicate of the conditional contains an assignment (in which
- # case we can't know for certain that that assignment doesn't impact the
- # statements inside the conditional) then we can't use the modifier form
- # and we must use the block form.
- if ContainsAssignment.call(node.predicate)
- format_break(q, force: true)
- return
- end
-
- if node.consequent || node.statements.empty? || contains_conditional?
- q.group { format_break(q, force: true) }
- else
- q.group do
- q
- .if_break { format_break(q, force: false) }
- .if_flat do
- Parentheses.flat(q) do
- q.format(node.statements)
- q.text(" #{keyword} ")
- q.format(node.predicate)
- end
- end
- end
- end
- end
- end
-
- private
-
- def format_flat(q)
- Parentheses.flat(q) do
- q.format(node.statements.body[0])
- q.text(" #{keyword} ")
- q.format(node.predicate)
- end
- end
-
- def format_break(q, force:)
- q.text("#{keyword} ")
- q.nest(keyword.length + 1) { q.format(node.predicate) }
-
- unless node.statements.empty?
- q.indent do
- force ? q.breakable_force : q.breakable_space
- q.format(node.statements)
- end
- end
-
- if node.consequent
- force ? q.breakable_force : q.breakable_space
- q.format(node.consequent)
- end
-
- force ? q.breakable_force : q.breakable_space
- q.text("end")
- end
-
- def format_ternary(q)
- q.group do
- q
- .if_break do
- q.text("#{keyword} ")
- q.nest(keyword.length + 1) { q.format(node.predicate) }
-
- q.indent do
- q.breakable_space
- q.format(node.statements)
- end
-
- q.breakable_space
- q.group do
- q.format(node.consequent.keyword)
- q.indent do
- # This is a very special case of breakable where we want to
- # force it into the output but we _don't_ want to explicitly
- # break the parent. If a break-parent shows up in the tree, then
- # it's going to force it all the way up to the tree, which is
- # going to negate the ternary.
- q.breakable(force: :skip_break_parent)
- q.format(node.consequent.statements)
- end
- end
-
- q.breakable_space
- q.text("end")
- end
- .if_flat do
- Parentheses.flat(q) do
- q.format(node.predicate)
- q.text(" ? ")
-
- statements = [node.statements, node.consequent.statements]
- statements.reverse! if keyword == "unless"
-
- q.format(statements[0])
- q.text(" : ")
- q.format(statements[1])
- end
- end
- end
- end
-
- def contains_conditional?
- statements = node.statements.body
- return false if statements.length != 1
-
- case statements.first
- when IfNode, IfOp, UnlessNode
- true
- else
- false
- end
- end
- end
-
- # If represents the first clause in an +if+ chain.
- #
- # if predicate
- # end
- #
- class IfNode < Node
- # [Node] the expression to be checked
- attr_reader :predicate
-
- # [Statements] the expressions to be executed
- attr_reader :statements
-
- # [nil | Elsif | Else] the next clause in the chain
- attr_reader :consequent
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(predicate:, statements:, consequent:, location:)
- @predicate = predicate
- @statements = statements
- @consequent = consequent
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_if(self)
- end
-
- def child_nodes
- [predicate, statements, consequent]
- end
-
- def copy(predicate: nil, statements: nil, consequent: nil, location: nil)
- node =
- IfNode.new(
- predicate: predicate || self.predicate,
- statements: statements || self.statements,
- consequent: consequent || self.consequent,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- predicate: predicate,
- statements: statements,
- consequent: consequent,
- location: location,
- comments: comments
- }
- end
-
- def format(q)
- ConditionalFormatter.new("if", self).format(q)
- end
-
- def ===(other)
- other.is_a?(IfNode) && predicate === other.predicate &&
- statements === other.statements && consequent === other.consequent
- end
-
- # Checks if the node was originally found in the modifier form.
- def modifier?
- predicate.location.start_char > statements.location.start_char
- end
- end
-
- # IfOp represents a ternary clause.
- #
- # predicate ? truthy : falsy
- #
- class IfOp < Node
- # [Node] the expression to be checked
- attr_reader :predicate
-
- # [Node] the expression to be executed if the predicate is truthy
- attr_reader :truthy
-
- # [Node] the expression to be executed if the predicate is falsy
- attr_reader :falsy
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(predicate:, truthy:, falsy:, location:)
- @predicate = predicate
- @truthy = truthy
- @falsy = falsy
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_if_op(self)
- end
-
- def child_nodes
- [predicate, truthy, falsy]
- end
-
- def copy(predicate: nil, truthy: nil, falsy: nil, location: nil)
- node =
- IfOp.new(
- predicate: predicate || self.predicate,
- truthy: truthy || self.truthy,
- falsy: falsy || self.falsy,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- predicate: predicate,
- truthy: truthy,
- falsy: falsy,
- location: location,
- comments: comments
- }
- end
-
- def format(q)
- force_flat = [
- AliasNode,
- Assign,
- Break,
- Command,
- CommandCall,
- Heredoc,
- IfNode,
- IfOp,
- Lambda,
- MAssign,
- Next,
- OpAssign,
- RescueMod,
- ReturnNode,
- Super,
- Undef,
- UnlessNode,
- VoidStmt,
- YieldNode,
- ZSuper
- ]
-
- if q.parent.is_a?(Paren) || force_flat.include?(truthy.class) ||
- force_flat.include?(falsy.class)
- q.group { format_flat(q) }
- return
- end
-
- q.group { q.if_break { format_break(q) }.if_flat { format_flat(q) } }
- end
-
- def ===(other)
- other.is_a?(IfOp) && predicate === other.predicate &&
- truthy === other.truthy && falsy === other.falsy
- end
-
- private
-
- def format_break(q)
- Parentheses.break(q) do
- q.text("if ")
- q.nest("if ".length) { q.format(predicate) }
-
- q.indent do
- q.breakable_space
- q.format(truthy)
- end
-
- q.breakable_space
- q.text("else")
-
- q.indent do
- q.breakable_space
- q.format(falsy)
- end
-
- q.breakable_space
- q.text("end")
- end
- end
-
- def format_flat(q)
- q.format(predicate)
- q.text(" ?")
-
- q.indent do
- q.breakable_space
- q.format(truthy)
- q.text(" :")
-
- q.breakable_space
- q.format(falsy)
- end
- end
- end
-
- # Imaginary represents an imaginary number literal.
- #
- # 1i
- #
- class Imaginary < Node
- # [String] the value of the imaginary number literal
- attr_reader :value
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:)
- @value = value
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_imaginary(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(value: nil, location: nil)
- node =
- Imaginary.new(
- value: value || self.value,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location, comments: comments }
- end
-
- def format(q)
- q.text(value)
- end
-
- def ===(other)
- other.is_a?(Imaginary) && value === other.value
- end
- end
-
- # In represents using the +in+ keyword within the Ruby 2.7+ pattern matching
- # syntax.
- #
- # case value
- # in pattern
- # end
- #
- class In < Node
- # [Node] the pattern to check against
- attr_reader :pattern
-
- # [Statements] the expressions to execute if the pattern matched
- attr_reader :statements
-
- # [nil | In | Else] the next clause in the chain
- attr_reader :consequent
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(pattern:, statements:, consequent:, location:)
- @pattern = pattern
- @statements = statements
- @consequent = consequent
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_in(self)
- end
-
- def child_nodes
- [pattern, statements, consequent]
- end
-
- def copy(pattern: nil, statements: nil, consequent: nil, location: nil)
- node =
- In.new(
- pattern: pattern || self.pattern,
- statements: statements || self.statements,
- consequent: consequent || self.consequent,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- pattern: pattern,
- statements: statements,
- consequent: consequent,
- location: location,
- comments: comments
- }
- end
-
- def format(q)
- keyword = "in "
- pattern = self.pattern
- consequent = self.consequent
-
- q.group do
- q.text(keyword)
- q.nest(keyword.length) { q.format(pattern) }
- q.text(" then") if pattern.is_a?(RangeNode) && pattern.right.nil?
-
- unless statements.empty?
- q.indent do
- q.breakable_force
- q.format(statements)
- end
- end
-
- if consequent
- q.breakable_force
- q.format(consequent)
- end
- end
- end
-
- def ===(other)
- other.is_a?(In) && pattern === other.pattern &&
- statements === other.statements && consequent === other.consequent
- end
- end
-
- # Int represents an integer number literal.
- #
- # 1
- #
- class Int < Node
- # [String] the value of the integer
- attr_reader :value
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:)
- @value = value
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_int(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(value: nil, location: nil)
- node =
- Int.new(value: value || self.value, location: location || self.location)
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location, comments: comments }
- end
-
- def format(q)
- if !value.start_with?(/\+?0/) && value.length >= 5 && !value.include?("_")
- # If it's a plain integer and it doesn't have any underscores separating
- # the values, then we're going to insert them every 3 characters
- # starting from the right.
- index = (value.length + 2) % 3
- q.text(" #{value}"[index..].scan(/.../).join("_").strip)
- else
- q.text(value)
- end
- end
-
- def ===(other)
- other.is_a?(Int) && value === other.value
- end
- end
-
- # IVar represents an instance variable literal.
- #
- # @variable
- #
- class IVar < Node
- # [String] the name of the instance variable
- attr_reader :value
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:)
- @value = value
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_ivar(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(value: nil, location: nil)
- node =
- IVar.new(
- value: value || self.value,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location, comments: comments }
- end
-
- def format(q)
- q.text(value)
- end
-
- def ===(other)
- other.is_a?(IVar) && value === other.value
- end
- end
-
- # Kw represents the use of a keyword. It can be almost anywhere in the syntax
- # tree, so you end up seeing it quite a lot.
- #
- # if value
- # end
- #
- # In the above example, there would be two Kw nodes: one for the if and one
- # for the end. Note that anything that matches the list of keywords in Ruby
- # will use a Kw, so if you use a keyword in a symbol literal for instance:
- #
- # :if
- #
- # then the contents of the symbol node will contain a Kw node.
- class Kw < Node
- # [String] the value of the keyword
- attr_reader :value
-
- # [Symbol] the symbol version of the value
- attr_reader :name
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:)
- @value = value
- @name = value.to_sym
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_kw(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(value: nil, location: nil)
- node =
- Kw.new(value: value || self.value, location: location || self.location)
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location, comments: comments }
- end
-
- def format(q)
- q.text(value)
- end
-
- def ===(other)
- other.is_a?(Kw) && value === other.value
- end
- end
-
- # KwRestParam represents defining a parameter in a method definition that
- # accepts all remaining keyword parameters.
- #
- # def method(**kwargs) end
- #
- class KwRestParam < Node
- # [nil | Ident] the name of the parameter
- attr_reader :name
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(name:, location:)
- @name = name
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_kwrest_param(self)
- end
-
- def child_nodes
- [name]
- end
-
- def copy(name: nil, location: nil)
- node =
- KwRestParam.new(
- name: name || self.name,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { name: name, location: location, comments: comments }
- end
-
- def format(q)
- q.text("**")
- q.format(name) if name
- end
-
- def ===(other)
- other.is_a?(KwRestParam) && name === other.name
- end
- end
-
- # Label represents the use of an identifier to associate with an object. You
- # can find it in a hash key, as in:
- #
- # { key: value }
- #
- # In this case "key:" would be the body of the label. You can also find it in
- # pattern matching, as in:
- #
- # case value
- # in key:
- # end
- #
- # In this case "key:" would be the body of the label.
- class Label < Node
- # [String] the value of the label
- attr_reader :value
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:)
- @value = value
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_label(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(value: nil, location: nil)
- node =
- Label.new(
- value: value || self.value,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location, comments: comments }
- end
-
- def format(q)
- q.text(value)
- end
-
- def ===(other)
- other.is_a?(Label) && value === other.value
- end
- end
-
- # LabelEnd represents the end of a dynamic symbol.
- #
- # { "key": value }
- #
- # In the example above, LabelEnd represents the "\":" token at the end of the
- # hash key. This node is important for determining the type of quote being
- # used by the label.
- class LabelEnd < Node
- # [String] the end of the label
- attr_reader :value
-
- def initialize(value:, location:)
- @value = value
- @location = location
- end
-
- def accept(visitor)
- visitor.visit_label_end(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(value: nil, location: nil)
- LabelEnd.new(
- value: value || self.value,
- location: location || self.location
- )
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location }
- end
-
- def ===(other)
- other.is_a?(LabelEnd) && value === other.value
- end
- end
-
- # Lambda represents using a lambda literal (not the lambda method call).
- #
- # ->(value) { value * 2 }
- #
- class Lambda < Node
- # [LambdaVar | Paren] the parameter declaration for this lambda
- attr_reader :params
-
- # [BodyStmt | Statements] the expressions to be executed in this lambda
- attr_reader :statements
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(params:, statements:, location:)
- @params = params
- @statements = statements
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_lambda(self)
- end
-
- def child_nodes
- [params, statements]
- end
-
- def copy(params: nil, statements: nil, location: nil)
- node =
- Lambda.new(
- params: params || self.params,
- statements: statements || self.statements,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- params: params,
- statements: statements,
- location: location,
- comments: comments
- }
- end
-
- def format(q)
- params = self.params
-
- q.text("->")
- q.group do
- if params.is_a?(Paren)
- q.format(params) unless params.contents.empty?
- elsif params.empty? && params.comments.any?
- q.format(params)
- elsif !params.empty?
- q.group do
- q.text("(")
- q.format(params)
- q.text(")")
- end
- end
-
- q.text(" ")
- q
- .if_break do
- q.text("do")
-
- unless statements.empty?
- q.indent do
- q.breakable_space
- q.format(statements)
- end
- end
-
- q.breakable_space
- q.text("end")
- end
- .if_flat do
- q.text("{")
-
- unless statements.empty?
- q.text(" ")
- q.format(statements)
- q.text(" ")
- end
-
- q.text("}")
- end
- end
- end
-
- def ===(other)
- other.is_a?(Lambda) && params === other.params &&
- statements === other.statements
- end
- end
-
- # LambdaVar represents the parameters being declared for a lambda. Effectively
- # this node is everything contained within the parentheses. This includes all
- # of the various parameter types, as well as block-local variable
- # declarations.
- #
- # -> (positional, optional = value, keyword:, █ local) do
- # end
- #
- class LambdaVar < Node
- # [Params] the parameters being declared with the block
- attr_reader :params
-
- # [Array[ Ident ]] the list of block-local variable declarations
- attr_reader :locals
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(params:, locals:, location:)
- @params = params
- @locals = locals
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_lambda_var(self)
- end
-
- def child_nodes
- [params, *locals]
- end
-
- def copy(params: nil, locals: nil, location: nil)
- node =
- LambdaVar.new(
- params: params || self.params,
- locals: locals || self.locals,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { params: params, locals: locals, location: location, comments: comments }
- end
-
- def empty?
- params.empty? && locals.empty?
- end
-
- def format(q)
- q.format(params)
-
- if locals.any?
- q.text("; ")
- q.seplist(locals, BlockVar::SEPARATOR) { |local| q.format(local) }
- end
- end
-
- def ===(other)
- other.is_a?(LambdaVar) && params === other.params &&
- ArrayMatch.call(locals, other.locals)
- end
- end
-
- # LBrace represents the use of a left brace, i.e., {.
- class LBrace < Node
- # [String] the left brace
- attr_reader :value
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:)
- @value = value
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_lbrace(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(value: nil, location: nil)
- node =
- LBrace.new(
- value: value || self.value,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location, comments: comments }
- end
-
- def format(q)
- q.text(value)
- end
-
- def ===(other)
- other.is_a?(LBrace) && value === other.value
- end
-
- # Because some nodes keep around a { token so that comments can be attached
- # to it if they occur in the source, oftentimes an LBrace is a child of
- # another node. This means it's required at initialization time. To make it
- # easier to create LBrace nodes without any specific value, this method
- # provides a default node.
- def self.default
- new(value: "{", location: Location.default)
- end
- end
-
- # LBracket represents the use of a left bracket, i.e., [.
- class LBracket < Node
- # [String] the left bracket
- attr_reader :value
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:)
- @value = value
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_lbracket(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(value: nil, location: nil)
- node =
- LBracket.new(
- value: value || self.value,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location, comments: comments }
- end
-
- def format(q)
- q.text(value)
- end
-
- def ===(other)
- other.is_a?(LBracket) && value === other.value
- end
-
- # Because some nodes keep around a [ token so that comments can be attached
- # to it if they occur in the source, oftentimes an LBracket is a child of
- # another node. This means it's required at initialization time. To make it
- # easier to create LBracket nodes without any specific value, this method
- # provides a default node.
- def self.default
- new(value: "[", location: Location.default)
- end
- end
-
- # LParen represents the use of a left parenthesis, i.e., (.
- class LParen < Node
- # [String] the left parenthesis
- attr_reader :value
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:)
- @value = value
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_lparen(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(value: nil, location: nil)
- node =
- LParen.new(
- value: value || self.value,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location, comments: comments }
- end
-
- def format(q)
- q.text(value)
- end
-
- def ===(other)
- other.is_a?(LParen) && value === other.value
- end
-
- # Because some nodes keep around a ( token so that comments can be attached
- # to it if they occur in the source, oftentimes an LParen is a child of
- # another node. This means it's required at initialization time. To make it
- # easier to create LParen nodes without any specific value, this method
- # provides a default node.
- def self.default
- new(value: "(", location: Location.default)
- end
- end
-
- # MAssign is a parent node of any kind of multiple assignment. This includes
- # splitting out variables on the left like:
- #
- # first, second, third = value
- #
- # as well as splitting out variables on the right, as in:
- #
- # value = first, second, third
- #
- # Both sides support splats, as well as variables following them. There's also
- # destructuring behavior that you can achieve with the following:
- #
- # first, = value
- #
- class MAssign < Node
- # [MLHS | MLHSParen] the target of the multiple assignment
- attr_reader :target
-
- # [Node] the value being assigned
- attr_reader :value
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(target:, value:, location:)
- @target = target
- @value = value
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_massign(self)
- end
-
- def child_nodes
- [target, value]
- end
-
- def copy(target: nil, value: nil, location: nil)
- node =
- MAssign.new(
- target: target || self.target,
- value: value || self.value,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { target: target, value: value, location: location, comments: comments }
- end
-
- def format(q)
- q.group do
- q.group { q.format(target) }
- q.text(" =")
- q.indent do
- q.breakable_space
- q.format(value)
- end
- end
- end
-
- def ===(other)
- other.is_a?(MAssign) && target === other.target && value === other.value
- end
- end
-
- # MethodAddBlock represents a method call with a block argument.
- #
- # method {}
- #
- class MethodAddBlock < Node
- # [ARef | CallNode | Command | CommandCall | Super | ZSuper] the method call
- attr_reader :call
-
- # [BlockNode] the block being sent with the method call
- attr_reader :block
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(call:, block:, location:)
- @call = call
- @block = block
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_method_add_block(self)
- end
-
- def child_nodes
- [call, block]
- end
-
- def copy(call: nil, block: nil, location: nil)
- node =
- MethodAddBlock.new(
- call: call || self.call,
- block: block || self.block,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { call: call, block: block, location: location, comments: comments }
- end
-
- def format(q)
- # If we're at the top of a call chain, then we're going to do some
- # specialized printing in case we can print it nicely. We _only_ do this
- # at the top of the chain to avoid weird recursion issues.
- if CallChainFormatter.chained?(call) &&
- !CallChainFormatter.chained?(q.parent)
- q.group do
- q
- .if_break { CallChainFormatter.new(self).format(q) }
- .if_flat { format_contents(q) }
- end
- else
- format_contents(q)
- end
- end
-
- def ===(other)
- other.is_a?(MethodAddBlock) && call === other.call &&
- block === other.block
- end
-
- def format_contents(q)
- q.format(call)
- q.format(block)
- end
- end
-
- # MLHS represents a list of values being destructured on the left-hand side
- # of a multiple assignment.
- #
- # first, second, third = value
- #
- class MLHS < Node
- # [
- # Array[
- # ARefField | ArgStar | ConstPathField | Field | Ident | MLHSParen |
- # TopConstField | VarField
- # ]
- # ] the parts of the left-hand side of a multiple assignment
- attr_reader :parts
-
- # [boolean] whether or not there is a trailing comma at the end of this
- # list, which impacts destructuring. It's an attr_accessor so that while
- # the syntax tree is being built it can be set by its parent node
- attr_accessor :comma
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(parts:, location:, comma: false)
- @parts = parts
- @comma = comma
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_mlhs(self)
- end
-
- def child_nodes
- parts
- end
-
- def copy(parts: nil, location: nil, comma: nil)
- node =
- MLHS.new(
- parts: parts || self.parts,
- location: location || self.location,
- comma: comma || self.comma
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { parts: parts, location: location, comma: comma, comments: comments }
- end
-
- def format(q)
- q.seplist(parts) { |part| q.format(part) }
- q.text(",") if comma
- end
-
- def ===(other)
- other.is_a?(MLHS) && ArrayMatch.call(parts, other.parts) &&
- comma === other.comma
- end
- end
-
- # MLHSParen represents parentheses being used to destruct values in a multiple
- # assignment on the left hand side.
- #
- # (left, right) = value
- #
- class MLHSParen < Node
- # [MLHS | MLHSParen] the contents inside of the parentheses
- attr_reader :contents
-
- # [boolean] whether or not there is a trailing comma at the end of this
- # list, which impacts destructuring. It's an attr_accessor so that while
- # the syntax tree is being built it can be set by its parent node
- attr_accessor :comma
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(contents:, location:, comma: false)
- @contents = contents
- @comma = comma
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_mlhs_paren(self)
- end
-
- def child_nodes
- [contents]
- end
-
- def copy(contents: nil, location: nil)
- node =
- MLHSParen.new(
- contents: contents || self.contents,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { contents: contents, location: location, comments: comments }
- end
-
- def format(q)
- parent = q.parent
-
- if parent.is_a?(MAssign) || parent.is_a?(MLHSParen)
- q.format(contents)
- q.text(",") if comma
- else
- q.text("(")
- q.group do
- q.indent do
- q.breakable_empty
- q.format(contents)
- end
-
- q.text(",") if comma
- q.breakable_empty
- end
- q.text(")")
- end
- end
-
- def ===(other)
- other.is_a?(MLHSParen) && contents === other.contents
- end
- end
-
- # ModuleDeclaration represents defining a module using the +module+ keyword.
- #
- # module Namespace
- # end
- #
- class ModuleDeclaration < Node
- # [ConstPathRef | ConstRef | TopConstRef] the name of the module
- attr_reader :constant
-
- # [BodyStmt] the expressions to be executed in the context of the module
- attr_reader :bodystmt
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(constant:, bodystmt:, location:)
- @constant = constant
- @bodystmt = bodystmt
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_module(self)
- end
-
- def child_nodes
- [constant, bodystmt]
- end
-
- def copy(constant: nil, bodystmt: nil, location: nil)
- node =
- ModuleDeclaration.new(
- constant: constant || self.constant,
- bodystmt: bodystmt || self.bodystmt,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- constant: constant,
- bodystmt: bodystmt,
- location: location,
- comments: comments
- }
- end
-
- def format(q)
- if bodystmt.empty?
- q.group do
- format_declaration(q)
- q.breakable_force
- q.text("end")
- end
- else
- q.group do
- format_declaration(q)
-
- q.indent do
- q.breakable_force
- q.format(bodystmt)
- end
-
- q.breakable_force
- q.text("end")
- end
- end
- end
-
- def ===(other)
- other.is_a?(ModuleDeclaration) && constant === other.constant &&
- bodystmt === other.bodystmt
- end
-
- private
-
- def format_declaration(q)
- q.group do
- q.text("module ")
- q.format(constant)
- end
- end
- end
-
- # MRHS represents the values that are being assigned on the right-hand side of
- # a multiple assignment.
- #
- # values = first, second, third
- #
- class MRHS < Node
- # [Array[Node]] the parts that are being assigned
- attr_reader :parts
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(parts:, location:)
- @parts = parts
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_mrhs(self)
- end
-
- def child_nodes
- parts
- end
-
- def copy(parts: nil, location: nil)
- node =
- MRHS.new(
- parts: parts || self.parts,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { parts: parts, location: location, comments: comments }
- end
-
- def format(q)
- q.seplist(parts) { |part| q.format(part) }
- end
-
- def ===(other)
- other.is_a?(MRHS) && ArrayMatch.call(parts, other.parts)
- end
- end
-
- # Next represents using the +next+ keyword.
- #
- # next
- #
- # The +next+ keyword can also optionally be called with an argument:
- #
- # next value
- #
- # +next+ can even be called with multiple arguments, but only if parentheses
- # are omitted, as in:
- #
- # next first, second, third
- #
- # If a single value is being given, parentheses can be used, as in:
- #
- # next(value)
- #
- class Next < Node
- # [Args] the arguments passed to the next keyword
- attr_reader :arguments
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(arguments:, location:)
- @arguments = arguments
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_next(self)
- end
-
- def child_nodes
- [arguments]
- end
-
- def copy(arguments: nil, location: nil)
- node =
- Next.new(
- arguments: arguments || self.arguments,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { arguments: arguments, location: location, comments: comments }
- end
-
- def format(q)
- FlowControlFormatter.new("next", self).format(q)
- end
-
- def ===(other)
- other.is_a?(Next) && arguments === other.arguments
- end
- end
-
- # Op represents an operator literal in the source.
- #
- # 1 + 2
- #
- # In the example above, the Op node represents the + operator.
- class Op < Node
- # [String] the operator
- attr_reader :value
-
- # [Symbol] the symbol version of the value
- attr_reader :name
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:)
- @value = value
- @name = value.to_sym
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_op(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(value: nil, location: nil)
- node =
- Op.new(value: value || self.value, location: location || self.location)
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location, comments: comments }
- end
-
- def format(q)
- q.text(value)
- end
-
- def ===(other)
- other.is_a?(Op) && value === other.value
- end
- end
-
- # OpAssign represents assigning a value to a variable or constant using an
- # operator like += or ||=.
- #
- # variable += value
- #
- class OpAssign < Node
- # [ARefField | ConstPathField | Field | TopConstField | VarField] the target
- # to assign the result of the expression to
- attr_reader :target
-
- # [Op] the operator being used for the assignment
- attr_reader :operator
-
- # [Node] the expression to be assigned
- attr_reader :value
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(target:, operator:, value:, location:)
- @target = target
- @operator = operator
- @value = value
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_opassign(self)
- end
-
- def child_nodes
- [target, operator, value]
- end
-
- def copy(target: nil, operator: nil, value: nil, location: nil)
- node =
- OpAssign.new(
- target: target || self.target,
- operator: operator || self.operator,
- value: value || self.value,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- target: target,
- operator: operator,
- value: value,
- location: location,
- comments: comments
- }
- end
-
- def format(q)
- q.group do
- q.format(target)
- q.text(" ")
- q.format(operator)
-
- if skip_indent?
- q.text(" ")
- q.format(value)
- else
- q.indent do
- q.breakable_space
- q.format(value)
- end
- end
- end
- end
-
- def ===(other)
- other.is_a?(OpAssign) && target === other.target &&
- operator === other.operator && value === other.value
- end
-
- private
-
- def skip_indent?
- target.comments.empty? &&
- (target.is_a?(ARefField) || AssignFormatting.skip_indent?(value))
- end
- end
-
- # If you have a modifier statement (for instance a modifier if statement or a
- # modifier while loop) there are times when you need to wrap the entire
- # statement in parentheses. This occurs when you have something like:
- #
- # foo[:foo] =
- # if bar?
- # baz
- # end
- #
- # Normally we would shorten this to an inline version, which would result in:
- #
- # foo[:foo] = baz if bar?
- #
- # but this actually has different semantic meaning. The first example will
- # result in a nil being inserted into the hash for the :foo key, whereas the
- # second example will result in an empty hash because the if statement applies
- # to the entire assignment.
- #
- # We can fix this in a couple of ways. We can use the then keyword, as in:
- #
- # foo[:foo] = if bar? then baz end
- #
- # But this isn't used very often. We can also just leave it as is with the
- # multi-line version, but for a short predicate and short value it looks
- # verbose. The last option and the one used here is to add parentheses on
- # both sides of the expression, as in:
- #
- # foo[:foo] = (baz if bar?)
- #
- # This approach maintains the nice conciseness of the inline version, while
- # keeping the correct semantic meaning.
- module Parentheses
- NODES = [
- Args,
- Assign,
- Assoc,
- Binary,
- CallNode,
- Defined,
- MAssign,
- OpAssign
- ].freeze
-
- def self.flat(q)
- return yield unless NODES.include?(q.parent.class)
-
- q.text("(")
- yield
- q.text(")")
- end
-
- def self.break(q)
- return yield unless NODES.include?(q.parent.class)
-
- q.text("(")
- q.indent do
- q.breakable_empty
- yield
- end
- q.breakable_empty
- q.text(")")
- end
- end
-
- # def on_operator_ambiguous(value)
- # value
- # end
-
- # Params represents defining parameters on a method or lambda.
- #
- # def method(param) end
- #
- class Params < Node
- # Formats the optional position of the parameters. This includes the label,
- # as well as the default value.
- class OptionalFormatter
- # [Ident] the name of the parameter
- attr_reader :name
-
- # [Node] the value of the parameter
- attr_reader :value
-
- def initialize(name, value)
- @name = name
- @value = value
- end
-
- def comments
- []
- end
-
- def format(q)
- q.format(name)
- q.text(" = ")
- q.format(value)
- end
- end
-
- # Formats the keyword position of the parameters. This includes the label,
- # as well as an optional default value.
- class KeywordFormatter
- # [Ident] the name of the parameter
- attr_reader :name
-
- # [nil | Node] the value of the parameter
- attr_reader :value
-
- def initialize(name, value)
- @name = name
- @value = value
- end
-
- def comments
- []
- end
-
- def format(q)
- q.format(name)
-
- if value
- q.text(" ")
- q.format(value)
- end
- end
- end
-
- # Formats the keyword_rest position of the parameters. This can be the **nil
- # syntax, the ... syntax, or the ** syntax.
- class KeywordRestFormatter
- # [:nil | ArgsForward | KwRestParam] the value of the parameter
- attr_reader :value
-
- def initialize(value)
- @value = value
- end
-
- def comments
- []
- end
-
- def format(q)
- value == :nil ? q.text("**nil") : q.format(value)
- end
- end
-
- # [Array[ Ident | MLHSParen ]] any required parameters
- attr_reader :requireds
-
- # [Array[ [ Ident, Node ] ]] any optional parameters and their default
- # values
- attr_reader :optionals
-
- # [nil | ArgsForward | ExcessedComma | RestParam] the optional rest
- # parameter
- attr_reader :rest
-
- # [Array[ Ident | MLHSParen ]] any positional parameters that exist after a
- # rest parameter
- attr_reader :posts
-
- # [Array[ [ Label, nil | Node ] ]] any keyword parameters and their
- # optional default values
- attr_reader :keywords
-
- # [nil | :nil | ArgsForward | KwRestParam] the optional keyword rest
- # parameter
- attr_reader :keyword_rest
-
- # [nil | BlockArg] the optional block parameter
- attr_reader :block
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(
- location:,
- requireds: [],
- optionals: [],
- rest: nil,
- posts: [],
- keywords: [],
- keyword_rest: nil,
- block: nil
- )
- @requireds = requireds
- @optionals = optionals
- @rest = rest
- @posts = posts
- @keywords = keywords
- @keyword_rest = keyword_rest
- @block = block
- @location = location
- @comments = []
- end
-
- # Params nodes are the most complicated in the tree. Occasionally you want
- # to know if they are "empty", which means not having any parameters
- # declared. This logic accesses every kind of parameter and determines if
- # it's missing.
- def empty?
- requireds.empty? && optionals.empty? && !rest && posts.empty? &&
- keywords.empty? && !keyword_rest && !block
- end
-
- def accept(visitor)
- visitor.visit_params(self)
- end
-
- def child_nodes
- keyword_rest = self.keyword_rest
-
- [
- *requireds,
- *optionals.flatten(1),
- rest,
- *posts,
- *keywords.flatten(1),
- (keyword_rest if keyword_rest != :nil),
- block
- ]
- end
-
- def copy(
- location: nil,
- requireds: nil,
- optionals: nil,
- rest: nil,
- posts: nil,
- keywords: nil,
- keyword_rest: nil,
- block: nil
- )
- node =
- Params.new(
- location: location || self.location,
- requireds: requireds || self.requireds,
- optionals: optionals || self.optionals,
- rest: rest || self.rest,
- posts: posts || self.posts,
- keywords: keywords || self.keywords,
- keyword_rest: keyword_rest || self.keyword_rest,
- block: block || self.block
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- location: location,
- requireds: requireds,
- optionals: optionals,
- rest: rest,
- posts: posts,
- keywords: keywords,
- keyword_rest: keyword_rest,
- block: block,
- comments: comments
- }
- end
-
- def format(q)
- rest = self.rest
- keyword_rest = self.keyword_rest
-
- parts = [
- *requireds,
- *optionals.map { |(name, value)| OptionalFormatter.new(name, value) }
- ]
-
- parts << rest if rest && !rest.is_a?(ExcessedComma)
- parts.concat(posts)
- parts.concat(
- keywords.map { |(name, value)| KeywordFormatter.new(name, value) }
- )
-
- parts << KeywordRestFormatter.new(keyword_rest) if keyword_rest
- parts << block if block
-
- if parts.empty?
- q.nest(0) { format_contents(q, parts) }
- return
- end
-
- if q.parent.is_a?(DefNode)
- q.nest(0) do
- q.text("(")
- q.group do
- q.indent do
- q.breakable_empty
- format_contents(q, parts)
- end
- q.breakable_empty
- end
- q.text(")")
- end
- else
- q.nest(0) { format_contents(q, parts) }
- end
- end
-
- def ===(other)
- other.is_a?(Params) && ArrayMatch.call(requireds, other.requireds) &&
- optionals.length == other.optionals.length &&
- optionals
- .zip(other.optionals)
- .all? { |left, right| ArrayMatch.call(left, right) } &&
- rest === other.rest && ArrayMatch.call(posts, other.posts) &&
- keywords.length == other.keywords.length &&
- keywords
- .zip(other.keywords)
- .all? { |left, right| ArrayMatch.call(left, right) } &&
- keyword_rest === other.keyword_rest && block === other.block
- end
-
- # Returns a range representing the possible number of arguments accepted
- # by this params node not including the block. For example:
- #
- # def foo(a, b = 1, c:, d: 2, &block)
- # ...
- # end
- #
- # has arity 2..4.
- #
- def arity
- optional_keywords = keywords.count { |_label, value| value }
-
- lower_bound =
- requireds.length + posts.length + keywords.length - optional_keywords
-
- upper_bound =
- if keyword_rest.nil? && rest.nil?
- lower_bound + optionals.length + optional_keywords
- end
-
- lower_bound..upper_bound
- end
-
- private
-
- def format_contents(q, parts)
- q.seplist(parts) { |part| q.format(part) }
- q.format(rest) if rest.is_a?(ExcessedComma)
- end
- end
-
- # Paren represents using balanced parentheses in a couple places in a Ruby
- # program. In general parentheses can be used anywhere a Ruby expression can
- # be used.
- #
- # (1 + 2)
- #
- class Paren < Node
- # [LParen] the left parenthesis that opened this statement
- attr_reader :lparen
-
- # [nil | Node] the expression inside the parentheses
- attr_reader :contents
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(lparen:, contents:, location:)
- @lparen = lparen
- @contents = contents
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_paren(self)
- end
-
- def child_nodes
- [lparen, contents]
- end
-
- def copy(lparen: nil, contents: nil, location: nil)
- node =
- Paren.new(
- lparen: lparen || self.lparen,
- contents: contents || self.contents,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- lparen: lparen,
- contents: contents,
- location: location,
- comments: comments
- }
- end
-
- def format(q)
- contents = self.contents
-
- q.group do
- q.format(lparen)
-
- if contents && (!contents.is_a?(Params) || !contents.empty?)
- q.indent do
- q.breakable_empty
- q.format(contents)
- end
- end
-
- q.breakable_empty
- q.text(")")
- end
- end
-
- def ===(other)
- other.is_a?(Paren) && lparen === other.lparen &&
- contents === other.contents
- end
- end
-
- # Period represents the use of the +.+ operator. It is usually found in method
- # calls.
- class Period < Node
- # [String] the period
- attr_reader :value
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:)
- @value = value
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_period(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(value: nil, location: nil)
- node =
- Period.new(
- value: value || self.value,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location, comments: comments }
- end
-
- def format(q)
- q.text(value)
- end
-
- def ===(other)
- other.is_a?(Period) && value === other.value
- end
- end
-
- # Program represents the overall syntax tree.
- class Program < Node
- # [Statements] the top-level expressions of the program
- attr_reader :statements
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(statements:, location:)
- @statements = statements
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_program(self)
- end
-
- def child_nodes
- [statements]
- end
-
- def copy(statements: nil, location: nil)
- node =
- Program.new(
- statements: statements || self.statements,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { statements: statements, location: location, comments: comments }
- end
-
- def format(q)
- q.format(statements)
-
- # We're going to put a newline on the end so that it always has one unless
- # it ends with the special __END__ syntax. In that case we want to
- # replicate the text exactly so we will just let it be.
- q.breakable_force unless statements.body.last.is_a?(EndContent)
- end
-
- def ===(other)
- other.is_a?(Program) && statements === other.statements
- end
- end
-
- # QSymbols represents a symbol literal array without interpolation.
- #
- # %i[one two three]
- #
- class QSymbols < Node
- # [QSymbolsBeg] the token that opens this array literal
- attr_reader :beginning
-
- # [Array[ TStringContent ]] the elements of the array
- attr_reader :elements
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(beginning:, elements:, location:)
- @beginning = beginning
- @elements = elements
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_qsymbols(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(beginning: nil, elements: nil, location: nil)
- node =
- QSymbols.new(
- beginning: beginning || self.beginning,
- elements: elements || self.elements,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- beginning: beginning,
- elements: elements,
- location: location,
- comments: comments
- }
- end
-
- def format(q)
- opening, closing = "%i[", "]"
-
- if elements.any? { |element| element.match?(/[\[\]]/) }
- opening = beginning.value
- closing = Quotes.matching(opening[2])
- end
-
- q.text(opening)
- q.group do
- q.indent do
- q.breakable_empty
- q.seplist(
- elements,
- ArrayLiteral::BREAKABLE_SPACE_SEPARATOR
- ) { |element| q.format(element) }
- end
- q.breakable_empty
- end
- q.text(closing)
- end
-
- def ===(other)
- other.is_a?(QSymbols) && beginning === other.beginning &&
- ArrayMatch.call(elements, other.elements)
- end
- end
-
- # QSymbolsBeg represents the beginning of a symbol literal array.
- #
- # %i[one two three]
- #
- # In the snippet above, QSymbolsBeg represents the "%i[" token. Note that
- # these kinds of arrays can start with a lot of different delimiter types
- # (e.g., %i| or %i<).
- class QSymbolsBeg < Node
- # [String] the beginning of the array literal
- attr_reader :value
-
- def initialize(value:, location:)
- @value = value
- @location = location
- end
-
- def accept(visitor)
- visitor.visit_qsymbols_beg(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(value: nil, location: nil)
- QSymbolsBeg.new(
- value: value || self.value,
- location: location || self.location
- )
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location }
- end
-
- def ===(other)
- other.is_a?(QSymbolsBeg) && value === other.value
- end
- end
-
- # QWords represents a string literal array without interpolation.
- #
- # %w[one two three]
- #
- class QWords < Node
- # [QWordsBeg] the token that opens this array literal
- attr_reader :beginning
-
- # [Array[ TStringContent ]] the elements of the array
- attr_reader :elements
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(beginning:, elements:, location:)
- @beginning = beginning
- @elements = elements
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_qwords(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(beginning: nil, elements: nil, location: nil)
- QWords.new(
- beginning: beginning || self.beginning,
- elements: elements || self.elements,
- location: location || self.location
- )
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- beginning: beginning,
- elements: elements,
- location: location,
- comments: comments
- }
- end
-
- def format(q)
- opening, closing = "%w[", "]"
-
- if elements.any? { |element| element.match?(/[\[\]]/) }
- opening = beginning.value
- closing = Quotes.matching(opening[2])
- end
-
- q.text(opening)
- q.group do
- q.indent do
- q.breakable_empty
- q.seplist(
- elements,
- ArrayLiteral::BREAKABLE_SPACE_SEPARATOR
- ) { |element| q.format(element) }
- end
- q.breakable_empty
- end
- q.text(closing)
- end
-
- def ===(other)
- other.is_a?(QWords) && beginning === other.beginning &&
- ArrayMatch.call(elements, other.elements)
- end
- end
-
- # QWordsBeg represents the beginning of a string literal array.
- #
- # %w[one two three]
- #
- # In the snippet above, QWordsBeg represents the "%w[" token. Note that these
- # kinds of arrays can start with a lot of different delimiter types (e.g.,
- # %w| or %w<).
- class QWordsBeg < Node
- # [String] the beginning of the array literal
- attr_reader :value
-
- def initialize(value:, location:)
- @value = value
- @location = location
- end
-
- def accept(visitor)
- visitor.visit_qwords_beg(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(value: nil, location: nil)
- QWordsBeg.new(
- value: value || self.value,
- location: location || self.location
- )
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location }
- end
-
- def ===(other)
- other.is_a?(QWordsBeg) && value === other.value
- end
- end
-
- # RationalLiteral represents the use of a rational number literal.
- #
- # 1r
- #
- class RationalLiteral < Node
- # [String] the rational number literal
- attr_reader :value
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:)
- @value = value
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_rational(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(value: nil, location: nil)
- node =
- RationalLiteral.new(
- value: value || self.value,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location, comments: comments }
- end
-
- def format(q)
- q.text(value)
- end
-
- def ===(other)
- other.is_a?(RationalLiteral) && value === other.value
- end
- end
-
- # RBrace represents the use of a right brace, i.e., +++.
- class RBrace < Node
- # [String] the right brace
- attr_reader :value
-
- def initialize(value:, location:)
- @value = value
- @location = location
- end
-
- def accept(visitor)
- visitor.visit_rbrace(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(value: nil, location: nil)
- RBrace.new(
- value: value || self.value,
- location: location || self.location
- )
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location }
- end
-
- def ===(other)
- other.is_a?(RBrace) && value === other.value
- end
- end
-
- # RBracket represents the use of a right bracket, i.e., +]+.
- class RBracket < Node
- # [String] the right bracket
- attr_reader :value
-
- def initialize(value:, location:)
- @value = value
- @location = location
- end
-
- def accept(visitor)
- visitor.visit_rbracket(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(value: nil, location: nil)
- RBracket.new(
- value: value || self.value,
- location: location || self.location
- )
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location }
- end
-
- def ===(other)
- other.is_a?(RBracket) && value === other.value
- end
- end
-
- # Redo represents the use of the +redo+ keyword.
- #
- # redo
- #
- class Redo < Node
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(location:)
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_redo(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(location: nil)
- node = Redo.new(location: location || self.location)
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { location: location, comments: comments }
- end
-
- def format(q)
- q.text("redo")
- end
-
- def ===(other)
- other.is_a?(Redo)
- end
- end
-
- # RegexpContent represents the body of a regular expression.
- #
- # /.+ #{pattern} .+/
- #
- # In the example above, a RegexpContent node represents everything contained
- # within the forward slashes.
- class RegexpContent < Node
- # [String] the opening of the regular expression
- attr_reader :beginning
-
- # [Array[ StringDVar | StringEmbExpr | TStringContent ]] the parts of the
- # regular expression
- attr_reader :parts
-
- def initialize(beginning:, parts:, location:)
- @beginning = beginning
- @parts = parts
- @location = location
- end
-
- def accept(visitor)
- visitor.visit_regexp_content(self)
- end
-
- def child_nodes
- parts
- end
-
- def copy(beginning: nil, parts: nil, location: nil)
- RegexpContent.new(
- beginning: beginning || self.beginning,
- parts: parts || self.parts,
- location: location || self.location
- )
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { beginning: beginning, parts: parts, location: location }
- end
-
- def ===(other)
- other.is_a?(RegexpContent) && beginning === other.beginning &&
- parts === other.parts
- end
- end
-
- # RegexpBeg represents the start of a regular expression literal.
- #
- # /.+/
- #
- # In the example above, RegexpBeg represents the first / token. Regular
- # expression literals can also be declared using the %r syntax, as in:
- #
- # %r{.+}
- #
- class RegexpBeg < Node
- # [String] the beginning of the regular expression
- attr_reader :value
-
- def initialize(value:, location:)
- @value = value
- @location = location
- end
-
- def accept(visitor)
- visitor.visit_regexp_beg(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(value: nil, location: nil)
- RegexpBeg.new(
- value: value || self.value,
- location: location || self.location
- )
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location }
- end
-
- def ===(other)
- other.is_a?(RegexpBeg) && value === other.value
- end
- end
-
- # RegexpEnd represents the end of a regular expression literal.
- #
- # /.+/m
- #
- # In the example above, the RegexpEnd event represents the /m at the end of
- # the regular expression literal. You can also declare regular expression
- # literals using %r, as in:
- #
- # %r{.+}m
- #
- class RegexpEnd < Node
- # [String] the end of the regular expression
- attr_reader :value
-
- def initialize(value:, location:)
- @value = value
- @location = location
- end
-
- def accept(visitor)
- visitor.visit_regexp_end(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(value: nil, location: nil)
- RegexpEnd.new(
- value: value || self.value,
- location: location || self.location
- )
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location }
- end
-
- def ===(other)
- other.is_a?(RegexpEnd) && value === other.value
- end
- end
-
- # RegexpLiteral represents a regular expression literal.
- #
- # /.+/
- #
- class RegexpLiteral < Node
- # [String] the beginning of the regular expression literal
- attr_reader :beginning
-
- # [String] the ending of the regular expression literal
- attr_reader :ending
-
- # [Array[ StringEmbExpr | StringDVar | TStringContent ]] the parts of the
- # regular expression literal
- attr_reader :parts
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(beginning:, ending:, parts:, location:)
- @beginning = beginning
- @ending = ending
- @parts = parts
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_regexp_literal(self)
- end
-
- def child_nodes
- parts
- end
-
- def copy(beginning: nil, ending: nil, parts: nil, location: nil)
- node =
- RegexpLiteral.new(
- beginning: beginning || self.beginning,
- ending: ending || self.ending,
- parts: parts || self.parts,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- beginning: beginning,
- ending: ending,
- options: options,
- parts: parts,
- location: location,
- comments: comments
- }
- end
-
- def format(q)
- braces = ambiguous?(q) || include?(%r{/})
-
- if braces && include?(/[{}]/)
- q.group do
- q.text(beginning)
- q.format_each(parts)
- q.text(ending)
- end
- elsif braces
- q.group do
- q.text("%r{")
-
- if beginning == "/"
- # If we're changing from a forward slash to a %r{, then we can
- # replace any escaped forward slashes with regular forward slashes.
- parts.each do |part|
- if part.is_a?(TStringContent)
- q.text(part.value.gsub("\\/", "/"))
- else
- q.format(part)
- end
- end
- else
- q.format_each(parts)
- end
-
- q.text("}")
- q.text(options)
- end
- else
- q.group do
- q.text("/")
- q.format_each(parts)
- q.text("/")
- q.text(options)
- end
- end
- end
-
- def ===(other)
- other.is_a?(RegexpLiteral) && beginning === other.beginning &&
- ending === other.ending && options === other.options &&
- ArrayMatch.call(parts, other.parts)
- end
-
- def options
- ending[1..]
- end
-
- private
-
- def include?(pattern)
- parts.any? do |part|
- part.is_a?(TStringContent) && part.value.match?(pattern)
- end
- end
-
- # If the first part of this regex is plain string content, we have a space
- # or an =, and we're contained within a command or command_call node, then
- # we want to use braces because otherwise we could end up with an ambiguous
- # operator, e.g. foo / bar/ or foo /=bar/
- def ambiguous?(q)
- return false if parts.empty?
- part = parts.first
-
- part.is_a?(TStringContent) && part.value.start_with?(" ", "=") &&
- q.parents.any? { |node| node.is_a?(Command) || node.is_a?(CommandCall) }
- end
- end
-
- # RescueEx represents the list of exceptions being rescued in a rescue clause.
- #
- # begin
- # rescue Exception => exception
- # end
- #
- class RescueEx < Node
- # [nil | Node] the list of exceptions being rescued
- attr_reader :exceptions
-
- # [nil | Field | VarField] the expression being used to capture the raised
- # exception
- attr_reader :variable
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(exceptions:, variable:, location:)
- @exceptions = exceptions
- @variable = variable
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_rescue_ex(self)
- end
-
- def child_nodes
- [*exceptions, variable]
- end
-
- def copy(exceptions: nil, variable: nil, location: nil)
- node =
- RescueEx.new(
- exceptions: exceptions || self.exceptions,
- variable: variable || self.variable,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- exceptions: exceptions,
- variable: variable,
- location: location,
- comments: comments
- }
- end
-
- def format(q)
- q.group do
- if exceptions
- q.text(" ")
- q.format(exceptions)
- end
-
- if variable
- q.text(" => ")
- q.format(variable)
- end
- end
- end
-
- def ===(other)
- other.is_a?(RescueEx) && exceptions === other.exceptions &&
- variable === other.variable
- end
- end
-
- # Rescue represents the use of the rescue keyword inside of a BodyStmt node.
- #
- # begin
- # rescue
- # end
- #
- class Rescue < Node
- # [Kw] the rescue keyword
- attr_reader :keyword
-
- # [nil | RescueEx] the exceptions being rescued
- attr_reader :exception
-
- # [Statements] the expressions to evaluate when an error is rescued
- attr_reader :statements
-
- # [nil | Rescue] the optional next clause in the chain
- attr_reader :consequent
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(keyword:, exception:, statements:, consequent:, location:)
- @keyword = keyword
- @exception = exception
- @statements = statements
- @consequent = consequent
- @location = location
- @comments = []
- end
-
- def bind_end(end_char, end_column)
- @location =
- Location.new(
- start_line: location.start_line,
- start_char: location.start_char,
- start_column: location.start_column,
- end_line: location.end_line,
- end_char: end_char,
- end_column: end_column
- )
-
- if (next_node = consequent)
- next_node.bind_end(end_char, end_column)
- statements.bind_end(
- next_node.location.start_char,
- next_node.location.start_column
- )
- else
- statements.bind_end(end_char, end_column)
- end
- end
-
- def accept(visitor)
- visitor.visit_rescue(self)
- end
-
- def child_nodes
- [keyword, exception, statements, consequent]
- end
-
- def copy(
- keyword: nil,
- exception: nil,
- statements: nil,
- consequent: nil,
- location: nil
- )
- node =
- Rescue.new(
- keyword: keyword || self.keyword,
- exception: exception || self.exception,
- statements: statements || self.statements,
- consequent: consequent || self.consequent,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- keyword: keyword,
- exception: exception,
- statements: statements,
- consequent: consequent,
- location: location,
- comments: comments
- }
- end
-
- def format(q)
- q.group do
- q.format(keyword)
-
- if exception
- q.nest(keyword.value.length + 1) { q.format(exception) }
- else
- q.text(" StandardError")
- end
-
- unless statements.empty?
- q.indent do
- q.breakable_force
- q.format(statements)
- end
- end
-
- if consequent
- q.breakable_force
- q.format(consequent)
- end
- end
- end
-
- def ===(other)
- other.is_a?(Rescue) && keyword === other.keyword &&
- exception === other.exception && statements === other.statements &&
- consequent === other.consequent
- end
- end
-
- # RescueMod represents the use of the modifier form of a +rescue+ clause.
- #
- # expression rescue value
- #
- class RescueMod < Node
- # [Node] the expression to execute
- attr_reader :statement
-
- # [Node] the value to use if the executed expression raises an error
- attr_reader :value
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(statement:, value:, location:)
- @statement = statement
- @value = value
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_rescue_mod(self)
- end
-
- def child_nodes
- [statement, value]
- end
-
- def copy(statement: nil, value: nil, location: nil)
- node =
- RescueMod.new(
- statement: statement || self.statement,
- value: value || self.value,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- statement: statement,
- value: value,
- location: location,
- comments: comments
- }
- end
-
- def format(q)
- q.text("begin")
- q.group do
- q.indent do
- q.breakable_force
- q.format(statement)
- end
- q.breakable_force
- q.text("rescue StandardError")
- q.indent do
- q.breakable_force
- q.format(value)
- end
- q.breakable_force
- end
- q.text("end")
- end
-
- def ===(other)
- other.is_a?(RescueMod) && statement === other.statement &&
- value === other.value
- end
- end
-
- # RestParam represents defining a parameter in a method definition that
- # accepts all remaining positional parameters.
- #
- # def method(*rest) end
- #
- class RestParam < Node
- # [nil | Ident] the name of the parameter
- attr_reader :name
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(name:, location:)
- @name = name
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_rest_param(self)
- end
-
- def child_nodes
- [name]
- end
-
- def copy(name: nil, location: nil)
- node =
- RestParam.new(
- name: name || self.name,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { name: name, location: location, comments: comments }
- end
-
- def format(q)
- q.text("*")
- q.format(name) if name
- end
-
- def ===(other)
- other.is_a?(RestParam) && name === other.name
- end
- end
-
- # Retry represents the use of the +retry+ keyword.
- #
- # retry
- #
- class Retry < Node
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(location:)
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_retry(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(location: nil)
- node = Retry.new(location: location || self.location)
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { location: location, comments: comments }
- end
-
- def format(q)
- q.text("retry")
- end
-
- def ===(other)
- other.is_a?(Retry)
- end
- end
-
- # Return represents using the +return+ keyword with arguments.
- #
- # return value
- #
- class ReturnNode < Node
- # [nil | Args] the arguments being passed to the keyword
- attr_reader :arguments
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(arguments:, location:)
- @arguments = arguments
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_return(self)
- end
-
- def child_nodes
- [arguments]
- end
-
- def copy(arguments: nil, location: nil)
- node =
- ReturnNode.new(
- arguments: arguments || self.arguments,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { arguments: arguments, location: location, comments: comments }
- end
-
- def format(q)
- FlowControlFormatter.new("return", self).format(q)
- end
-
- def ===(other)
- other.is_a?(ReturnNode) && arguments === other.arguments
- end
- end
-
- # RParen represents the use of a right parenthesis, i.e., +)+.
- class RParen < Node
- # [String] the parenthesis
- attr_reader :value
-
- def initialize(value:, location:)
- @value = value
- @location = location
- end
-
- def accept(visitor)
- visitor.visit_rparen(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(value: nil, location: nil)
- RParen.new(
- value: value || self.value,
- location: location || self.location
- )
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location }
- end
-
- def ===(other)
- other.is_a?(RParen) && value === other.value
- end
- end
-
- # SClass represents a block of statements that should be evaluated within the
- # context of the singleton class of an object. It's frequently used to define
- # singleton methods.
- #
- # class << self
- # end
- #
- class SClass < Node
- # [Node] the target of the singleton class to enter
- attr_reader :target
-
- # [BodyStmt] the expressions to be executed
- attr_reader :bodystmt
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(target:, bodystmt:, location:)
- @target = target
- @bodystmt = bodystmt
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_sclass(self)
- end
-
- def child_nodes
- [target, bodystmt]
- end
-
- def copy(target: nil, bodystmt: nil, location: nil)
- node =
- SClass.new(
- target: target || self.target,
- bodystmt: bodystmt || self.bodystmt,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- target: target,
- bodystmt: bodystmt,
- location: location,
- comments: comments
- }
- end
-
- def format(q)
- q.text("class << ")
- q.group do
- q.format(target)
- q.indent do
- q.breakable_force
- q.format(bodystmt)
- end
- q.breakable_force
- end
- q.text("end")
- end
-
- def ===(other)
- other.is_a?(SClass) && target === other.target &&
- bodystmt === other.bodystmt
- end
- end
-
- # Everything that has a block of code inside of it has a list of statements.
- # Normally we would just track those as a node that has an array body, but we
- # have some special handling in order to handle empty statement lists. They
- # need to have the right location information, so all of the parent node of
- # stmts nodes will report back down the location information. We then
- # propagate that onto void_stmt nodes inside the stmts in order to make sure
- # all comments get printed appropriately.
- class Statements < Node
- # [Array[ Node ]] the list of expressions contained within this node
- attr_reader :body
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(body:, location:)
- @body = body
- @location = location
- @comments = []
- end
-
- def bind(parser, start_char, start_column, end_char, end_column)
- @location =
- Location.new(
- start_line: location.start_line,
- start_char: start_char,
- start_column: start_column,
- end_line: location.end_line,
- end_char: end_char,
- end_column: end_column
- )
-
- if (void_stmt = body[0]).is_a?(VoidStmt)
- location = void_stmt.location
- location =
- Location.new(
- start_line: location.start_line,
- start_char: start_char,
- start_column: start_column,
- end_line: location.end_line,
- end_char: start_char,
- end_column: end_column
- )
-
- body[0] = VoidStmt.new(location: location)
- end
-
- attach_comments(parser, start_char, end_char)
- end
-
- def bind_end(end_char, end_column)
- @location =
- Location.new(
- start_line: location.start_line,
- start_char: location.start_char,
- start_column: location.start_column,
- end_line: location.end_line,
- end_char: end_char,
- end_column: end_column
- )
- end
-
- def empty?
- body.all? do |statement|
- statement.is_a?(VoidStmt) && statement.comments.empty?
- end
- end
-
- def accept(visitor)
- visitor.visit_statements(self)
- end
-
- def child_nodes
- body
- end
-
- def copy(body: nil, location: nil)
- node =
- Statements.new(
- body: body || self.body,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { body: body, location: location, comments: comments }
- end
-
- def format(q)
- line = nil
-
- # This handles a special case where you've got a block of statements where
- # the only value is a comment. In that case a lot of nodes like
- # brace_block will attempt to format as a single line, but since that
- # wouldn't work with a comment, we intentionally break the parent group.
- if body.length == 2
- void_stmt, comment = body
-
- if void_stmt.is_a?(VoidStmt) && comment.is_a?(Comment)
- q.format(comment)
- q.break_parent
- return
- end
- end
-
- previous = nil
- body.each do |statement|
- next if statement.is_a?(VoidStmt)
-
- if line.nil?
- q.format(statement)
- elsif (statement.location.start_line - line) > 1
- q.breakable_force
- q.breakable_force
- q.format(statement)
- elsif (statement.is_a?(VCall) && statement.access_control?) ||
- (previous.is_a?(VCall) && previous.access_control?)
- q.breakable_force
- q.breakable_force
- q.format(statement)
- elsif statement.location.start_line != line
- q.breakable_force
- q.format(statement)
- elsif !q.parent.is_a?(StringEmbExpr)
- q.breakable_force
- q.format(statement)
- else
- q.text("; ")
- q.format(statement)
- end
-
- line = statement.location.end_line
- previous = statement
- end
- end
-
- def ===(other)
- other.is_a?(Statements) && ArrayMatch.call(body, other.body)
- end
-
- private
-
- # As efficiently as possible, gather up all of the comments that have been
- # found while this statements list was being parsed and add them into the
- # body.
- def attach_comments(parser, start_char, end_char)
- parser_comments = parser.comments
-
- comment_index = 0
- body_index = 0
-
- while comment_index < parser_comments.size
- comment = parser_comments[comment_index]
- location = comment.location
-
- if !comment.inline? && (start_char <= location.start_char) &&
- (end_char >= location.end_char) && !comment.ignore?
- while (node = body[body_index]) &&
- (
- node.is_a?(VoidStmt) ||
- node.location.start_char < location.start_char
- )
- body_index += 1
- end
-
- if body_index != 0 &&
- body[body_index - 1].location.start_char < location.start_char &&
- body[body_index - 1].location.end_char > location.start_char
- # The previous node entirely encapsules the comment, so we don't
- # want to attach it here since it will get attached normally. This
- # is mostly in the case of hash and array literals.
- comment_index += 1
- else
- parser_comments.delete_at(comment_index)
- body.insert(body_index, comment)
- end
- else
- comment_index += 1
- end
- end
- end
- end
-
- # StringContent represents the contents of a string-like value.
- #
- # "string"
- #
- class StringContent < Node
- # [Array[ StringEmbExpr | StringDVar | TStringContent ]] the parts of the
- # string
- attr_reader :parts
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(parts:, location:)
- @parts = parts
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_string_content(self)
- end
-
- def child_nodes
- parts
- end
-
- def copy(parts: nil, location: nil)
- StringContent.new(
- parts: parts || self.parts,
- location: location || self.location
- )
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { parts: parts, location: location }
- end
-
- def ===(other)
- other.is_a?(StringContent) && ArrayMatch.call(parts, other.parts)
- end
-
- def format(q)
- q.text(q.quote)
- q.group do
- parts.each do |part|
- if part.is_a?(TStringContent)
- value = Quotes.normalize(part.value, q.quote)
- first = true
-
- value.each_line(chomp: true) do |line|
- if first
- first = false
- else
- q.breakable_return
- end
-
- q.text(line)
- end
-
- q.breakable_return if value.end_with?("\n")
- else
- q.format(part)
- end
- end
- end
- q.text(q.quote)
- end
- end
-
- # StringConcat represents concatenating two strings together using a backward
- # slash.
- #
- # "first" \
- # "second"
- #
- class StringConcat < Node
- # [Heredoc | StringConcat | StringLiteral] the left side of the
- # concatenation
- attr_reader :left
-
- # [StringLiteral] the right side of the concatenation
- attr_reader :right
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(left:, right:, location:)
- @left = left
- @right = right
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_string_concat(self)
- end
-
- def child_nodes
- [left, right]
- end
-
- def copy(left: nil, right: nil, location: nil)
- node =
- StringConcat.new(
- left: left || self.left,
- right: right || self.right,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { left: left, right: right, location: location, comments: comments }
- end
-
- def format(q)
- q.group do
- q.format(left)
- q.text(" \\")
- q.indent do
- q.breakable_force
- q.format(right)
- end
- end
- end
-
- def ===(other)
- other.is_a?(StringConcat) && left === other.left && right === other.right
- end
- end
-
- # StringDVar represents shorthand interpolation of a variable into a string.
- # It allows you to take an instance variable, class variable, or global
- # variable and omit the braces when interpolating.
- #
- # "#@variable"
- #
- class StringDVar < Node
- # [Backref | VarRef] the variable being interpolated
- attr_reader :variable
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(variable:, location:)
- @variable = variable
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_string_dvar(self)
- end
-
- def child_nodes
- [variable]
- end
-
- def copy(variable: nil, location: nil)
- node =
- StringDVar.new(
- variable: variable || self.variable,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { variable: variable, location: location, comments: comments }
- end
-
- def format(q)
- q.text('#{')
- q.format(variable)
- q.text("}")
- end
-
- def ===(other)
- other.is_a?(StringDVar) && variable === other.variable
- end
- end
-
- # StringEmbExpr represents interpolated content. It can be contained within a
- # couple of different parent nodes, including regular expressions, strings,
- # and dynamic symbols.
- #
- # "string #{expression}"
- #
- class StringEmbExpr < Node
- # [Statements] the expressions to be interpolated
- attr_reader :statements
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(statements:, location:)
- @statements = statements
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_string_embexpr(self)
- end
-
- def child_nodes
- [statements]
- end
-
- def copy(statements: nil, location: nil)
- node =
- StringEmbExpr.new(
- statements: statements || self.statements,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { statements: statements, location: location, comments: comments }
- end
-
- def format(q)
- if location.start_line == location.end_line
- # If the contents of this embedded expression were originally on the
- # same line in the source, then we're going to leave them in place and
- # assume that's the way the developer wanted this expression
- # represented.
- q.remove_breaks(
- q.group do
- q.text('#{')
- q.format(statements)
- q.text("}")
- end
- )
- else
- q.group do
- q.text('#{')
- q.indent do
- q.breakable_empty
- q.format(statements)
- end
- q.breakable_empty
- q.text("}")
- end
- end
- end
-
- def ===(other)
- other.is_a?(StringEmbExpr) && statements === other.statements
- end
- end
-
- # StringLiteral represents a string literal.
- #
- # "string"
- #
- class StringLiteral < Node
- # [Array[ StringEmbExpr | StringDVar | TStringContent ]] the parts of the
- # string literal
- attr_reader :parts
-
- # [nil | String] which quote was used by the string literal
- attr_reader :quote
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(parts:, quote:, location:)
- @parts = parts
- @quote = quote
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_string_literal(self)
- end
-
- def child_nodes
- parts
- end
-
- def copy(parts: nil, quote: nil, location: nil)
- node =
- StringLiteral.new(
- parts: parts || self.parts,
- quote: quote || self.quote,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { parts: parts, quote: quote, location: location, comments: comments }
- end
-
- def format(q)
- if parts.empty?
- q.text("#{q.quote}#{q.quote}")
- return
- end
-
- opening_quote, closing_quote =
- if !Quotes.locked?(self, q.quote)
- [q.quote, q.quote]
- elsif quote&.start_with?("%")
- [quote, Quotes.matching(quote[/%[qQ]?(.)/, 1])]
- else
- [quote, quote]
- end
-
- q.text(opening_quote)
- q.group do
- parts.each do |part|
- if part.is_a?(TStringContent)
- value = Quotes.normalize(part.value, closing_quote)
- first = true
-
- value.each_line(chomp: true) do |line|
- if first
- first = false
- else
- q.breakable_return
- end
-
- q.text(line)
- end
-
- q.breakable_return if value.end_with?("\n")
- else
- q.format(part)
- end
- end
- end
- q.text(closing_quote)
- end
-
- def ===(other)
- other.is_a?(StringLiteral) && ArrayMatch.call(parts, other.parts) &&
- quote === other.quote
- end
- end
-
- # Super represents using the +super+ keyword with arguments. It can optionally
- # use parentheses.
- #
- # super(value)
- #
- class Super < Node
- # [ArgParen | Args] the arguments to the keyword
- attr_reader :arguments
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(arguments:, location:)
- @arguments = arguments
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_super(self)
- end
-
- def child_nodes
- [arguments]
- end
-
- def copy(arguments: nil, location: nil)
- node =
- Super.new(
- arguments: arguments || self.arguments,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { arguments: arguments, location: location, comments: comments }
- end
-
- def format(q)
- q.group do
- q.text("super")
-
- if arguments.is_a?(ArgParen)
- q.format(arguments)
- else
- q.text(" ")
- q.nest("super ".length) { q.format(arguments) }
- end
- end
- end
-
- def ===(other)
- other.is_a?(Super) && arguments === other.arguments
- end
- end
-
- # SymBeg represents the beginning of a symbol literal.
- #
- # :symbol
- #
- # SymBeg is also used for dynamic symbols, as in:
- #
- # :"symbol"
- #
- # Finally, SymBeg is also used for symbols using the %s syntax, as in:
- #
- # %s[symbol]
- #
- # The value of this node is a string. In most cases (as in the first example
- # above) it will contain just ":". In the case of dynamic symbols it will
- # contain ":'" or ":\"". In the case of %s symbols, it will contain the start
- # of the symbol including the %s and the delimiter.
- class SymBeg < Node
- # [String] the beginning of the symbol
- attr_reader :value
-
- def initialize(value:, location:)
- @value = value
- @location = location
- end
-
- def accept(visitor)
- visitor.visit_symbeg(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(value: nil, location: nil)
- SymBeg.new(
- value: value || self.value,
- location: location || self.location
- )
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location }
- end
-
- def ===(other)
- other.is_a?(SymBeg) && value === other.value
- end
- end
-
- # SymbolContent represents symbol contents and is always the child of a
- # SymbolLiteral node.
- #
- # :symbol
- #
- class SymbolContent < Node
- # [Backtick | Const | CVar | GVar | Ident | IVar | Kw | Op] the value of the
- # symbol
- attr_reader :value
-
- def initialize(value:, location:)
- @value = value
- @location = location
- end
-
- def accept(visitor)
- visitor.visit_symbol_content(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(value: nil, location: nil)
- SymbolContent.new(
- value: value || self.value,
- location: location || self.location
- )
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location }
- end
-
- def ===(other)
- other.is_a?(SymbolContent) && value === other.value
- end
- end
-
- # SymbolLiteral represents a symbol in the system with no interpolation
- # (as opposed to a DynaSymbol which has interpolation).
- #
- # :symbol
- #
- class SymbolLiteral < Node
- # [Backtick | Const | CVar | GVar | Ident | IVar | Kw | Op | TStringContent]
- # the value of the symbol
- attr_reader :value
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:)
- @value = value
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_symbol_literal(self)
- end
-
- def child_nodes
- [value]
- end
-
- def copy(value: nil, location: nil)
- node =
- SymbolLiteral.new(
- value: value || self.value,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location, comments: comments }
- end
-
- def format(q)
- q.text(":")
- q.text("\\") if value.comments.any?
- q.format(value)
- end
-
- def ===(other)
- other.is_a?(SymbolLiteral) && value === other.value
- end
- end
-
- # Symbols represents a symbol array literal with interpolation.
- #
- # %I[one two three]
- #
- class Symbols < Node
- # [SymbolsBeg] the token that opens this array literal
- attr_reader :beginning
-
- # [Array[ Word ]] the words in the symbol array literal
- attr_reader :elements
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(beginning:, elements:, location:)
- @beginning = beginning
- @elements = elements
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_symbols(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(beginning: nil, elements: nil, location: nil)
- Symbols.new(
- beginning: beginning || self.beginning,
- elements: elements || self.elements,
- location: location || self.location
- )
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- beginning: beginning,
- elements: elements,
- location: location,
- comments: comments
- }
- end
-
- def format(q)
- opening, closing = "%I[", "]"
-
- if elements.any? { |element| element.match?(/[\[\]]/) }
- opening = beginning.value
- closing = Quotes.matching(opening[2])
- end
-
- q.text(opening)
- q.group do
- q.indent do
- q.breakable_empty
- q.seplist(
- elements,
- ArrayLiteral::BREAKABLE_SPACE_SEPARATOR
- ) { |element| q.format(element) }
- end
- q.breakable_empty
- end
- q.text(closing)
- end
-
- def ===(other)
- other.is_a?(Symbols) && beginning === other.beginning &&
- ArrayMatch.call(elements, other.elements)
- end
- end
-
- # SymbolsBeg represents the start of a symbol array literal with
- # interpolation.
- #
- # %I[one two three]
- #
- # In the snippet above, SymbolsBeg represents the "%I[" token. Note that these
- # kinds of arrays can start with a lot of different delimiter types
- # (e.g., %I| or %I<).
- class SymbolsBeg < Node
- # [String] the beginning of the symbol literal array
- attr_reader :value
-
- def initialize(value:, location:)
- @value = value
- @location = location
- end
-
- def accept(visitor)
- visitor.visit_symbols_beg(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(value: nil, location: nil)
- SymbolsBeg.new(
- value: value || self.value,
- location: location || self.location
- )
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location }
- end
-
- def ===(other)
- other.is_a?(SymbolsBeg) && value === other.value
- end
- end
-
- # TLambda represents the beginning of a lambda literal.
- #
- # -> { value }
- #
- # In the example above the TLambda represents the +->+ operator.
- class TLambda < Node
- # [String] the beginning of the lambda literal
- attr_reader :value
-
- def initialize(value:, location:)
- @value = value
- @location = location
- end
-
- def accept(visitor)
- visitor.visit_tlambda(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(value: nil, location: nil)
- TLambda.new(
- value: value || self.value,
- location: location || self.location
- )
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location }
- end
-
- def ===(other)
- other.is_a?(TLambda) && value === other.value
- end
- end
-
- # TLamBeg represents the beginning of the body of a lambda literal using
- # braces.
- #
- # -> { value }
- #
- # In the example above the TLamBeg represents the +{+ operator.
- class TLamBeg < Node
- # [String] the beginning of the body of the lambda literal
- attr_reader :value
-
- def initialize(value:, location:)
- @value = value
- @location = location
- end
-
- def accept(visitor)
- visitor.visit_tlambeg(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(value: nil, location: nil)
- TLamBeg.new(
- value: value || self.value,
- location: location || self.location
- )
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location }
- end
-
- def ===(other)
- other.is_a?(TLamBeg) && value === other.value
- end
- end
-
- # TopConstField is always the child node of some kind of assignment. It
- # represents when you're assigning to a constant that is being referenced at
- # the top level.
- #
- # ::Constant = value
- #
- class TopConstField < Node
- # [Const] the constant being assigned
- attr_reader :constant
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(constant:, location:)
- @constant = constant
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_top_const_field(self)
- end
-
- def child_nodes
- [constant]
- end
-
- def copy(constant: nil, location: nil)
- node =
- TopConstField.new(
- constant: constant || self.constant,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { constant: constant, location: location, comments: comments }
- end
-
- def format(q)
- q.text("::")
- q.format(constant)
- end
-
- def ===(other)
- other.is_a?(TopConstField) && constant === other.constant
- end
- end
-
- # TopConstRef is very similar to TopConstField except that it is not involved
- # in an assignment.
- #
- # ::Constant
- #
- class TopConstRef < Node
- # [Const] the constant being referenced
- attr_reader :constant
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(constant:, location:)
- @constant = constant
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_top_const_ref(self)
- end
-
- def child_nodes
- [constant]
- end
-
- def copy(constant: nil, location: nil)
- node =
- TopConstRef.new(
- constant: constant || self.constant,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { constant: constant, location: location, comments: comments }
- end
-
- def format(q)
- q.text("::")
- q.format(constant)
- end
-
- def ===(other)
- other.is_a?(TopConstRef) && constant === other.constant
- end
- end
-
- # TStringBeg represents the beginning of a string literal.
- #
- # "string"
- #
- # In the example above, TStringBeg represents the first set of quotes. Strings
- # can also use single quotes. They can also be declared using the +%q+ and
- # +%Q+ syntax, as in:
- #
- # %q{string}
- #
- class TStringBeg < Node
- # [String] the beginning of the string
- attr_reader :value
-
- def initialize(value:, location:)
- @value = value
- @location = location
- end
-
- def accept(visitor)
- visitor.visit_tstring_beg(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(value: nil, location: nil)
- TStringBeg.new(
- value: value || self.value,
- location: location || self.location
- )
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location }
- end
-
- def ===(other)
- other.is_a?(TStringBeg) && value === other.value
- end
- end
-
- # TStringContent represents plain characters inside of an entity that accepts
- # string content like a string, heredoc, command string, or regular
- # expression.
- #
- # "string"
- #
- # In the example above, TStringContent represents the +string+ token contained
- # within the string.
- class TStringContent < Node
- # [String] the content of the string
- attr_reader :value
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:)
- @value = value
- @location = location
- @comments = []
- end
-
- def match?(pattern)
- value.match?(pattern)
- end
-
- def accept(visitor)
- visitor.visit_tstring_content(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(value: nil, location: nil)
- node =
- TStringContent.new(
- value: value || self.value,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location, comments: comments }
- end
-
- def format(q)
- q.text(value)
- end
-
- def ===(other)
- other.is_a?(TStringContent) && value === other.value
- end
- end
-
- # TStringEnd represents the end of a string literal.
- #
- # "string"
- #
- # In the example above, TStringEnd represents the second set of quotes.
- # Strings can also use single quotes. They can also be declared using the +%q+
- # and +%Q+ syntax, as in:
- #
- # %q{string}
- #
- class TStringEnd < Node
- # [String] the end of the string
- attr_reader :value
-
- def initialize(value:, location:)
- @value = value
- @location = location
- end
-
- def accept(visitor)
- visitor.visit_tstring_end(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(value: nil, location: nil)
- TStringEnd.new(
- value: value || self.value,
- location: location || self.location
- )
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location }
- end
-
- def ===(other)
- other.is_a?(TStringEnd) && value === other.value
- end
- end
-
- # Not represents the unary +not+ method being called on an expression.
- #
- # not value
- #
- class Not < Node
- # [nil | Node] the statement on which to operate
- attr_reader :statement
-
- # [boolean] whether or not parentheses were used
- attr_reader :parentheses
- alias parentheses? parentheses
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(statement:, parentheses:, location:)
- @statement = statement
- @parentheses = parentheses
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_not(self)
- end
-
- def child_nodes
- [statement]
- end
-
- def copy(statement: nil, parentheses: nil, location: nil)
- node =
- Not.new(
- statement: statement || self.statement,
- parentheses: parentheses || self.parentheses,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- statement: statement,
- parentheses: parentheses,
- location: location,
- comments: comments
- }
- end
-
- def format(q)
- q.text("not")
-
- if parentheses
- q.text("(")
- q.format(statement) if statement
- q.text(")")
- else
- grandparent = q.grandparent
- ternary =
- (grandparent.is_a?(IfNode) || grandparent.is_a?(UnlessNode)) &&
- Ternaryable.call(q, grandparent)
-
- if ternary
- q.if_break { q.text(" ") }.if_flat { q.text("(") }
- q.format(statement) if statement
- q.if_flat { q.text(")") } if ternary
- else
- q.text(" ")
- q.format(statement) if statement
- end
- end
- end
-
- def ===(other)
- other.is_a?(Not) && statement === other.statement &&
- parentheses === other.parentheses
- end
- end
-
- # Unary represents a unary method being called on an expression, as in +!+ or
- # +~+.
- #
- # !value
- #
- class Unary < Node
- # [String] the operator being used
- attr_reader :operator
-
- # [Node] the statement on which to operate
- attr_reader :statement
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(operator:, statement:, location:)
- @operator = operator
- @statement = statement
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_unary(self)
- end
-
- def child_nodes
- [statement]
- end
-
- def copy(operator: nil, statement: nil, location: nil)
- node =
- Unary.new(
- operator: operator || self.operator,
- statement: statement || self.statement,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- operator: operator,
- statement: statement,
- location: location,
- comments: comments
- }
- end
-
- def format(q)
- q.text(operator)
- q.format(statement)
- end
-
- def ===(other)
- other.is_a?(Unary) && operator === other.operator &&
- statement === other.statement
- end
- end
-
- # Undef represents the use of the +undef+ keyword.
- #
- # undef method
- #
- class Undef < Node
- # Undef accepts a variable number of arguments that can be either DynaSymbol
- # or SymbolLiteral objects. For SymbolLiteral objects we descend directly
- # into the value in order to have it come out as bare words.
- class UndefArgumentFormatter
- # [DynaSymbol | SymbolLiteral] the symbol to undefine
- attr_reader :node
-
- def initialize(node)
- @node = node
- end
-
- def comments
- if node.is_a?(SymbolLiteral)
- node.comments + node.value.comments
- else
- node.comments
- end
- end
-
- def format(q)
- node.is_a?(SymbolLiteral) ? q.format(node.value) : q.format(node)
- end
- end
-
- # [Array[ DynaSymbol | SymbolLiteral ]] the symbols to undefine
- attr_reader :symbols
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(symbols:, location:)
- @symbols = symbols
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_undef(self)
- end
-
- def child_nodes
- symbols
- end
-
- def copy(symbols: nil, location: nil)
- node =
- Undef.new(
- symbols: symbols || self.symbols,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { symbols: symbols, location: location, comments: comments }
- end
-
- def format(q)
- keyword = "undef "
- formatters = symbols.map { |symbol| UndefArgumentFormatter.new(symbol) }
-
- q.group do
- q.text(keyword)
- q.nest(keyword.length) do
- q.seplist(formatters) { |formatter| q.format(formatter) }
- end
- end
- end
-
- def ===(other)
- other.is_a?(Undef) && ArrayMatch.call(symbols, other.symbols)
- end
- end
-
- # Unless represents the first clause in an +unless+ chain.
- #
- # unless predicate
- # end
- #
- class UnlessNode < Node
- # [Node] the expression to be checked
- attr_reader :predicate
-
- # [Statements] the expressions to be executed
- attr_reader :statements
-
- # [nil | Elsif | Else] the next clause in the chain
- attr_reader :consequent
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(predicate:, statements:, consequent:, location:)
- @predicate = predicate
- @statements = statements
- @consequent = consequent
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_unless(self)
- end
-
- def child_nodes
- [predicate, statements, consequent]
- end
-
- def copy(predicate: nil, statements: nil, consequent: nil, location: nil)
- node =
- UnlessNode.new(
- predicate: predicate || self.predicate,
- statements: statements || self.statements,
- consequent: consequent || self.consequent,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- predicate: predicate,
- statements: statements,
- consequent: consequent,
- location: location,
- comments: comments
- }
- end
-
- def format(q)
- ConditionalFormatter.new("unless", self).format(q)
- end
-
- def ===(other)
- other.is_a?(UnlessNode) && predicate === other.predicate &&
- statements === other.statements && consequent === other.consequent
- end
-
- # Checks if the node was originally found in the modifier form.
- def modifier?
- predicate.location.start_char > statements.location.start_char
- end
- end
-
- # Formats an Until or While node.
- class LoopFormatter
- # [String] the name of the keyword used for this loop
- attr_reader :keyword
-
- # [Until | While] the node that is being formatted
- attr_reader :node
-
- def initialize(keyword, node)
- @keyword = keyword
- @node = node
- end
-
- def format(q)
- # If we're in the modifier form and we're modifying a `begin`, then this
- # is a special case where we need to explicitly use the modifier form
- # because otherwise the semantic meaning changes. This looks like:
- #
- # begin
- # foo
- # end while bar
- #
- # Also, if the statement of the modifier includes an assignment, then we
- # can't know for certain that it won't impact the predicate, so we need to
- # force it to stay as it is. This looks like:
- #
- # foo = bar while foo
- #
- if node.modifier? && (statement = node.statements.body.first) &&
- (statement.is_a?(Begin) || ContainsAssignment.call(statement))
- q.format(statement)
- q.text(" #{keyword} ")
- q.format(node.predicate)
- elsif node.statements.empty?
- q.group do
- q.text("#{keyword} ")
- q.nest(keyword.length + 1) { q.format(node.predicate) }
- q.breakable_force
- q.text("end")
- end
- elsif ContainsAssignment.call(node.predicate)
- format_break(q)
- q.break_parent
- else
- q.group do
- q
- .if_break { format_break(q) }
- .if_flat do
- Parentheses.flat(q) do
- q.format(node.statements)
- q.text(" #{keyword} ")
- q.format(node.predicate)
- end
- end
- end
- end
- end
-
- private
-
- def format_break(q)
- q.text("#{keyword} ")
- q.nest(keyword.length + 1) { q.format(node.predicate) }
- q.indent do
- q.breakable_empty
- q.format(node.statements)
- end
- q.breakable_empty
- q.text("end")
- end
- end
-
- # Until represents an +until+ loop.
- #
- # until predicate
- # end
- #
- class UntilNode < Node
- # [Node] the expression to be checked
- attr_reader :predicate
-
- # [Statements] the expressions to be executed
- attr_reader :statements
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(predicate:, statements:, location:)
- @predicate = predicate
- @statements = statements
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_until(self)
- end
-
- def child_nodes
- [predicate, statements]
- end
-
- def copy(predicate: nil, statements: nil, location: nil)
- node =
- UntilNode.new(
- predicate: predicate || self.predicate,
- statements: statements || self.statements,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- predicate: predicate,
- statements: statements,
- location: location,
- comments: comments
- }
- end
-
- def format(q)
- LoopFormatter.new("until", self).format(q)
- end
-
- def ===(other)
- other.is_a?(UntilNode) && predicate === other.predicate &&
- statements === other.statements
- end
-
- def modifier?
- predicate.location.start_char > statements.location.start_char
- end
- end
-
- # VarField represents a variable that is being assigned a value. As such, it
- # is always a child of an assignment type node.
- #
- # variable = value
- #
- # In the example above, the VarField node represents the +variable+ token.
- class VarField < Node
- # [nil | :nil | Const | CVar | GVar | Ident | IVar] the target of this node
- attr_reader :value
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:)
- @value = value
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_var_field(self)
- end
-
- def child_nodes
- value == :nil ? [] : [value]
- end
-
- def copy(value: nil, location: nil)
- node =
- VarField.new(
- value: value || self.value,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location, comments: comments }
- end
-
- def format(q)
- if value == :nil
- q.text("nil")
- elsif value
- q.format(value)
- end
- end
-
- def ===(other)
- other.is_a?(VarField) && value === other.value
- end
- end
-
- # VarRef represents a variable reference.
- #
- # true
- #
- # This can be a plain local variable like the example above. It can also be a
- # constant, a class variable, a global variable, an instance variable, a
- # keyword (like +self+, +nil+, +true+, or +false+), or a numbered block
- # variable.
- class VarRef < Node
- # [Const | CVar | GVar | Ident | IVar | Kw] the value of this node
- attr_reader :value
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:)
- @value = value
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_var_ref(self)
- end
-
- def child_nodes
- [value]
- end
-
- def copy(value: nil, location: nil)
- node =
- VarRef.new(
- value: value || self.value,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location, comments: comments }
- end
-
- def format(q)
- q.format(value)
- end
-
- def ===(other)
- other.is_a?(VarRef) && value === other.value
- end
-
- # Oh man I hate this so much. Basically, ripper doesn't provide enough
- # functionality to actually know where pins are within an expression. So we
- # have to walk the tree ourselves and insert more information. In doing so,
- # we have to replace this node by a pinned node when necessary.
- #
- # To be clear, this method should just not exist. It's not good. It's a
- # place of shame. But it's necessary for now, so I'm keeping it.
- def pin(parent, pin)
- replace =
- PinnedVarRef.new(value: value, location: pin.location.to(location))
-
- parent
- .deconstruct_keys([])
- .each do |key, value|
- if value == self
- parent.instance_variable_set(:"@#{key}", replace)
- break
- elsif value.is_a?(Array) && (index = value.index(self))
- parent.public_send(key)[index] = replace
- break
- elsif value.is_a?(Array) &&
- (index = value.index { |(_k, v)| v == self })
- parent.public_send(key)[index][1] = replace
- break
- end
- end
- end
- end
-
- # PinnedVarRef represents a pinned variable reference within a pattern
- # matching pattern.
- #
- # case value
- # in ^variable
- # end
- #
- # This can be a plain local variable like the example above. It can also be a
- # a class variable, a global variable, or an instance variable.
- class PinnedVarRef < Node
- # [Const | CVar | GVar | Ident | IVar] the value of this node
- attr_reader :value
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:)
- @value = value
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_pinned_var_ref(self)
- end
-
- def child_nodes
- [value]
- end
-
- def copy(value: nil, location: nil)
- node =
- PinnedVarRef.new(
- value: value || self.value,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location, comments: comments }
- end
-
- def format(q)
- q.group do
- q.text("^")
- q.format(value)
- end
- end
-
- def ===(other)
- other.is_a?(PinnedVarRef) && value === other.value
- end
- end
-
- # VCall represent any plain named object with Ruby that could be either a
- # local variable or a method call.
- #
- # variable
- #
- class VCall < Node
- # [Ident] the value of this expression
- attr_reader :value
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(value:, location:)
- @value = value
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_vcall(self)
- end
-
- def child_nodes
- [value]
- end
-
- def copy(value: nil, location: nil)
- node =
- VCall.new(
- value: value || self.value,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location, comments: comments }
- end
-
- def format(q)
- q.format(value)
- end
-
- def ===(other)
- other.is_a?(VCall) && value === other.value
- end
-
- def access_control?
- @access_control ||= %w[private protected public].include?(value.value)
- end
-
- def arity
- 0
- end
- end
-
- # VoidStmt represents an empty lexical block of code.
- #
- # ;;
- #
- class VoidStmt < Node
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(location:)
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_void_stmt(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(location: nil)
- node = VoidStmt.new(location: location || self.location)
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { location: location, comments: comments }
- end
-
- def format(q)
- end
-
- def ===(other)
- other.is_a?(VoidStmt)
- end
- end
-
- # When represents a +when+ clause in a +case+ chain.
- #
- # case value
- # when predicate
- # end
- #
- class When < Node
- # [Args] the arguments to the when clause
- attr_reader :arguments
-
- # [Statements] the expressions to be executed
- attr_reader :statements
-
- # [nil | Else | When] the next clause in the chain
- attr_reader :consequent
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(arguments:, statements:, consequent:, location:)
- @arguments = arguments
- @statements = statements
- @consequent = consequent
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_when(self)
- end
-
- def child_nodes
- [arguments, statements, consequent]
- end
-
- def copy(arguments: nil, statements: nil, consequent: nil, location: nil)
- node =
- When.new(
- arguments: arguments || self.arguments,
- statements: statements || self.statements,
- consequent: consequent || self.consequent,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- arguments: arguments,
- statements: statements,
- consequent: consequent,
- location: location,
- comments: comments
- }
- end
-
- # We have a special separator here for when clauses which causes them to
- # fill as much of the line as possible as opposed to everything breaking
- # into its own line as soon as you hit the print limit.
- class Separator
- def call(q)
- q.group do
- q.text(",")
- q.breakable_space
- end
- end
- end
-
- # We're going to keep a single instance of this separator around so we don't
- # have to allocate a new one every time we format a when clause.
- SEPARATOR = Separator.new.freeze
-
- def format(q)
- keyword = "when "
-
- q.group do
- q.group do
- q.text(keyword)
- q.nest(keyword.length) do
- if arguments.comments.any?
- q.format(arguments)
- else
- q.seplist(arguments.parts, SEPARATOR) { |part| q.format(part) }
- end
-
- # Very special case here. If you're inside of a when clause and the
- # last argument to the predicate is and endless range, then you are
- # forced to use the "then" keyword to make it parse properly.
- last = arguments.parts.last
- q.text(" then") if last.is_a?(RangeNode) && !last.right
- end
- end
-
- unless statements.empty?
- q.indent do
- q.breakable_force
- q.format(statements)
- end
- end
-
- if consequent
- q.breakable_force
- q.format(consequent)
- end
- end
- end
-
- def ===(other)
- other.is_a?(When) && arguments === other.arguments &&
- statements === other.statements && consequent === other.consequent
- end
- end
-
- # While represents a +while+ loop.
- #
- # while predicate
- # end
- #
- class WhileNode < Node
- # [Node] the expression to be checked
- attr_reader :predicate
-
- # [Statements] the expressions to be executed
- attr_reader :statements
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(predicate:, statements:, location:)
- @predicate = predicate
- @statements = statements
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_while(self)
- end
-
- def child_nodes
- [predicate, statements]
- end
-
- def copy(predicate: nil, statements: nil, location: nil)
- node =
- WhileNode.new(
- predicate: predicate || self.predicate,
- statements: statements || self.statements,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- predicate: predicate,
- statements: statements,
- location: location,
- comments: comments
- }
- end
-
- def format(q)
- LoopFormatter.new("while", self).format(q)
- end
-
- def ===(other)
- other.is_a?(WhileNode) && predicate === other.predicate &&
- statements === other.statements
- end
-
- def modifier?
- predicate.location.start_char > statements.location.start_char
- end
- end
-
- # Word represents an element within a special array literal that accepts
- # interpolation.
- #
- # %W[a#{b}c xyz]
- #
- # In the example above, there would be two Word nodes within a parent Words
- # node.
- class Word < Node
- # [Array[ StringEmbExpr | StringDVar | TStringContent ]] the parts of the
- # word
- attr_reader :parts
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(parts:, location:)
- @parts = parts
- @location = location
- @comments = []
- end
-
- def match?(pattern)
- parts.any? { |part| part.is_a?(TStringContent) && part.match?(pattern) }
- end
-
- def accept(visitor)
- visitor.visit_word(self)
- end
-
- def child_nodes
- parts
- end
-
- def copy(parts: nil, location: nil)
- node =
- Word.new(
- parts: parts || self.parts,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { parts: parts, location: location, comments: comments }
- end
-
- def format(q)
- q.format_each(parts)
- end
-
- def ===(other)
- other.is_a?(Word) && ArrayMatch.call(parts, other.parts)
- end
- end
-
- # Words represents a string literal array with interpolation.
- #
- # %W[one two three]
- #
- class Words < Node
- # [WordsBeg] the token that opens this array literal
- attr_reader :beginning
-
- # [Array[ Word ]] the elements of this array
- attr_reader :elements
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(beginning:, elements:, location:)
- @beginning = beginning
- @elements = elements
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_words(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(beginning: nil, elements: nil, location: nil)
- Words.new(
- beginning: beginning || self.beginning,
- elements: elements || self.elements,
- location: location || self.location
- )
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- {
- beginning: beginning,
- elements: elements,
- location: location,
- comments: comments
- }
- end
-
- def format(q)
- opening, closing = "%W[", "]"
-
- if elements.any? { |element| element.match?(/[\[\]]/) }
- opening = beginning.value
- closing = Quotes.matching(opening[2])
- end
-
- q.text(opening)
- q.group do
- q.indent do
- q.breakable_empty
- q.seplist(
- elements,
- ArrayLiteral::BREAKABLE_SPACE_SEPARATOR
- ) { |element| q.format(element) }
- end
- q.breakable_empty
- end
- q.text(closing)
- end
-
- def ===(other)
- other.is_a?(Words) && beginning === other.beginning &&
- ArrayMatch.call(elements, other.elements)
- end
- end
-
- # WordsBeg represents the beginning of a string literal array with
- # interpolation.
- #
- # %W[one two three]
- #
- # In the snippet above, a WordsBeg would be created with the value of "%W[".
- # Note that these kinds of arrays can start with a lot of different delimiter
- # types (e.g., %W| or %W<).
- class WordsBeg < Node
- # [String] the start of the word literal array
- attr_reader :value
-
- def initialize(value:, location:)
- @value = value
- @location = location
- end
-
- def accept(visitor)
- visitor.visit_words_beg(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(value: nil, location: nil)
- WordsBeg.new(
- value: value || self.value,
- location: location || self.location
- )
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { value: value, location: location }
- end
-
- def ===(other)
- other.is_a?(WordsBeg) && value === other.value
- end
- end
-
- # XString represents the contents of an XStringLiteral.
- #
- # `ls`
- #
- class XString < Node
- # [Array[ StringEmbExpr | StringDVar | TStringContent ]] the parts of the
- # xstring
- attr_reader :parts
-
- def initialize(parts:, location:)
- @parts = parts
- @location = location
- end
-
- def accept(visitor)
- visitor.visit_xstring(self)
- end
-
- def child_nodes
- parts
- end
-
- def copy(parts: nil, location: nil)
- XString.new(
- parts: parts || self.parts,
- location: location || self.location
- )
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { parts: parts, location: location }
- end
-
- def ===(other)
- other.is_a?(XString) && ArrayMatch.call(parts, other.parts)
- end
- end
-
- # XStringLiteral represents a string that gets executed.
- #
- # `ls`
- #
- class XStringLiteral < Node
- # [Array[ StringEmbExpr | StringDVar | TStringContent ]] the parts of the
- # xstring
- attr_reader :parts
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(parts:, location:)
- @parts = parts
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_xstring_literal(self)
- end
-
- def child_nodes
- parts
- end
-
- def copy(parts: nil, location: nil)
- node =
- XStringLiteral.new(
- parts: parts || self.parts,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { parts: parts, location: location, comments: comments }
- end
-
- def format(q)
- q.text("`")
- q.format_each(parts)
- q.text("`")
- end
-
- def ===(other)
- other.is_a?(XStringLiteral) && ArrayMatch.call(parts, other.parts)
- end
- end
-
- # Yield represents using the +yield+ keyword with arguments.
- #
- # yield value
- #
- class YieldNode < Node
- # [nil | Args | Paren] the arguments passed to the yield
- attr_reader :arguments
-
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(arguments:, location:)
- @arguments = arguments
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_yield(self)
- end
-
- def child_nodes
- [arguments]
- end
-
- def copy(arguments: nil, location: nil)
- node =
- YieldNode.new(
- arguments: arguments || self.arguments,
- location: location || self.location
- )
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { arguments: arguments, location: location, comments: comments }
- end
-
- def format(q)
- if arguments.nil?
- q.text("yield")
- return
- end
-
- q.group do
- q.text("yield")
-
- if arguments.is_a?(Paren)
- q.format(arguments)
- else
- q.if_break { q.text("(") }.if_flat { q.text(" ") }
- q.indent do
- q.breakable_empty
- q.format(arguments)
- end
- q.breakable_empty
- q.if_break { q.text(")") }
- end
- end
- end
-
- def ===(other)
- other.is_a?(YieldNode) && arguments === other.arguments
- end
- end
-
- # ZSuper represents the bare +super+ keyword with no arguments.
- #
- # super
- #
- class ZSuper < Node
- # [Array[ Comment | EmbDoc ]] the comments attached to this node
- attr_reader :comments
-
- def initialize(location:)
- @location = location
- @comments = []
- end
-
- def accept(visitor)
- visitor.visit_zsuper(self)
- end
-
- def child_nodes
- []
- end
-
- def copy(location: nil)
- node = ZSuper.new(location: location || self.location)
-
- node.comments.concat(comments.map(&:copy))
- node
- end
-
- alias deconstruct child_nodes
-
- def deconstruct_keys(_keys)
- { location: location, comments: comments }
- end
-
- def format(q)
- q.text("super")
- end
-
- def ===(other)
- other.is_a?(ZSuper)
- end
- end
-end
diff --git a/lib/syntax_tree/parser.rb b/lib/syntax_tree/parser.rb
deleted file mode 100644
index 3869dd9d..00000000
--- a/lib/syntax_tree/parser.rb
+++ /dev/null
@@ -1,4648 +0,0 @@
-# frozen_string_literal: true
-
-module SyntaxTree
- # Parser is a subclass of the Ripper library that subscribes to the stream of
- # tokens and nodes coming from the parser and builds up a syntax tree.
- 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].max]
- end
- end
-
- # This represents all of the tokens coming back from the lexer. It is
- # replacing a simple array because it keeps track of the last deleted token
- # from the list for better error messages.
- class TokenList
- attr_reader :tokens, :last_deleted
-
- def initialize
- @tokens = []
- @last_deleted = nil
- end
-
- def <<(token)
- tokens << token
- end
-
- def [](index)
- tokens[index]
- end
-
- def any?(&block)
- tokens.any?(&block)
- end
-
- def reverse_each(&block)
- tokens.reverse_each(&block)
- end
-
- def rindex(&block)
- tokens.rindex(&block)
- end
-
- def delete(value)
- @last_deleted = tokens.delete(value) || @last_deleted
- end
-
- def delete_at(index)
- @last_deleted = tokens.delete_at(index)
- end
- end
-
- # [String] the source being parsed
- attr_reader :source
-
- # [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
-
- # 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 = TokenList.new
-
- # 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.each_line do |line|
- @line_counts << if line.size == line.bytesize
- SingleByteString.new(last_index)
- else
- 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
-
- # This represents the current column we're in relative to the beginning of
- # the current line.
- def current_column
- line = line_counts[lineno - 1]
- line[column].to_i - line.start
- end
-
- # Returns the current location that is being looked at for the parser for
- # the purpose of locating the error.
- def find_token_error(location)
- if location
- # If we explicitly passed a location into this find_token_error method,
- # that means that's the source of the error, so we'll use that
- # information for our error object.
- lineno = location.start_line
- [lineno, location.start_char - line_counts[lineno - 1].start]
- elsif lineno && column
- # If there is a line number associated with the current ripper state,
- # then we'll use that information to generate the error.
- [lineno, column]
- elsif (location = tokens.last_deleted&.location)
- # If we've already deleted a token from the list of tokens that we are
- # consuming, then we'll fall back to that token's location.
- lineno = location.start_line
- [lineno, location.start_char - line_counts[lineno - 1].start]
- else
- # Finally, it's possible that when we hit this error the parsing thread
- # for ripper has died. In that case, lineno and column both return nil.
- # So we're just going to set it to line 1, column 0 in the hopes that
- # that makes any sense.
- [1, 0]
- end
- 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.
- #
- # 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.
-
- def find_token(type)
- index = tokens.rindex { |token| token.is_a?(type) }
- tokens[index] if index
- end
-
- def find_token_between(type, left, right)
- bounds = left.location.end_char...right.location.start_char
- index =
- tokens.rindex do |token|
- char = token.location.start_char
- break if char < bounds.begin
-
- token.is_a?(type) && bounds.cover?(char)
- end
-
- tokens[index] if index
- end
-
- def find_keyword(name)
- index = tokens.rindex { |token| token.is_a?(Kw) && (token.name == name) }
- tokens[index] if index
- end
-
- def find_keyword_between(name, left, right)
- bounds = left.end_char...right.start_char
- index =
- tokens.rindex do |token|
- char = token.location.start_char
- break if char < bounds.begin
-
- token.is_a?(Kw) && (token.name == name) && bounds.cover?(char)
- end
-
- tokens[index] if index
- end
-
- def find_operator(name)
- index = tokens.rindex { |token| token.is_a?(Op) && (token.name == name) }
- tokens[index] if index
- end
-
- def consume_error(name, location)
- message = "Cannot find expected #{name}"
- raise ParseError.new(message, *find_token_error(location))
- end
-
- def consume_token(type)
- index = tokens.rindex { |token| token.is_a?(type) }
- consume_error(type.name.split("::", 2).last, nil) unless index
- tokens.delete_at(index)
- end
-
- def consume_tstring_end(location)
- index = tokens.rindex { |token| token.is_a?(TStringEnd) }
- consume_error("string ending", location) unless index
- tokens.delete_at(index)
- end
-
- def consume_keyword(name)
- index = tokens.rindex { |token| token.is_a?(Kw) && (token.name == name) }
- consume_error(name, nil) unless index
- tokens.delete_at(index)
- end
-
- def consume_operator(name)
- index = tokens.rindex { |token| token.is_a?(Op) && (token.name == name) }
- consume_error(name, nil) unless index
- tokens.delete_at(index)
- 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)
- maximum = source.length
-
- position.upto(maximum) do |pound_index|
- case source[pound_index]
- when "#"
- return source.index("\n", pound_index + 1) || maximum
- when " "
- # continue
- else
- return position
- end
- end
- 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 = consume_token(LBrace)
- rbrace = consume_token(RBrace)
-
- start_char = find_next_statement_start(lbrace.location.end_char)
- statements.bind(
- self,
- start_char,
- start_char - line_counts[lbrace.location.start_line - 1].start,
- rbrace.location.start_char,
- rbrace.location.start_column
- )
-
- keyword = consume_keyword(: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,
- column: current_column,
- size: value.size
- )
- )
- end
-
- # :call-seq:
- # on_END: (Statements statements) -> ENDBlock
- def on_END(statements)
- lbrace = consume_token(LBrace)
- rbrace = consume_token(RBrace)
-
- start_char = find_next_statement_start(lbrace.location.end_char)
- statements.bind(
- self,
- start_char,
- start_char - line_counts[lbrace.location.start_line - 1].start,
- rbrace.location.start_char,
- rbrace.location.start_column
- )
-
- keyword = consume_keyword(: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)..],
- location:
- Location.token(
- line: lineno,
- char: char_pos,
- column: current_column,
- size: value.size
- )
- )
- end
-
- # :call-seq:
- # on_alias: (
- # (DynaSymbol | SymbolLiteral) left,
- # (DynaSymbol | SymbolLiteral) right
- # ) -> AliasNode
- def on_alias(left, right)
- keyword = consume_keyword(:alias)
-
- AliasNode.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)
- consume_token(LBracket)
- rbracket = consume_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)
- consume_token(LBracket)
- rbracket = consume_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 = consume_token(LParen)
- rparen = consume_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)
- end_char = arguments.parts.any? && arguments.location.end_char
-
- # First, see if there is an & operator that could potentially be
- # associated with the block part of this args_add_block. If there is not,
- # then just return the arguments.
- index =
- tokens.rindex do |token|
- # 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.
- return arguments if end_char && token.location.start_char < end_char
- token.is_a?(Op) && (token.name == :&)
- end
-
- return arguments unless index
-
- # 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.
- operator = tokens.delete_at(index)
-
- # Construct the location that represents the block argument.
- location = operator.location
- location = operator.location.to(block.location) if block
-
- # 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 = consume_operator(:*)
- 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 = consume_operator(:"...")
-
- ArgsForward.new(location: op.location)
- end
-
- # :call-seq:
- # on_args_new: () -> Args
- def on_args_new
- Args.new(
- parts: [],
- location:
- Location.fixed(line: lineno, column: current_column, 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 = consume_token(LBracket)
- rbracket = consume_token(RBracket)
-
- ArrayLiteral.new(
- lbracket: lbracket,
- contents: contents,
- location: lbracket.location.to(rbracket.location)
- )
- else
- tstring_end = consume_tstring_end(contents.beginning.location)
-
- contents.class.new(
- beginning: contents.beginning,
- elements: contents.elements,
- location: contents.location.to(tstring_end.location)
- )
- end
- end
-
- # Visitor is a parent class that provides the ability to walk down the tree
- # and handle a subset of nodes. By defining your own subclass, you can
- # explicitly handle a node type by defining a visit_* method.
- class Visitor
- def visit(node)
- node&.accept(self)
- end
-
- def visit_all(nodes)
- nodes.map { |node| visit(node) }
- end
-
- def visit_child_nodes(node)
- visit_all(node.child_nodes)
- end
-
- # Visit an ARef node.
- alias visit_aref visit_child_nodes
-
- # Visit an ARefField node.
- alias visit_aref_field visit_child_nodes
-
- # Visit an AliasNode node.
- alias visit_alias visit_child_nodes
-
- # Visit an ArgBlock node.
- alias visit_arg_block visit_child_nodes
-
- # Visit an ArgParen node.
- alias visit_arg_paren visit_child_nodes
-
- # Visit an ArgStar node.
- alias visit_arg_star visit_child_nodes
-
- # Visit an Args node.
- alias visit_args visit_child_nodes
-
- # Visit an ArgsForward node.
- alias visit_args_forward visit_child_nodes
-
- # Visit an ArrayLiteral node.
- alias visit_array visit_child_nodes
-
- # Visit an AryPtn node.
- alias visit_aryptn visit_child_nodes
-
- # Visit an Assign node.
- alias visit_assign visit_child_nodes
-
- # Visit an Assoc node.
- alias visit_assoc visit_child_nodes
-
- # Visit an AssocSplat node.
- alias visit_assoc_splat visit_child_nodes
-
- # Visit a Backref node.
- alias visit_backref visit_child_nodes
-
- # Visit a Backtick node.
- alias visit_backtick visit_child_nodes
-
- # Visit a BareAssocHash node.
- alias visit_bare_assoc_hash visit_child_nodes
-
- # Visit a BEGINBlock node.
- alias visit_BEGIN visit_child_nodes
-
- # Visit a Begin node.
- alias visit_begin visit_child_nodes
-
- # Visit a Binary node.
- alias visit_binary visit_child_nodes
-
- # Visit a Block node.
- alias visit_block visit_child_nodes
-
- # Visit a BlockArg node.
- alias visit_blockarg visit_child_nodes
-
- # Visit a BlockVar node.
- alias visit_block_var visit_child_nodes
-
- # Visit a BodyStmt node.
- alias visit_bodystmt visit_child_nodes
-
- # Visit a Break node.
- alias visit_break visit_child_nodes
-
- # Visit a Call node.
- alias visit_call visit_child_nodes
-
- # Visit a Case node.
- alias visit_case visit_child_nodes
-
- # Visit a CHAR node.
- alias visit_CHAR visit_child_nodes
-
- # Visit a ClassDeclaration node.
- alias visit_class visit_child_nodes
-
- # Visit a Comma node.
- alias visit_comma visit_child_nodes
-
- # Visit a Command node.
- alias visit_command visit_child_nodes
-
- # Visit a CommandCall node.
- alias visit_command_call visit_child_nodes
-
- # Visit a Comment node.
- alias visit_comment visit_child_nodes
-
- # Visit a Const node.
- alias visit_const visit_child_nodes
-
- # Visit a ConstPathField node.
- alias visit_const_path_field visit_child_nodes
-
- # Visit a ConstPathRef node.
- alias visit_const_path_ref visit_child_nodes
-
- # Visit a ConstRef node.
- alias visit_const_ref visit_child_nodes
-
- # Visit a CVar node.
- alias visit_cvar visit_child_nodes
-
- # Visit a Def node.
- alias visit_def visit_child_nodes
-
- # Visit a Defined node.
- alias visit_defined visit_child_nodes
-
- # Visit a DynaSymbol node.
- alias visit_dyna_symbol visit_child_nodes
-
- # Visit an ENDBlock node.
- alias visit_END visit_child_nodes
-
- # Visit an Else node.
- alias visit_else visit_child_nodes
-
- # Visit an Elsif node.
- alias visit_elsif visit_child_nodes
-
- # Visit an EmbDoc node.
- alias visit_embdoc visit_child_nodes
-
- # Visit an EmbExprBeg node.
- alias visit_embexpr_beg visit_child_nodes
-
- # Visit an EmbExprEnd node.
- alias visit_embexpr_end visit_child_nodes
-
- # Visit an EmbVar node.
- alias visit_embvar visit_child_nodes
-
- # Visit an Ensure node.
- alias visit_ensure visit_child_nodes
-
- # Visit an ExcessedComma node.
- alias visit_excessed_comma visit_child_nodes
-
- # Visit a Field node.
- alias visit_field visit_child_nodes
-
- # Visit a FloatLiteral node.
- alias visit_float visit_child_nodes
-
- # Visit a FndPtn node.
- alias visit_fndptn visit_child_nodes
-
- # Visit a For node.
- alias visit_for visit_child_nodes
-
- # Visit a GVar node.
- alias visit_gvar visit_child_nodes
-
- # Visit a HashLiteral node.
- alias visit_hash visit_child_nodes
-
- # Visit a Heredoc node.
- alias visit_heredoc visit_child_nodes
-
- # Visit a HeredocBeg node.
- alias visit_heredoc_beg visit_child_nodes
-
- # Visit a HeredocEnd node.
- alias visit_heredoc_end visit_child_nodes
-
- # Visit a HshPtn node.
- alias visit_hshptn visit_child_nodes
-
- # Visit an Ident node.
- alias visit_ident visit_child_nodes
-
- # Visit an IfNode node.
- alias visit_if visit_child_nodes
-
- # Visit an IfOp node.
- alias visit_if_op visit_child_nodes
-
- # Visit an Imaginary node.
- alias visit_imaginary visit_child_nodes
-
- # Visit an In node.
- alias visit_in visit_child_nodes
-
- # Visit an Int node.
- alias visit_int visit_child_nodes
-
- # Visit an IVar node.
- alias visit_ivar visit_child_nodes
-
- # Visit a Kw node.
- alias visit_kw visit_child_nodes
-
- # Visit a KwRestParam node.
- alias visit_kwrest_param visit_child_nodes
-
- # Visit a Label node.
- alias visit_label visit_child_nodes
-
- # Visit a LabelEnd node.
- alias visit_label_end visit_child_nodes
-
- # Visit a Lambda node.
- alias visit_lambda visit_child_nodes
-
- # Visit a LambdaVar node.
- alias visit_lambda_var visit_child_nodes
-
- # Visit a LBrace node.
- alias visit_lbrace visit_child_nodes
-
- # Visit a LBracket node.
- alias visit_lbracket visit_child_nodes
-
- # Visit a LParen node.
- alias visit_lparen visit_child_nodes
-
- # Visit a MAssign node.
- alias visit_massign visit_child_nodes
-
- # Visit a MethodAddBlock node.
- alias visit_method_add_block visit_child_nodes
-
- # Visit a MLHS node.
- alias visit_mlhs visit_child_nodes
-
- # Visit a MLHSParen node.
- alias visit_mlhs_paren visit_child_nodes
-
- # Visit a ModuleDeclaration node.
- alias visit_module visit_child_nodes
-
- # Visit a MRHS node.
- alias visit_mrhs visit_child_nodes
-
- # Visit a Next node.
- alias visit_next visit_child_nodes
-
- # Visit a Not node.
- alias visit_not visit_child_nodes
-
- # Visit an Op node.
- alias visit_op visit_child_nodes
-
- # Visit an OpAssign node.
- alias visit_opassign visit_child_nodes
-
- # Visit a Params node.
- alias visit_params visit_child_nodes
-
- # Visit a Paren node.
- alias visit_paren visit_child_nodes
-
- # Visit a Period node.
- alias visit_period visit_child_nodes
-
- # Visit a PinnedBegin node.
- alias visit_pinned_begin visit_child_nodes
-
- # Visit a PinnedVarRef node.
- alias visit_pinned_var_ref visit_child_nodes
-
- # Visit a Program node.
- alias visit_program visit_child_nodes
-
- # Visit a QSymbols node.
- alias visit_qsymbols visit_child_nodes
-
- # Visit a QSymbolsBeg node.
- alias visit_qsymbols_beg visit_child_nodes
-
- # Visit a QWords node.
- alias visit_qwords visit_child_nodes
-
- # Visit a QWordsBeg node.
- alias visit_qwords_beg visit_child_nodes
-
- # Visit a RangeNode node
- alias visit_range visit_child_nodes
-
- # Visit a RAssign node.
- alias visit_rassign visit_child_nodes
-
- # Visit a RationalLiteral node.
- alias visit_rational visit_child_nodes
-
- # Visit a RBrace node.
- alias visit_rbrace visit_child_nodes
-
- # Visit a RBracket node.
- alias visit_rbracket visit_child_nodes
-
- # Visit a Redo node.
- alias visit_redo visit_child_nodes
-
- # Visit a RegexpBeg node.
- alias visit_regexp_beg visit_child_nodes
-
- # Visit a RegexpContent node.
- alias visit_regexp_content visit_child_nodes
-
- # Visit a RegexpEnd node.
- alias visit_regexp_end visit_child_nodes
-
- # Visit a RegexpLiteral node.
- alias visit_regexp_literal visit_child_nodes
-
- # Visit a Rescue node.
- alias visit_rescue visit_child_nodes
-
- # Visit a RescueEx node.
- alias visit_rescue_ex visit_child_nodes
-
- # Visit a RescueMod node.
- alias visit_rescue_mod visit_child_nodes
-
- # Visit a RestParam node.
- alias visit_rest_param visit_child_nodes
-
- # Visit a Retry node.
- alias visit_retry visit_child_nodes
-
- # Visit a Return node.
- alias visit_return visit_child_nodes
-
- # Visit a RParen node.
- alias visit_rparen visit_child_nodes
-
- # Visit a SClass node.
- alias visit_sclass visit_child_nodes
-
- # Visit a Statements node.
- alias visit_statements visit_child_nodes
-
- # Visit a StringConcat node.
- alias visit_string_concat visit_child_nodes
-
- # Visit a StringContent node.
- alias visit_string_content visit_child_nodes
-
- # Visit a StringDVar node.
- alias visit_string_dvar visit_child_nodes
-
- # Visit a StringEmbExpr node.
- alias visit_string_embexpr visit_child_nodes
-
- # Visit a StringLiteral node.
- alias visit_string_literal visit_child_nodes
-
- # Visit a Super node.
- alias visit_super visit_child_nodes
-
- # Visit a SymBeg node.
- alias visit_symbeg visit_child_nodes
-
- # Visit a SymbolContent node.
- alias visit_symbol_content visit_child_nodes
-
- # Visit a SymbolLiteral node.
- alias visit_symbol_literal visit_child_nodes
-
- # Visit a Symbols node.
- alias visit_symbols visit_child_nodes
-
- # Visit a SymbolsBeg node.
- alias visit_symbols_beg visit_child_nodes
-
- # Visit a TLambda node.
- alias visit_tlambda visit_child_nodes
-
- # Visit a TLamBeg node.
- alias visit_tlambeg visit_child_nodes
-
- # Visit a TopConstField node.
- alias visit_top_const_field visit_child_nodes
-
- # Visit a TopConstRef node.
- alias visit_top_const_ref visit_child_nodes
-
- # Visit a TStringBeg node.
- alias visit_tstring_beg visit_child_nodes
-
- # Visit a TStringContent node.
- alias visit_tstring_content visit_child_nodes
-
- # Visit a TStringEnd node.
- alias visit_tstring_end visit_child_nodes
-
- # Visit an Unary node.
- alias visit_unary visit_child_nodes
-
- # Visit an Undef node.
- alias visit_undef visit_child_nodes
-
- # Visit an UnlessNode node.
- alias visit_unless visit_child_nodes
-
- # Visit an UntilNode node.
- alias visit_until visit_child_nodes
-
- # Visit a VarField node.
- alias visit_var_field visit_child_nodes
-
- # Visit a VarRef node.
- alias visit_var_ref visit_child_nodes
-
- # Visit a VCall node.
- alias visit_vcall visit_child_nodes
-
- # Visit a VoidStmt node.
- alias visit_void_stmt visit_child_nodes
-
- # Visit a When node.
- alias visit_when visit_child_nodes
-
- # Visit a WhileNode node.
- alias visit_while visit_child_nodes
-
- # Visit a Word node.
- alias visit_word visit_child_nodes
-
- # Visit a Words node.
- alias visit_words visit_child_nodes
-
- # Visit a WordsBeg node.
- alias visit_words_beg visit_child_nodes
-
- # Visit a XString node.
- alias visit_xstring visit_child_nodes
-
- # Visit a XStringLiteral node.
- alias visit_xstring_literal visit_child_nodes
-
- # Visit a YieldNode node.
- alias visit_yield visit_child_nodes
-
- # Visit a ZSuper node.
- alias visit_zsuper visit_child_nodes
-
- # Visit an EndContent node.
- alias visit___end__ visit_child_nodes
- end
-
- # Ugh... I really do not like this class. Basically, ripper doesn't provide
- # enough information about where pins are located in the tree. It only gives
- # events for ^ ops and var_ref nodes. You have to piece it together
- # yourself.
- #
- # Note that there are edge cases here that we straight up do not address,
- # because I honestly think it's going to be faster to write a new parser
- # than to address them. For example, this will not work properly:
- #
- # foo in ^((bar = 0; bar; baz))
- #
- # If someone actually does something like that, we'll have to find another
- # way to make this work.
- class PinVisitor < Visitor
- attr_reader :pins, :stack
-
- def initialize(pins)
- @pins = pins
- @stack = []
- end
-
- def visit(node)
- return if pins.empty?
- stack << node
- super
- stack.pop
- end
-
- def visit_var_ref(node)
- if node.start_char > pins.first.start_char
- node.pin(stack[-2], pins.shift)
- else
- super
- end
- end
-
- def self.visit(node, tokens)
- start_char = node.start_char
- allocated = []
-
- tokens.reverse_each do |token|
- char = token.location.start_char
- break if char <= start_char
-
- if token.is_a?(Op) && token.value == "^"
- allocated.unshift(tokens.delete(token))
- end
- end
-
- new(allocated).visit(node) if allocated.any?
- 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)
- lbracket = find_token(LBracket)
- lbracket ||= find_token(LParen) if constant
-
- rbracket = find_token(RBracket)
- rbracket ||= find_token(RParen) if constant
-
- parts = [constant, lbracket, *requireds, rest, *posts, rbracket].compact
-
- # The location is going to be determined by the first part to the last
- # part. This includes potential brackets.
- location = parts[0].location.to(parts[-1].location)
-
- # Now that we have the location calculated, we can remove the brackets
- # from the list of tokens.
- tokens.delete(lbracket) if lbracket
- tokens.delete(rbracket) if rbracket
-
- # If there is a plain *, then we're going to fix up the location of it
- # here because it currently doesn't have anything to use for its precise
- # location. If we hit a comma, then we've gone too far.
- if rest.is_a?(VarField) && rest.value.nil?
- tokens.rindex do |rtoken|
- case rtoken
- when Comma
- break
- when Op
- if rtoken.value == "*"
- rest = VarField.new(value: nil, location: rtoken.location)
- break
- end
- end
- end
- end
-
- AryPtn.new(
- constant: constant,
- requireds: requireds || [],
- rest: rest,
- posts: posts || [],
- location: 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 = consume_operator(:**)
-
- AssocSplat.new(
- value: value,
- location: operator.location.to((value || operator).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,
- column: current_column,
- 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,
- column: current_column,
- 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_operator(:^)
-
- if pin && pin.location.start_char < bodystmt.location.start_char
- tokens.delete(pin)
- consume_token(LParen)
-
- rparen = consume_token(RParen)
- location = pin.location.to(rparen.location)
-
- PinnedBegin.new(statement: bodystmt, location: location)
- else
- keyword = consume_keyword(:begin)
- end_location =
- if bodystmt.else_clause
- bodystmt.location
- else
- consume_keyword(:end).location
- end
-
- bodystmt.bind(
- self,
- find_next_statement_start(keyword.location.end_char),
- keyword.location.end_column,
- end_location.end_char,
- end_location.end_column
- )
-
- location = keyword.location.to(end_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.
- range = (left.location.end_char + 1)...right.location.start_char
- index =
- tokens.rindex do |token|
- token.is_a?(Op) && token.name == operator &&
- range.cover?(token.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 { |node| node.is_a?(Op) && %w[| ||].include?(node.value) }
-
- ending = tokens.delete_at(index)
- beginning = ending.value == "||" ? ending : consume_operator(:|)
-
- # If there are no parameters, then we didn't have anything to base the
- # location information of off. Now that we have an opening of the
- # block, we can correct this.
- if params.empty?
- start_line = params.location.start_line
- start_char =
- (
- if beginning.value == "||"
- beginning.location.start_char
- else
- find_next_statement_start(beginning.location.end_char)
- end
- )
-
- location =
- Location.fixed(
- line: start_line,
- char: start_char,
- column: start_char - line_counts[start_line - 1].start
- )
-
- params = params.copy(location: location)
- end
-
- 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 = consume_operator(:&)
-
- 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)
- # In certain versions of Ruby, the `statements` argument can be any node
- # in the case that we're inside of an endless method definition. In this
- # case we'll wrap it in a Statements node to be consistent.
- unless statements.is_a?(Statements)
- statements =
- Statements.new(body: [statements], location: statements.location)
- end
-
- parts = [statements, rescue_clause, else_clause, ensure_clause].compact
-
- BodyStmt.new(
- statements: statements,
- rescue_clause: rescue_clause,
- else_keyword: else_clause && consume_keyword(:else),
- else_clause: else_clause,
- ensure_clause: ensure_clause,
- location: parts.first.location.to(parts.last.location)
- )
- end
-
- # :call-seq:
- # on_brace_block: (
- # (nil | BlockVar) block_var,
- # Statements statements
- # ) -> BlockNode
- def on_brace_block(block_var, statements)
- lbrace = consume_token(LBrace)
- rbrace = consume_token(RBrace)
- location = (block_var || lbrace).location
-
- start_char = find_next_statement_start(location.end_char)
- statements.bind(
- self,
- start_char,
- start_char - line_counts[location.start_line - 1].start,
- rbrace.location.start_char,
- rbrace.location.start_column
- )
-
- location =
- Location.new(
- start_line: lbrace.location.start_line,
- start_char: lbrace.location.start_char,
- start_column: lbrace.location.start_column,
- end_line: [
- rbrace.location.end_line,
- statements.location.end_line
- ].max,
- end_char: rbrace.location.end_char,
- end_column: rbrace.location.end_column
- )
-
- BlockNode.new(
- opening: lbrace,
- block_var: block_var,
- bodystmt: statements,
- location: location
- )
- end
-
- # :call-seq:
- # on_break: (Args arguments) -> Break
- def on_break(arguments)
- keyword = consume_keyword(: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
- # ) -> CallNode
- def on_call(receiver, operator, message)
- ending =
- if message != :call
- message
- elsif operator != :"::"
- operator
- else
- receiver
- end
-
- CallNode.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 value && (operator = find_keyword(:in) || find_operator(:"=>")) &&
- (value.location.end_char...consequent.location.start_char).cover?(
- operator.location.start_char
- )
- tokens.delete(operator)
-
- node =
- RAssign.new(
- value: value,
- operator: operator,
- pattern: consequent,
- location: value.location.to(consequent.location)
- )
-
- PinVisitor.visit(node, tokens)
- node
- else
- keyword = consume_keyword(:case)
-
- Case.new(
- keyword: keyword,
- value: value,
- consequent: consequent,
- location: keyword.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 = consume_keyword(:class)
- ending = consume_keyword(:end)
- location = (superclass || constant).location
- start_char = find_next_statement_start(location.end_char)
-
- bodystmt.bind(
- self,
- start_char,
- start_char - line_counts[location.start_line - 1].start,
- ending.location.start_char,
- ending.location.start_column
- )
-
- 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,
- column: current_column,
- 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,
- block: nil,
- 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,
- block: nil,
- location: receiver.location.to(ending.location)
- )
- end
-
- # :call-seq:
- # on_comment: (String value) -> Comment
- def on_comment(value)
- # char is the index of the # character in the source.
- char = char_pos
- location =
- Location.token(
- line: lineno,
- char: char,
- column: current_column,
- size: value.size - 1
- )
-
- # Loop backward in the source string, starting from the beginning of the
- # comment, and find the first character that is not a space or a tab. If
- # index is -1, this indicates that we've checked all of the characters
- # back to the start of the source, so this comment must be at the
- # beginning of the file.
- #
- # We are purposefully not using rindex or regular expressions here because
- # they check if there are invalid characters, which is actually possible
- # with the use of __END__ syntax.
- index = char - 1
- while index > -1 && (source[index] == "\t" || source[index] == " ")
- index -= 1
- end
-
- # If we found a character that was not a space or a tab before the comment
- # and it's a newline, then this comment is inline. Otherwise, it stands on
- # its own and can be attached as its own node in the tree.
- inline = index != -1 && source[index] != "\n"
- comment =
- Comment.new(value: value.chomp, inline: inline, location: location)
-
- @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,
- column: current_column,
- size: value.size
- )
- )
- end
-
- # :call-seq:
- # on_const_path_field: (untyped parent, Const constant) ->
- # ConstPathField | Field
- def on_const_path_field(parent, constant)
- if constant.is_a?(Const)
- ConstPathField.new(
- parent: parent,
- constant: constant,
- location: parent.location.to(constant.location)
- )
- else
- Field.new(
- parent: parent,
- operator: consume_operator(:"::"),
- name: constant,
- location: parent.location.to(constant.location)
- )
- 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
-
- # :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,
- column: current_column,
- size: value.size
- )
- )
- end
-
- # :call-seq:
- # on_def: (
- # (Backtick | Const | Ident | Kw | Op) name,
- # (nil | Params | Paren) params,
- # untyped bodystmt
- # ) -> DefNode
- 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 = consume_keyword(: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
- end_column = name.location.end_column
- location =
- Location.new(
- start_line: params.location.start_line,
- start_char: end_char,
- start_column: end_column,
- end_line: params.location.end_line,
- end_char: end_char,
- end_column: end_column
- )
-
- params = Params.new(location: location)
- end
-
- ending = find_keyword(:end)
-
- if ending
- tokens.delete(ending)
- start_char = find_next_statement_start(params.location.end_char)
-
- bodystmt.bind(
- self,
- start_char,
- start_char - line_counts[params.location.start_line - 1].start,
- ending.location.start_char,
- ending.location.start_column
- )
-
- DefNode.new(
- target: nil,
- operator: nil,
- 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
-
- DefNode.new(
- target: nil,
- operator: nil,
- name: name,
- params: params,
- bodystmt: statement,
- location: beginning.location.to(bodystmt.location)
- )
- end
- end
-
- # :call-seq:
- # on_defined: (untyped value) -> Defined
- def on_defined(value)
- beginning = consume_keyword(:defined?)
- ending = value
-
- range = beginning.location.end_char...value.location.start_char
- if source[range].include?("(")
- consume_token(LParen)
- ending = consume_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
- # ) -> DefNode
- 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
- end_column = name.location.end_column
- location =
- Location.new(
- start_line: params.location.start_line,
- start_char: end_char,
- start_column: end_column,
- end_line: params.location.end_line,
- end_char: end_char,
- end_column: end_column
- )
-
- params = Params.new(location: location)
- end
-
- beginning = consume_keyword(:def)
- ending = find_keyword(:end)
-
- if ending
- tokens.delete(ending)
- start_char = find_next_statement_start(params.location.end_char)
-
- bodystmt.bind(
- self,
- start_char,
- start_char - line_counts[params.location.start_line - 1].start,
- ending.location.start_char,
- ending.location.start_column
- )
-
- DefNode.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
-
- DefNode.new(
- target: target,
- operator: operator,
- name: name,
- params: params,
- bodystmt: statement,
- location: beginning.location.to(bodystmt.location)
- )
- end
- end
-
- # :call-seq:
- # on_do_block: (BlockVar block_var, BodyStmt bodystmt) -> BlockNode
- def on_do_block(block_var, bodystmt)
- beginning = consume_keyword(:do)
- ending = consume_keyword(:end)
- location = (block_var || beginning).location
- start_char = find_next_statement_start(location.end_char)
-
- bodystmt.bind(
- self,
- start_char,
- start_char - line_counts[location.start_line - 1].start,
- ending.location.start_char,
- ending.location.start_column
- )
-
- BlockNode.new(
- opening: beginning,
- block_var: block_var,
- bodystmt: bodystmt,
- location: beginning.location.to(ending.location)
- )
- end
-
- # :call-seq:
- # on_dot2: ((nil | untyped) left, (nil | untyped) right) -> RangeNode
- def on_dot2(left, right)
- operator = consume_operator(:"..")
-
- beginning = left || operator
- ending = right || operator
-
- RangeNode.new(
- left: left,
- operator: operator,
- right: right,
- location: beginning.location.to(ending.location)
- )
- end
-
- # :call-seq:
- # on_dot3: ((nil | untyped) left, (nil | untyped) right) -> RangeNode
- def on_dot3(left, right)
- operator = consume_operator(:"...")
-
- beginning = left || operator
- ending = right || operator
-
- RangeNode.new(
- left: left,
- operator: operator,
- 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 (symbeg = find_token(SymBeg))
- # A normal dynamic symbol
- tokens.delete(symbeg)
- tstring_end = consume_tstring_end(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 = consume_token(TStringBeg)
- label_end = consume_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)
- keyword = consume_keyword(: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
-
- if index.nil?
- message = "Cannot find expected else ending"
- raise ParseError.new(message, *find_token_error(keyword.location))
- end
-
- node = tokens[index]
- ending = node.value == "end" ? tokens.delete_at(index) : node
-
- start_char = find_next_statement_start(keyword.location.end_char)
- statements.bind(
- self,
- start_char,
- start_char - line_counts[keyword.location.start_line - 1].start,
- ending.location.start_char,
- ending.location.start_column
- )
-
- Else.new(
- keyword: keyword,
- statements: statements,
- location: keyword.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 = consume_keyword(:elsif)
- ending = consequent || consume_keyword(:end)
-
- delimiter =
- find_keyword_between(:then, predicate, statements) ||
- find_token_between(Semicolon, predicate, statements)
-
- tokens.delete(delimiter) if delimiter
- start_char =
- find_next_statement_start((delimiter || predicate).location.end_char)
-
- statements.bind(
- self,
- start_char,
- start_char - line_counts[predicate.location.start_line - 1].start,
- ending.location.start_char,
- ending.location.start_column
- )
-
- 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, column: current_column, 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,
- start_column: location.start_column,
- end_line: lineno,
- end_char: char_pos + value.length - 1,
- end_column: current_column + 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,
- column: current_column,
- 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,
- column: current_column,
- 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,
- column: current_column,
- size: value.size
- )
- )
-
- tokens << node
- node
- end
-
- # :call-seq:
- # on_ensure: (Statements statements) -> Ensure
- def on_ensure(statements)
- keyword = consume_keyword(:ensure)
-
- # We don't want to consume the :@kw event, because that would break
- # def..ensure..end chains.
- ending = find_keyword(:end)
- start_char = find_next_statement_start(keyword.location.end_char)
- statements.bind(
- self,
- start_char,
- start_char - line_counts[keyword.location.start_line - 1].start,
- ending.location.start_char,
- ending.location.start_column
- )
-
- 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 = consume_token(Comma)
-
- ExcessedComma.new(value: comma.value, location: comma.location)
- end
-
- # :call-seq:
- # on_fcall: ((Const | Ident) value) -> CallNode
- def on_fcall(value)
- CallNode.new(
- receiver: nil,
- operator: nil,
- message: value,
- arguments: nil,
- location: value.location
- )
- end
-
- # :call-seq:
- # on_field: (
- # untyped parent,
- # (:"::" | Op | Period | 73) operator
- # (Const | Ident) name
- # ) -> Field
- def on_field(parent, operator, name)
- Field.new(
- parent: parent,
- operator: operator == 73 ? :"::" : 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,
- column: current_column,
- 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)
- # The left and right of a find pattern are always going to be splats, so
- # we're going to consume the * operators and use their location
- # information to extend the location of the splats.
- right, left =
- [right, left].map do |node|
- operator = consume_operator(:*)
- location =
- if node.value
- operator.location.to(node.location)
- else
- operator.location
- end
-
- node.copy(location: location)
- end
-
- # The opening of this find pattern is either going to be a left bracket, a
- # right left parenthesis, or the left splat. We're going to use this to
- # determine how to find the closing of the pattern, as well as determining
- # the location of the node.
- opening = find_token(LBracket) || find_token(LParen) || left
-
- # The closing is based on the opening, which is either the matched
- # punctuation or the right splat.
- closing =
- case opening
- when LBracket
- tokens.delete(opening)
- consume_token(RBracket)
- when LParen
- tokens.delete(opening)
- consume_token(RParen)
- else
- right
- end
-
- FndPtn.new(
- constant: constant,
- left: left,
- values: values,
- right: right,
- location: (constant || opening).location.to(closing.location)
- )
- end
-
- # :call-seq:
- # on_for: (
- # (MLHS | VarField) value,
- # untyped collection,
- # Statements statements
- # ) -> For
- def on_for(index, collection, statements)
- beginning = consume_keyword(:for)
- in_keyword = consume_keyword(:in)
- ending = consume_keyword(:end)
-
- delimiter =
- find_keyword_between(:do, collection, ending) ||
- find_token_between(Semicolon, collection, ending)
-
- tokens.delete(delimiter) if delimiter
-
- start_char =
- find_next_statement_start((delimiter || collection).location.end_char)
-
- statements.bind(
- self,
- start_char,
- start_char -
- line_counts[(delimiter || collection).location.end_line - 1].start,
- ending.location.start_char,
- ending.location.start_column
- )
-
- 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,
- column: current_column,
- size: value.size
- )
- )
- end
-
- # :call-seq:
- # on_hash: ((nil | Array[AssocNew | AssocSplat]) assocs) -> HashLiteral
- def on_hash(assocs)
- lbrace = consume_token(LBrace)
- rbrace = consume_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,
- column: current_column,
- size: value.size
- )
-
- # 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,
- dedent: width,
- parts: string.parts,
- location: heredoc.location
- )
- end
-
- # :call-seq:
- # on_heredoc_end: (String value) -> Heredoc
- def on_heredoc_end(value)
- heredoc = @heredocs[-1]
-
- location =
- Location.token(
- line: lineno,
- char: char_pos,
- column: current_column,
- size: value.size
- )
-
- heredoc_end = HeredocEnd.new(value: value.chomp, location: location)
-
- @heredocs[-1] = Heredoc.new(
- beginning: heredoc.beginning,
- ending: heredoc_end,
- dedent: heredoc.dedent,
- parts: heredoc.parts,
- location:
- Location.new(
- start_line: heredoc.location.start_line,
- start_char: heredoc.location.start_char,
- start_column: heredoc.location.start_column,
- end_line: location.end_line,
- end_char: location.end_char,
- end_column: location.end_column
- )
- )
- end
-
- # :call-seq:
- # on_hshptn: (
- # (nil | untyped) constant,
- # Array[[Label | StringContent, untyped]] keywords,
- # (nil | VarField) keyword_rest
- # ) -> HshPtn
- def on_hshptn(constant, keywords, keyword_rest)
- keywords =
- (keywords || []).map do |(label, value)|
- if label.is_a?(Label)
- [label, value]
- else
- tstring_beg_index =
- tokens.rindex do |token|
- token.is_a?(TStringBeg) &&
- token.location.start_char < label.location.start_char
- end
-
- tstring_beg = tokens.delete_at(tstring_beg_index)
-
- label_end_index =
- tokens.rindex do |token|
- token.is_a?(LabelEnd) &&
- token.location.start_char == label.location.end_char
- end
-
- label_end = tokens.delete_at(label_end_index)
-
- [
- DynaSymbol.new(
- parts: label.parts,
- quote: label_end.value[0],
- location: tstring_beg.location.to(label_end.location)
- ),
- value
- ]
- end
- end
-
- if keyword_rest
- # We're doing this to delete the token from the list so that it doesn't
- # confuse future patterns by thinking they have an extra ** on the end.
- consume_operator(:**)
- elsif (token = find_operator(:**))
- tokens.delete(token)
-
- # Create an artificial VarField if we find an extra ** on the end. This
- # means the formatting will be a little more consistent.
- keyword_rest = VarField.new(value: nil, location: token.location)
- end
-
- parts = [constant, *keywords.flatten(1), keyword_rest].compact
-
- # If there's no constant, there may be braces, so we're going to look for
- # those to get our bounds.
- unless constant
- lbrace = find_token(LBrace)
- rbrace = find_token(RBrace)
-
- if lbrace && rbrace
- parts = [lbrace, *parts, rbrace]
- tokens.delete(lbrace)
- tokens.delete(rbrace)
- end
- end
-
- 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,
- column: current_column,
- size: value.size
- )
- )
- end
-
- # :call-seq:
- # on_if: (
- # untyped predicate,
- # Statements statements,
- # (nil | Elsif | Else) consequent
- # ) -> IfNode
- def on_if(predicate, statements, consequent)
- beginning = consume_keyword(:if)
- ending = consequent || consume_keyword(:end)
-
- if (keyword = find_keyword_between(:then, predicate, ending))
- tokens.delete(keyword)
- end
-
- start_char =
- find_next_statement_start((keyword || predicate).location.end_char)
-
- statements.bind(
- self,
- start_char,
- start_char - line_counts[predicate.location.end_line - 1].start,
- ending.location.start_char,
- ending.location.start_column
- )
-
- IfNode.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) -> IfNode
- def on_if_mod(predicate, statement)
- consume_keyword(:if)
-
- IfNode.new(
- predicate: predicate,
- statements:
- Statements.new(body: [statement], location: statement.location),
- consequent: nil,
- 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,
- column: current_column,
- 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 = consume_keyword(:in)
- ending = consequent || consume_keyword(:end)
-
- statements_start = pattern
- if (token = find_keyword_between(:then, pattern, statements))
- tokens.delete(token)
- statements_start = token
- end
-
- start_char =
- find_next_statement_start((token || statements_start).location.end_char)
-
- # Ripper ignores parentheses on patterns, so we need to do the same in
- # order to attach comments correctly to the pattern.
- if source[start_char] == ")"
- start_char = find_next_statement_start(start_char + 1)
- end
-
- statements.bind(
- self,
- start_char,
- start_char -
- line_counts[statements_start.location.start_line - 1].start,
- ending.location.start_char,
- ending.location.start_column
- )
-
- node =
- In.new(
- pattern: pattern,
- statements: statements,
- consequent: consequent,
- location: beginning.location.to(ending.location)
- )
-
- PinVisitor.visit(node, tokens)
- node
- end
-
- # :call-seq:
- # on_int: (String value) -> Int
- def on_int(value)
- Int.new(
- value: value,
- location:
- Location.token(
- line: lineno,
- char: char_pos,
- column: current_column,
- 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,
- column: current_column,
- 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,
- column: current_column,
- size: value.size
- )
- )
-
- tokens << node
- node
- end
-
- # :call-seq:
- # on_kwrest_param: ((nil | Ident) name) -> KwRestParam
- def on_kwrest_param(name)
- location = consume_operator(:**).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,
- column: current_column,
- 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,
- column: current_column,
- size: value.size
- )
- )
-
- tokens << node
- node
- end
-
- # :call-seq:
- # on_lambda: (
- # (Params | Paren) params,
- # (BodyStmt | Statements) statements
- # ) -> Lambda
- def on_lambda(params, statements)
- beginning = consume_token(TLambda)
- braces =
- tokens.any? do |token|
- token.is_a?(TLamBeg) &&
- token.location.start_char > beginning.location.start_char
- end
-
- if braces
- opening = consume_token(TLamBeg)
- closing = consume_token(RBrace)
- else
- opening = consume_keyword(:do)
- closing = consume_keyword(:end)
- end
-
- # We need to do some special mapping here. Since ripper doesn't support
- # capturing lambda vars, we need to normalize all of that here.
- params =
- if params.is_a?(Paren)
- # In this case we've gotten to the parentheses wrapping a set of
- # parameters case. Here we need to manually scan for lambda locals.
- range = (params.location.start_char + 1)...params.location.end_char
- locals = lambda_locals(source[range])
-
- location = params.contents.location
- location = location.to(locals.last.location) if locals.any?
-
- node =
- Paren.new(
- lparen: params.lparen,
- contents:
- LambdaVar.new(
- params: params.contents,
- locals: locals,
- location: location
- ),
- location: params.location
- )
-
- node.comments.concat(params.comments)
- node
- else
- # If there are no parameters, then we didn't have anything to base the
- # location information of off. Now that we have an opening of the
- # block, we can correct this.
- if params.empty?
- opening_location = opening.location
- location =
- Location.fixed(
- line: opening_location.start_line,
- char: opening_location.start_char,
- column: opening_location.start_column
- )
-
- params = params.copy(location: location)
- end
-
- # In this case we've gotten to the plain set of parameters. In this
- # case there cannot be lambda locals, so we will wrap the parameters
- # into a lambda var that has no locals.
- LambdaVar.new(params: params, locals: [], location: params.location)
- end
-
- start_char = find_next_statement_start(opening.location.end_char)
- statements.bind(
- self,
- start_char,
- start_char - line_counts[opening.location.end_line - 1].start,
- closing.location.start_char,
- closing.location.start_column
- )
-
- Lambda.new(
- params: params,
- statements: statements,
- location: beginning.location.to(closing.location)
- )
- end
-
- # :call-seq:
- # on_lambda_var: (Params params, Array[ Ident ] locals) -> LambdaVar
- def on_lambda_var(params, locals)
- location = params.location
- location = location.to(locals.last.location) if locals.any?
-
- LambdaVar.new(params: params, locals: locals || [], location: location)
- end
-
- # Ripper doesn't support capturing lambda local variables until 3.2. To
- # mitigate this, we have to parse that code for ourselves. We use the range
- # from the parentheses to find where we _should_ be looking. Then we check
- # if the resulting tokens match a pattern that we determine means that the
- # declaration has block-local variables. Once it does, we parse those out
- # and convert them into Ident nodes.
- def lambda_locals(source)
- tokens = Ripper.lex(source)
-
- # First, check that we have a semi-colon. If we do, then we can start to
- # parse the tokens _after_ the semicolon.
- index = tokens.rindex { |token| token[1] == :on_semicolon }
- return [] unless index
-
- # Next, map over the tokens and convert them into Ident nodes. Bail out
- # midway through if we encounter a token we didn't expect. Basically we're
- # making our own mini-parser here. To do that we'll walk through a small
- # state machine:
- #
- # ┌────────┐ ┌────────┐ ┌─────────┐
- # │ │ │ │ │┌───────┐│
- # ──> │ item │ ─── ident ──> │ next │ ─── rparen ──> ││ final ││
- # │ │ <── comma ─── │ │ │└───────┘│
- # └────────┘ └────────┘ └─────────┘
- # │ ^ │ ^
- # └──┘ └──┘
- # ignored_nl, sp nl, sp
- #
- state = :item
- transitions = {
- item: {
- on_ignored_nl: :item,
- on_sp: :item,
- on_ident: :next
- },
- next: {
- on_nl: :next,
- on_sp: :next,
- on_comma: :item,
- on_rparen: :final
- },
- final: {
- }
- }
-
- parent_line = lineno - 1
- parent_column =
- consume_token(Semicolon).location.start_column - tokens[index][0][1]
-
- tokens[(index + 1)..].each_with_object([]) do |token, locals|
- (lineno, column), type, value, = token
- column += parent_column if lineno == 1
- lineno += parent_line
-
- # Make the state transition for the parser. If there isn't a transition
- # from the current state to a new state for this type, then we're in a
- # pattern that isn't actually locals. In that case we can return [].
- state = transitions[state].fetch(type) { return [] }
-
- # If we hit an identifier, then add it to our list.
- next if type != :on_ident
-
- location =
- Location.token(
- line: lineno,
- char: line_counts[lineno - 1][column],
- column: column,
- size: value.size
- )
-
- locals << Ident.new(value: value, location: location)
- 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,
- column: current_column,
- 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,
- column: current_column,
- 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,
- column: current_column,
- 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: (
- # CallNode call,
- # (ArgParen | Args) arguments
- # ) -> CallNode
- def on_method_add_arg(call, arguments)
- location = call.location
- location = location.to(arguments.location) if arguments.is_a?(ArgParen)
-
- CallNode.new(
- receiver: call.receiver,
- operator: call.operator,
- message: call.message,
- arguments: arguments,
- location: location
- )
- end
-
- # :call-seq:
- # on_method_add_block: (
- # (Break | Call | Command | CommandCall, Next) call,
- # Block block
- # ) -> Break | MethodAddBlock
- def on_method_add_block(call, block)
- location = call.location.to(block.location)
-
- case call
- when Break, Next, ReturnNode
- parts = call.arguments.parts
-
- node = parts.pop
- copied =
- node.copy(block: block, location: node.location.to(block.location))
-
- copied.comments.concat(call.comments)
- parts << copied
-
- call.copy(location: location)
- when Command, CommandCall
- node = call.copy(block: block, location: location)
- node.comments.concat(call.comments)
- node
- else
- MethodAddBlock.new(call: call, block: block, location: location)
- 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 = consume_operator(:*)
- 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, column: current_column)
- )
- end
-
- # :call-seq:
- # on_mlhs_paren: ((MLHS | MLHSParen) contents) -> MLHSParen
- def on_mlhs_paren(contents)
- lparen = consume_token(LParen)
- rparen = consume_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 = consume_keyword(:module)
- ending = consume_keyword(:end)
- start_char = find_next_statement_start(constant.location.end_char)
-
- bodystmt.bind(
- self,
- start_char,
- start_char - line_counts[constant.location.start_line - 1].start,
- ending.location.start_char,
- ending.location.start_column
- )
-
- 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, column: current_column)
- )
- end
-
- # :call-seq:
- # on_mrhs_add: (MRHS mrhs, untyped part) -> MRHS
- def on_mrhs_add(mrhs, part)
- location =
- (mrhs.parts.empty? ? mrhs.location : mrhs.location.to(part.location))
-
- 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 = consume_operator(:*)
- 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 = consume_keyword(: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,
- column: current_column,
- 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
- )
- # This is to make it so that required keyword arguments
- # have a `nil` for the value instead of a `false`.
- keywords&.map! { |(key, value)| [key, value || nil] }
-
- # Here we're going to build up a list of all of the params so that we can
- # determine our location information.
- parts = []
-
- requireds&.each { |required| parts << required.location }
- optionals&.each do |(key, value)|
- parts << key.location
- parts << value.location if value
- end
-
- parts << rest.location if rest
- posts&.each { |post| parts << post.location }
-
- keywords&.each do |(key, value)|
- parts << key.location
- parts << value.location if value
- end
-
- if keyword_rest == :nil
- # When we get a :nil here, it means that we have **nil syntax, which
- # means this set of parameters accepts no more keyword arguments. In
- # this case we need to go and find the location of these two tokens.
- operator = consume_operator(:**)
- parts << operator.location.to(consume_keyword(:nil).location)
- elsif keyword_rest
- parts << keyword_rest.location
- end
-
- parts << block.location if block && block != :&
- parts = parts.compact
-
- location =
- if parts.any?
- parts[0].to(parts[-1])
- else
- Location.fixed(line: lineno, char: char_pos, column: current_column)
- 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 = consume_token(LParen)
- rparen = consume_token(RParen)
-
- if contents.is_a?(Params)
- location = contents.location
- start_char = find_next_statement_start(lparen.location.end_char)
- location =
- Location.new(
- start_line: location.start_line,
- start_char: start_char,
- start_column:
- start_char - line_counts[lparen.location.start_line - 1].start,
- end_line: location.end_line,
- end_char: rparen.location.start_char,
- end_column: rparen.location.start_column
- )
-
- 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
- alias compile_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,
- column: current_column,
- size: value.size
- )
- )
- end
-
- # :call-seq:
- # on_program: (Statements statements) -> Program
- def on_program(statements)
- last_column = source.length - line_counts.last.start
- location =
- Location.new(
- start_line: 1,
- start_char: 0,
- start_column: 0,
- end_line: line_counts.length - 1,
- end_char: source.length,
- end_column: last_column
- )
-
- statements.body << @__end__ if @__end__
- statements.bind(self, 0, 0, source.length, last_column)
-
- 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,
- column: current_column,
- size: value.size
- )
- )
-
- tokens << node
- node
- end
-
- # :call-seq:
- # on_qsymbols_new: () -> QSymbols
- def on_qsymbols_new
- beginning = consume_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,
- column: current_column,
- size: value.size
- )
- )
-
- tokens << node
- node
- end
-
- # :call-seq:
- # on_qwords_new: () -> QWords
- def on_qwords_new
- beginning = consume_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,
- column: current_column,
- 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,
- column: current_column,
- 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,
- column: current_column,
- size: value.size
- )
- )
-
- tokens << node
- node
- end
-
- # :call-seq:
- # on_redo: () -> Redo
- def on_redo
- keyword = consume_keyword(:redo)
-
- Redo.new(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,
- column: current_column,
- 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,
- column: current_column,
- size: value.size
- )
- )
- end
-
- # :call-seq:
- # on_regexp_literal: (
- # RegexpContent regexp_content,
- # (nil | RegexpEnd) ending
- # ) -> RegexpLiteral
- def on_regexp_literal(regexp_content, ending)
- location = regexp_content.location
-
- if ending.nil?
- message = "Cannot find expected regular expression ending"
- raise ParseError.new(message, *find_token_error(location))
- end
-
- RegexpLiteral.new(
- beginning: regexp_content.beginning,
- ending: ending.value,
- parts: regexp_content.parts,
- location: location.to(ending.location)
- )
- end
-
- # :call-seq:
- # on_regexp_new: () -> RegexpContent
- def on_regexp_new
- regexp_beg = consume_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 = consume_keyword(:rescue)
- exceptions = exceptions[0] if exceptions.is_a?(Array)
-
- last_node = variable || exceptions || keyword
- start_char = find_next_statement_start(last_node.end_char)
- statements.bind(
- self,
- start_char,
- start_char - line_counts[last_node.location.start_line - 1].start,
- char_pos,
- current_column
- )
-
- # 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,
- start_column: keyword.location.end_column + 1,
- end_line: last_node.location.end_line,
- end_char: last_node.end_char,
- end_column: last_node.location.end_column
- )
- )
- end
-
- Rescue.new(
- keyword: keyword,
- exception: rescue_ex,
- statements: statements,
- consequent: consequent,
- location:
- Location.new(
- start_line: keyword.location.start_line,
- start_char: keyword.location.start_char,
- start_column: keyword.location.start_column,
- end_line: lineno,
- end_char: char_pos,
- end_column: current_column
- )
- )
- end
-
- # :call-seq:
- # on_rescue_mod: (untyped statement, untyped value) -> RescueMod
- def on_rescue_mod(statement, value)
- consume_keyword(: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 = consume_operator(:*).location
- location = location.to(name.location) if name
-
- RestParam.new(name: name, location: location)
- end
-
- # :call-seq:
- # on_retry: () -> Retry
- def on_retry
- keyword = consume_keyword(:retry)
-
- Retry.new(location: keyword.location)
- end
-
- # :call-seq:
- # on_return: (Args arguments) -> ReturnNode
- def on_return(arguments)
- keyword = consume_keyword(:return)
-
- ReturnNode.new(
- arguments: arguments,
- location: keyword.location.to(arguments.location)
- )
- end
-
- # :call-seq:
- # on_return0: () -> ReturnNode
- def on_return0
- keyword = consume_keyword(:return)
-
- ReturnNode.new(arguments: nil, 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,
- column: current_column,
- size: value.size
- )
- )
-
- tokens << node
- node
- end
-
- # :call-seq:
- # on_sclass: (untyped target, BodyStmt bodystmt) -> SClass
- def on_sclass(target, bodystmt)
- beginning = consume_keyword(:class)
- ending = consume_keyword(:end)
- start_char = find_next_statement_start(target.location.end_char)
-
- bodystmt.bind(
- self,
- start_char,
- start_char - line_counts[target.location.start_line - 1].start,
- ending.location.start_char,
- ending.location.start_column
- )
-
- SClass.new(
- target: target,
- bodystmt: bodystmt,
- location: beginning.location.to(ending.location)
- )
- end
-
- # Semicolons are tokens that get added to the token list but never get
- # attached to the AST. Because of this they only need to track their
- # associated location so they can be used for computing bounds.
- class Semicolon
- attr_reader :location
-
- def initialize(location)
- @location = location
- end
- end
-
- # :call-seq:
- # on_semicolon: (String value) -> Semicolon
- def on_semicolon(value)
- tokens << Semicolon.new(
- Location.token(
- line: lineno,
- char: char_pos,
- column: current_column,
- size: value.size
- )
- )
- 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(body: statements.body << statement, location: location)
- end
-
- # :call-seq:
- # on_stmts_new: () -> Statements
- def on_stmts_new
- Statements.new(
- body: [],
- location:
- Location.fixed(line: lineno, char: char_pos, column: current_column)
- )
- end
-
- # :call-seq:
- # on_string_add: (
- # String string,
- # (StringEmbExpr | StringDVar | TStringContent) part
- # ) -> StringContent
- def on_string_add(string, part)
- # Due to some eccentricities in how ripper works, you need this here in
- # case you have a syntax error with an embedded expression that doesn't
- # finish, as in: "#{"
- return string if part.is_a?(String)
-
- 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, column: current_column)
- )
- end
-
- # :call-seq:
- # on_string_dvar: ((Backref | VarRef) variable) -> StringDVar
- def on_string_dvar(variable)
- embvar = consume_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 = consume_token(EmbExprBeg)
- embexpr_end = consume_token(EmbExprEnd)
-
- statements.bind(
- self,
- embexpr_beg.location.end_char,
- embexpr_beg.location.end_column,
- embexpr_end.location.start_char,
- embexpr_end.location.start_column
- )
-
- location =
- Location.new(
- start_line: embexpr_beg.location.start_line,
- start_char: embexpr_beg.location.start_char,
- start_column: embexpr_beg.location.start_column,
- end_line: [
- embexpr_end.location.end_line,
- statements.location.end_line
- ].max,
- end_char: embexpr_end.location.end_char,
- end_column: embexpr_end.location.end_column
- )
-
- 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&.ending
- heredoc = @heredocs.pop
-
- Heredoc.new(
- beginning: heredoc.beginning,
- ending: heredoc.ending,
- dedent: heredoc.dedent,
- parts: string.parts,
- location: heredoc.location
- )
- else
- tstring_beg = consume_token(TStringBeg)
- tstring_end = consume_tstring_end(tstring_beg.location)
-
- location =
- Location.new(
- start_line: tstring_beg.location.start_line,
- start_char: tstring_beg.location.start_char,
- start_column: tstring_beg.location.start_column,
- end_line: [
- tstring_end.location.end_line,
- string.location.end_line
- ].max,
- end_char: tstring_end.location.end_char,
- end_column: tstring_end.location.end_column
- )
-
- 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 = consume_keyword(: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,
- column: current_column,
- 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 = consume_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,
- column: current_column,
- size: value.size
- )
- )
-
- tokens << node
- node
- end
-
- # :call-seq:
- # on_symbols_new: () -> Symbols
- def on_symbols_new
- beginning = consume_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,
- column: current_column,
- 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,
- column: current_column,
- 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,
- column: current_column,
- 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,
- column: current_column,
- 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,
- column: current_column,
- 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 = consume_keyword(:not)
- ending = statement || beginning
- parentheses = source[beginning.location.end_char] == "("
-
- if parentheses
- consume_token(LParen)
- ending = consume_token(RParen)
- end
-
- Not.new(
- statement: statement,
- parentheses: parentheses,
- 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 = consume_keyword(: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)
- # ) -> UnlessNode
- def on_unless(predicate, statements, consequent)
- beginning = consume_keyword(:unless)
- ending = consequent || consume_keyword(:end)
-
- if (keyword = find_keyword_between(:then, predicate, ending))
- tokens.delete(keyword)
- end
-
- start_char =
- find_next_statement_start((keyword || predicate).location.end_char)
-
- statements.bind(
- self,
- start_char,
- start_char - line_counts[predicate.location.end_line - 1].start,
- ending.location.start_char,
- ending.location.start_column
- )
-
- UnlessNode.new(
- predicate: predicate,
- statements: statements,
- consequent: consequent,
- location: beginning.location.to(ending.location)
- )
- end
-
- # :call-seq:
- # on_unless_mod: (untyped predicate, untyped statement) -> UnlessNode
- def on_unless_mod(predicate, statement)
- consume_keyword(:unless)
-
- UnlessNode.new(
- predicate: predicate,
- statements:
- Statements.new(body: [statement], location: statement.location),
- consequent: nil,
- location: statement.location.to(predicate.location)
- )
- end
-
- # :call-seq:
- # on_until: (untyped predicate, Statements statements) -> UntilNode
- def on_until(predicate, statements)
- beginning = consume_keyword(:until)
- ending = consume_keyword(:end)
-
- delimiter =
- find_keyword_between(:do, predicate, statements) ||
- find_token_between(Semicolon, predicate, statements)
-
- tokens.delete(delimiter) if delimiter
-
- # Update the Statements location information
- start_char =
- find_next_statement_start((delimiter || predicate).location.end_char)
-
- statements.bind(
- self,
- start_char,
- start_char - line_counts[predicate.location.end_line - 1].start,
- ending.location.start_char,
- ending.location.start_column
- )
-
- UntilNode.new(
- predicate: predicate,
- statements: statements,
- location: beginning.location.to(ending.location)
- )
- end
-
- # :call-seq:
- # on_until_mod: (untyped predicate, untyped statement) -> UntilNode
- def on_until_mod(predicate, statement)
- consume_keyword(:until)
-
- UntilNode.new(
- predicate: predicate,
- statements:
- Statements.new(body: [statement], location: statement.location),
- location: statement.location.to(predicate.location)
- )
- end
-
- # :call-seq:
- # on_var_alias: (GVar left, (Backref | GVar) right) -> AliasNode
- def on_var_alias(left, right)
- keyword = consume_keyword(:alias)
-
- AliasNode.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 != :nil
- 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, column: current_column)
- 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)
- VarRef.new(value: value, location: value.location)
- 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, column: current_column)
- )
- end
-
- # :call-seq:
- # on_when: (
- # Args arguments,
- # Statements statements,
- # (nil | Else | When) consequent
- # ) -> When
- def on_when(arguments, statements, consequent)
- beginning = consume_keyword(:when)
- ending = consequent || consume_keyword(:end)
-
- statements_start = arguments
- if (token = find_keyword(:then))
- tokens.delete(token)
- statements_start = token
- end
-
- start_char =
- find_next_statement_start((token || statements_start).location.end_char)
-
- statements.bind(
- self,
- start_char,
- start_char -
- line_counts[statements_start.location.start_line - 1].start,
- ending.location.start_char,
- ending.location.start_column
- )
-
- When.new(
- arguments: arguments,
- statements: statements,
- consequent: consequent,
- location: beginning.location.to(ending.location)
- )
- end
-
- # :call-seq:
- # on_while: (untyped predicate, Statements statements) -> WhileNode
- def on_while(predicate, statements)
- beginning = consume_keyword(:while)
- ending = consume_keyword(:end)
-
- delimiter =
- find_keyword_between(:do, predicate, statements) ||
- find_token_between(Semicolon, predicate, statements)
-
- tokens.delete(delimiter) if delimiter
-
- # Update the Statements location information
- start_char =
- find_next_statement_start((delimiter || predicate).location.end_char)
-
- statements.bind(
- self,
- start_char,
- start_char - line_counts[predicate.location.end_line - 1].start,
- ending.location.start_char,
- ending.location.start_column
- )
-
- WhileNode.new(
- predicate: predicate,
- statements: statements,
- location: beginning.location.to(ending.location)
- )
- end
-
- # :call-seq:
- # on_while_mod: (untyped predicate, untyped statement) -> WhileNode
- def on_while_mod(predicate, statement)
- consume_keyword(:while)
-
- WhileNode.new(
- predicate: predicate,
- statements:
- Statements.new(body: [statement], location: statement.location),
- 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, column: current_column)
- )
- 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,
- column: current_column,
- size: value.size
- )
- )
-
- tokens << node
- node
- end
-
- # :call-seq:
- # on_words_new: () -> Words
- def on_words_new
- beginning = consume_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
- consume_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,
- dedent: heredoc.dedent,
- parts: xstring.parts,
- location: heredoc.location
- )
- else
- ending = consume_tstring_end(xstring.location)
-
- XStringLiteral.new(
- parts: xstring.parts,
- location: xstring.location.to(ending.location)
- )
- end
- end
-
- # :call-seq:
- # on_yield: ((Args | Paren) arguments) -> YieldNode
- def on_yield(arguments)
- keyword = consume_keyword(:yield)
-
- YieldNode.new(
- arguments: arguments,
- location: keyword.location.to(arguments.location)
- )
- end
-
- # :call-seq:
- # on_yield0: () -> YieldNode
- def on_yield0
- keyword = consume_keyword(:yield)
-
- YieldNode.new(arguments: nil, location: keyword.location)
- end
-
- # :call-seq:
- # on_zsuper: () -> ZSuper
- def on_zsuper
- keyword = consume_keyword(:super)
-
- ZSuper.new(location: keyword.location)
- end
- end
-end
diff --git a/lib/syntax_tree/plugin/disable_auto_ternary.rb b/lib/syntax_tree/plugin/disable_auto_ternary.rb
deleted file mode 100644
index dd38c783..00000000
--- a/lib/syntax_tree/plugin/disable_auto_ternary.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-module SyntaxTree
- class Formatter
- DISABLE_AUTO_TERNARY = true
- end
-end
diff --git a/lib/syntax_tree/plugin/single_quotes.rb b/lib/syntax_tree/plugin/single_quotes.rb
deleted file mode 100644
index c7405e2c..00000000
--- a/lib/syntax_tree/plugin/single_quotes.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-module SyntaxTree
- class Formatter
- SINGLE_QUOTES = true
- end
-end
diff --git a/lib/syntax_tree/plugin/trailing_comma.rb b/lib/syntax_tree/plugin/trailing_comma.rb
deleted file mode 100644
index 1ae2b96d..00000000
--- a/lib/syntax_tree/plugin/trailing_comma.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-module SyntaxTree
- class Formatter
- TRAILING_COMMA = true
- end
-end
diff --git a/lib/syntax_tree/rake.rb b/lib/syntax_tree/rake.rb
new file mode 100644
index 00000000..e7fd5243
--- /dev/null
+++ b/lib/syntax_tree/rake.rb
@@ -0,0 +1,128 @@
+# frozen_string_literal: true
+
+require "syntax_tree"
+require "rake"
+require "rake/tasklib"
+
+module SyntaxTree
+ module Rake
+ # A parent Rake task that runs a command on a set of source files.
+ class Task < ::Rake::TaskLib
+ # Name of the task.
+ attr_accessor :name
+
+ # Glob pattern to match source files. Defaults to 'lib/**/*.rb'.
+ attr_accessor :source_files
+
+ # Glob pattern to ignore source files. String. Optional.
+ attr_accessor :ignore_files
+
+ # List of plugins to load. Array of strings. Optional.
+ attr_accessor :plugins
+
+ # Print width for the formatter. Integer. Optional.
+ attr_accessor :print_width
+
+ # Preferred quote style. " or '. Optional.
+ attr_accessor :preferred_quote
+
+ # Trailing comma style. Boolean. Optional.
+ attr_accessor :trailing_comma
+
+ def initialize(
+ name = :"stree:#{command}",
+ source_files = ::Rake::FileList["lib/**/*.rb"],
+ ignore_files = "",
+ plugins = [],
+ print_width = :default,
+ preferred_quote = :default,
+ trailing_comma = :default
+ )
+ @name = name
+
+ @source_files = source_files
+ @ignore_files = ignore_files
+ @plugins = plugins
+
+ @print_width = print_width
+ @preferred_quote = preferred_quote
+ @trailing_comma = trailing_comma
+
+ yield self if block_given?
+ define_task
+ end
+
+ private
+
+ def command
+ raise NotImplementedError
+ end
+
+ def define_task
+ desc "Runs `stree #{command}` over source files"
+ task(name) do
+ arguments = [command]
+
+ arguments << "--ignore-files=#{ignore_files}" if ignore_files != ""
+ arguments << "--plugins=#{plugins.join(",")}" if plugins.any?
+
+ arguments << "--print-width=#{print_width}" if print_width != :default
+ arguments << "--preferred-quote=#{preferred_quote}" if preferred_quote != :default
+
+ if trailing_comma != :default
+ arguments << "--#{"no-" unless trailing_comma}trailing-comma"
+ end
+
+ arguments.concat(Array(source_files))
+ abort if CLI.run(arguments) != 0
+ end
+ end
+ end
+
+ private_constant :Task
+
+ # A Rake task that runs check on a set of source files.
+ #
+ # Example:
+ #
+ # require "syntax_tree/rake"
+ #
+ # SyntaxTree::Rake::CheckTask.new do |t|
+ # t.source_files = "{app,config,lib}/**/*.rb"
+ # end
+ #
+ # This will create a task that can be run with:
+ #
+ # rake stree:check
+ #
+ class CheckTask < Task
+ private
+
+ def command
+ "check"
+ end
+ end
+
+ # A Rake task that runs write on a set of source files.
+ #
+ # Example:
+ #
+ # require "syntax_tree/rake"
+ #
+ # SyntaxTree::Rake::WriteTask.new do |t|
+ # t.source_files = "{app,config,lib}/**/*.rb"
+ # end
+ #
+ # This will create a task that can be run with:
+ #
+ # rake stree:write
+ #
+ class WriteTask < Task
+ private
+
+ def command
+ "write"
+ end
+ end
+ end
+end
diff --git a/lib/syntax_tree/rake/check_task.rb b/lib/syntax_tree/rake/check_task.rb
deleted file mode 100644
index 5b441a5b..00000000
--- a/lib/syntax_tree/rake/check_task.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-# frozen_string_literal: true
-
-require_relative "task"
-
-module SyntaxTree
- module Rake
- # A Rake task that runs check on a set of source files.
- #
- # Example:
- #
- # require "syntax_tree/rake/check_task"
- #
- # SyntaxTree::Rake::CheckTask.new do |t|
- # t.source_files = "{app,config,lib}/**/*.rb"
- # end
- #
- # This will create task that can be run with:
- #
- # rake stree:check
- #
- class CheckTask < Task
- private
-
- def command
- "check"
- end
- end
- end
-end
diff --git a/lib/syntax_tree/rake/task.rb b/lib/syntax_tree/rake/task.rb
deleted file mode 100644
index e9a20433..00000000
--- a/lib/syntax_tree/rake/task.rb
+++ /dev/null
@@ -1,85 +0,0 @@
-# frozen_string_literal: true
-
-require "rake"
-require "rake/tasklib"
-
-require "syntax_tree"
-require "syntax_tree/cli"
-
-module SyntaxTree
- module Rake
- # A parent Rake task that runs a command on a set of source files.
- class Task < ::Rake::TaskLib
- # Name of the task.
- attr_accessor :name
-
- # Glob pattern to match source files.
- # Defaults to 'lib/**/*.rb'.
- attr_accessor :source_files
-
- # The set of plugins to require.
- # Defaults to [].
- attr_accessor :plugins
-
- # Max line length.
- # Defaults to 80.
- attr_accessor :print_width
-
- # The target Ruby version to use for formatting.
- # Defaults to Gem::Version.new(RUBY_VERSION).
- attr_accessor :target_ruby_version
-
- # Glob pattern to ignore source files.
- # Defaults to ''.
- attr_accessor :ignore_files
-
- def initialize(
- name = :"stree:#{command}",
- source_files = ::Rake::FileList["lib/**/*.rb"],
- plugins = [],
- print_width = DEFAULT_PRINT_WIDTH,
- target_ruby_version = Gem::Version.new(RUBY_VERSION),
- ignore_files = ""
- )
- @name = name
- @source_files = source_files
- @plugins = plugins
- @print_width = print_width
- @target_ruby_version = target_ruby_version
- @ignore_files = ignore_files
-
- yield self if block_given?
- define_task
- end
-
- private
-
- # This method needs to be overridden in the child tasks.
- def command
- raise NotImplementedError
- end
-
- def define_task
- desc "Runs `stree #{command}` over source files"
- task(name) { run_task }
- end
-
- def run_task
- arguments = [command]
- arguments << "--plugins=#{plugins.join(",")}" if plugins.any?
-
- if print_width != DEFAULT_PRINT_WIDTH
- arguments << "--print-width=#{print_width}"
- end
-
- if target_ruby_version != Gem::Version.new(RUBY_VERSION)
- arguments << "--target-ruby-version=#{target_ruby_version}"
- end
-
- arguments << "--ignore-files=#{ignore_files}" if ignore_files != ""
-
- abort if SyntaxTree::CLI.run(arguments + Array(source_files)) != 0
- end
- end
- end
-end
diff --git a/lib/syntax_tree/rake/write_task.rb b/lib/syntax_tree/rake/write_task.rb
deleted file mode 100644
index 8037792e..00000000
--- a/lib/syntax_tree/rake/write_task.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-# frozen_string_literal: true
-
-require_relative "task"
-
-module SyntaxTree
- module Rake
- # A Rake task that runs write on a set of source files.
- #
- # Example:
- #
- # require "syntax_tree/rake/write_task"
- #
- # SyntaxTree::Rake::WriteTask.new do |t|
- # t.source_files = "{app,config,lib}/**/*.rb"
- # end
- #
- # This will create task that can be run with:
- #
- # rake stree:write
- #
- class WriteTask < Task
- private
-
- def command
- "write"
- end
- end
- end
-end
diff --git a/lib/syntax_tree/rake_tasks.rb b/lib/syntax_tree/rake_tasks.rb
deleted file mode 100644
index b53743e5..00000000
--- a/lib/syntax_tree/rake_tasks.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-# frozen_string_literal: true
-
-require_relative "rake/check_task"
-require_relative "rake/write_task"
diff --git a/syntax_tree.gemspec b/syntax_tree.gemspec
index f6c4a734..58f6e292 100644
--- a/syntax_tree.gemspec
+++ b/syntax_tree.gemspec
@@ -8,28 +8,37 @@ Gem::Specification.new do |spec|
spec.authors = ["Kevin Newton"]
spec.email = ["kddnewton@gmail.com"]
- spec.summary = "A parser based on ripper"
- spec.homepage = "https://github.com/kddnewton/syntax_tree"
+ spec.summary = "A Ruby formatter"
+ spec.homepage = "https://github.com/ruby-syntax-tree/syntax_tree"
spec.license = "MIT"
- spec.metadata = { "rubygems_mfa_required" => "true" }
-
- spec.files =
- Dir.chdir(__dir__) do
- `git ls-files -z`.split("\x0")
- .reject { |f| f.match(%r{^(test|spec|features)/}) }
- end
-
- spec.required_ruby_version = ">= 2.7.0"
+ spec.metadata = {
+ "rubygems_mfa_required" => "true",
+ "allowed_push_host" => "https://rubygems.org",
+ "source_code_uri" => spec.homepage,
+ "changelog_uri" => "#{spec.homepage}/blob/main/CHANGELOG.md"
+ }
+
+ spec.files = %w[
+ CHANGELOG.md
+ CODE_OF_CONDUCT.md
+ LICENSE
+ README.md
+ config/rubocop.yml
+ doc/logo.svg
+ exe/stree
+ lib/prism/format.rb
+ lib/syntax_tree.rb
+ lib/syntax_tree/cli.rb
+ lib/syntax_tree/lsp.rb
+ lib/syntax_tree/rake.rb
+ lib/syntax_tree/version.rb
+ syntax_tree.gemspec
+ ]
+
+ spec.required_ruby_version = ">= 3.2.0"
spec.bindir = "exe"
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
spec.require_paths = %w[lib]
-
- spec.add_dependency "prettier_print", ">= 1.2.0"
-
- spec.add_development_dependency "bundler"
- spec.add_development_dependency "minitest"
- spec.add_development_dependency "rake"
- spec.add_development_dependency "rubocop"
- spec.add_development_dependency "simplecov"
+ spec.add_dependency "prism"
end
diff --git a/test/cli_test.rb b/test/cli_test.rb
index a81aa8cb..d9921968 100644
--- a/test/cli_test.rb
+++ b/test/cli_test.rb
@@ -25,27 +25,10 @@ def test_handler
file = Tempfile.new(%w[test- .test])
file.puts("test")
- result = run_cli("ast", contents: file)
- assert_equal("\"test\\n\" + \"test\\n\"\n", result.stdio)
+ result = run_cli("format", contents: file)
+ assert_equal("Formatted test\n", result.stdio)
ensure
- SyntaxTree::HANDLERS.delete(".test")
- end
-
- def test_ast
- result = run_cli("ast")
- assert_includes(result.stdio, "\"test\"")
- end
-
- def test_ast_ignore
- result = run_cli("ast", "--ignore-files='*/test*'")
- assert_equal(0, result.status)
- assert_empty(result.stdio)
- end
-
- def test_ast_syntax_error
- result = run_cli("ast", contents: "foo\n<>\nbar\n")
- assert_includes(result.stderr, "syntax error")
- refute_equal(0, result.status)
+ SyntaxTree.unregister_handler(".test")
end
def test_check
@@ -65,11 +48,6 @@ def test_check_print_width
assert_includes(result.stdio, "match")
end
- def test_check_target_ruby_version
- result = run_cli("check", "--target-ruby-version=2.6.0")
- assert_includes(result.stdio, "match")
- end
-
def test_debug
result = run_cli("debug")
assert_includes(result.stdio, "idempotently")
@@ -86,11 +64,6 @@ def test_debug_non_idempotent_format
end
end
- def test_doc
- result = run_cli("doc")
- assert_includes(result.stdio, "test")
- end
-
def test_format
result = run_cli("format")
assert_equal("test\n", result.stdio)
@@ -117,7 +90,7 @@ def test_write_syntax_tree
def test_write_script
args = ["write", "-e", "1 + 2"]
- stdout, stderr = capture_io { SyntaxTree::CLI.run(args) }
+ stdout, stderr = capture_cli_io { SyntaxTree::CLI.run(args) }
assert_includes stdout, "script"
assert_empty stderr
@@ -128,7 +101,7 @@ def test_write_stdin
$stdin = StringIO.new("1 + 2")
begin
- stdout, stderr = capture_io { SyntaxTree::CLI.run(["write"]) }
+ stdout, stderr = capture_cli_io { SyntaxTree::CLI.run(["write"]) }
assert_includes stdout, "stdin"
assert_empty stderr
@@ -138,13 +111,13 @@ def test_write_stdin
end
def test_help
- stdio, = capture_io { SyntaxTree::CLI.run(["help"]) }
+ stdio, = capture_cli_io { SyntaxTree::CLI.run(["help"]) }
assert_includes(stdio, "stree help")
end
def test_help_default
status = 0
- *, stderr = capture_io { status = SyntaxTree::CLI.run(["foobar"]) }
+ *, stderr = capture_cli_io { status = SyntaxTree::CLI.run(["foobar"]) }
assert_includes(stderr, "stree help")
refute_equal(0, status)
end
@@ -153,42 +126,39 @@ def test_no_arguments
stdin = $stdin
$stdin = StringIO.new("1+1")
- stdio, = capture_io { SyntaxTree::CLI.run(["format"]) }
+ stdio, = capture_cli_io { SyntaxTree::CLI.run(["format"]) }
assert_equal("1 + 1\n", stdio)
ensure
$stdin = stdin
end
def test_inline_script
- stdio, = capture_io { SyntaxTree::CLI.run(%w[format -e 1+1]) }
+ stdio, = capture_cli_io { SyntaxTree::CLI.run(%w[format -e 1+1]) }
assert_equal("1 + 1\n", stdio)
end
def test_multiple_inline_scripts
- stdio, = capture_io { SyntaxTree::CLI.run(%w[format -e 1+1 -e 2+2]) }
+ stdio, = capture_cli_io { SyntaxTree::CLI.run(%w[format -e 1+1 -e 2+2]) }
assert_equal(["1 + 1", "2 + 2"], stdio.split("\n").sort)
end
def test_format_script_with_custom_handler
SyntaxTree.register_handler(".test", TestHandler.new)
- stdio, =
- capture_io do
- SyntaxTree::CLI.run(%w[format --extension=test -e ])
- end
+ stdio, = capture_cli_io { SyntaxTree::CLI.run(%w[format --extension=test -e ]) }
assert_equal("Formatted \n", stdio)
ensure
- SyntaxTree::HANDLERS.delete(".test")
+ SyntaxTree.unregister_handler(".test")
end
def test_format_stdin_with_custom_handler
SyntaxTree.register_handler(".test", TestHandler.new)
stdin = $stdin
$stdin = StringIO.new("")
- stdio, = capture_io { SyntaxTree::CLI.run(%w[format --extension=test]) }
+ stdio, = capture_cli_io { SyntaxTree::CLI.run(%w[format --extension=test]) }
assert_equal("Formatted \n", stdio)
ensure
$stdin = stdin
- SyntaxTree::HANDLERS.delete(".test")
+ SyntaxTree.unregister_handler(".test")
end
def test_generic_error
@@ -208,13 +178,12 @@ def test_plugins
end
end
- def test_language_server
+ def test_lsp
prev_stdin = $stdin
prev_stdout = $stdout
request = { method: "shutdown" }.merge(jsonrpc: "2.0").to_json
- $stdin =
- StringIO.new("Content-Length: #{request.bytesize}\r\n\r\n#{request}")
+ $stdin = StringIO.new("Content-Length: #{request.bytesize}\r\n\r\n#{request}")
$stdout = StringIO.new
assert_equal(0, SyntaxTree::CLI.run(["lsp"]))
@@ -308,9 +277,7 @@ def test_config_file_custom_path_space_separated
end
def test_config_file_nonexistent_path
- assert_raises(ArgumentError) do
- run_cli("format", "--config=/nonexistent/path.streerc")
- end
+ assert_raises(ArgumentError) { run_cli("format", "--config=/nonexistent/path.streerc") }
end
Result = Struct.new(:status, :stdio, :stderr, keyword_init: true)
@@ -332,7 +299,7 @@ def run_cli(command, *args, contents: :default)
status = nil
stdio, stderr =
- capture_io do
+ capture_cli_io do
status =
begin
SyntaxTree::CLI.run([command, *args, tempfile.path])
@@ -347,6 +314,12 @@ def run_cli(command, *args, contents: :default)
tempfile.unlink
end
+ if Process.respond_to?(:fork)
+ alias capture_cli_io capture_subprocess_io
+ else
+ alias capture_cli_io capture_io
+ end
+
def with_config_file(contents, filepath = nil)
filepath ||= File.join(Dir.pwd, SyntaxTree::CLI::ConfigFile::FILENAME)
File.write(filepath, contents)
diff --git a/test/encoded.rb b/test/encoded.rb
deleted file mode 100644
index a67aebf3..00000000
--- a/test/encoded.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-# encoding: Shift_JIS
-# frozen_string_literal: true
diff --git a/test/fixtures/alias.rb b/test/fixtures/alias.rb
index 8962cc32..44dbfc15 100644
--- a/test/fixtures/alias.rb
+++ b/test/fixtures/alias.rb
@@ -13,11 +13,11 @@
%
alias :"foo" :bar
-
-alias :"foo" bar
+alias foo bar
%
alias :foo :"bar"
-
-alias foo :"bar"
+alias foo bar
%
alias foooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo bar
-
@@ -31,3 +31,5 @@
%
alias foo # comment1
bar # comment2
+%
+alias :"foo#{1}" :"bar#{1}"
diff --git a/test/fixtures/altpattern.rb b/test/fixtures/altpattern.rb
new file mode 100644
index 00000000..9c4f4c13
--- /dev/null
+++ b/test/fixtures/altpattern.rb
@@ -0,0 +1,4 @@
+%
+case foo
+in bar | baz
+end
diff --git a/test/fixtures/array_literal.rb b/test/fixtures/array_literal.rb
index 391d2eae..92eddfb9 100644
--- a/test/fixtures/array_literal.rb
+++ b/test/fixtures/array_literal.rb
@@ -87,5 +87,7 @@
[:foo, "bar"]
%
[:foo, :"bar"]
+-
+%i[foo bar]
%
[foo, bar] # comment
diff --git a/test/fixtures/assoc.rb b/test/fixtures/assoc.rb
index 83a4887a..44643060 100644
--- a/test/fixtures/assoc.rb
+++ b/test/fixtures/assoc.rb
@@ -42,8 +42,6 @@
{ foo: }
%
{ "foo": "bar" }
--
-{ foo: "bar" }
%
{ "foo #{bar}": "baz" }
%
diff --git a/test/fixtures/bare_assoc_hash.rb b/test/fixtures/bare_assoc_hash.rb
index d25d0bf4..f76a8863 100644
--- a/test/fixtures/bare_assoc_hash.rb
+++ b/test/fixtures/bare_assoc_hash.rb
@@ -15,7 +15,7 @@
%
foo(bar => bar, "baz": baz)
-
-foo(bar => bar, :"baz" => baz)
+foo(bar => bar, :baz => baz)
%
foo(bar: barrrrrrrrrrrrrrrrrrrrrrrrrrrrrr, baz: bazzzzzzzzzzzzzzzzzzzzzzzzzzzzzz)
-
diff --git a/test/fixtures/def.rb b/test/fixtures/def.rb
index 0cc49e0a..8852f33f 100644
--- a/test/fixtures/def.rb
+++ b/test/fixtures/def.rb
@@ -29,3 +29,9 @@ def foo( # comment
=end
a
end
+%
+def foo
+rescue StandardError
+else
+ensure
+end
diff --git a/test/fixtures/def_endless.rb b/test/fixtures/def_endless.rb
index 8d1f9d33..d1b4a63a 100644
--- a/test/fixtures/def_endless.rb
+++ b/test/fixtures/def_endless.rb
@@ -28,7 +28,7 @@ def a()
=end
=1
-
-def a() =
+def a()
=begin
=end
- 1
+= 1
diff --git a/test/fixtures/dyna_symbol.rb b/test/fixtures/dyna_symbol.rb
index 7ac74a31..103e444f 100644
--- a/test/fixtures/dyna_symbol.rb
+++ b/test/fixtures/dyna_symbol.rb
@@ -1,7 +1,5 @@
%
:'foo'
--
-:"foo"
%
:"foo"
%
@@ -10,8 +8,6 @@
:"foo #{bar}"
%
%s[foo #{bar}]
--
-:'foo #{bar}'
%
{ %s[foo] => bar }
-
diff --git a/test/fixtures/hash.rb b/test/fixtures/hash.rb
index 70e89f69..e3e046a6 100644
--- a/test/fixtures/hash.rb
+++ b/test/fixtures/hash.rb
@@ -17,7 +17,7 @@
%
{ bar => bar, "baz": baz }
-
-{ bar => bar, :"baz" => baz }
+{ bar => bar, :baz => baz }
%
{ bar: barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr, baz: bazzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz }
-
diff --git a/test/fixtures/hshptn.rb b/test/fixtures/hshptn.rb
index 02d1cf75..455c48a5 100644
--- a/test/fixtures/hshptn.rb
+++ b/test/fixtures/hshptn.rb
@@ -2,14 +2,26 @@
case foo
in ** then
end
+-
+case foo
+in { ** }
+end
%
case foo
in bar:
end
+-
+case foo
+in { bar: }
+end
%
case foo
in bar: bar
end
+-
+case foo
+in { bar: bar }
+end
%
case foo
in bar:, baz:
@@ -30,6 +42,10 @@
case foo
in **bar
end
+-
+case foo
+in { **bar }
+end
% # >= 2.7.3
case foo
in {
@@ -74,6 +90,10 @@
case foo
in **nil
end
+-
+case foo
+in { **nil }
+end
%
case foo
in bar, { baz:, **nil }
@@ -82,5 +102,5 @@
-
case foo
in [bar, { baz:, **nil }]
-in qux:
+in { qux: }
end
diff --git a/test/fixtures/if.rb b/test/fixtures/if.rb
index b25386b9..0fd39bc0 100644
--- a/test/fixtures/if.rb
+++ b/test/fixtures/if.rb
@@ -47,8 +47,6 @@
else
c
end
--
-not(a) ? b : c
%
(if foo then bar else baz end)
-
diff --git a/test/fixtures/label.rb b/test/fixtures/label.rb
index 14de6874..8921e437 100644
--- a/test/fixtures/label.rb
+++ b/test/fixtures/label.rb
@@ -4,3 +4,7 @@
case foo
in bar:
end
+-
+case foo
+in { bar: }
+end
diff --git a/test/fixtures/lambda.rb b/test/fixtures/lambda.rb
index 8b922ef0..d8ea860d 100644
--- a/test/fixtures/lambda.rb
+++ b/test/fixtures/lambda.rb
@@ -68,14 +68,6 @@
->(; a, b) {}
%
->(a = (b; c)) {}
--
-->(
- a = (
- b
- c
- )
-) do
-end
%
-> do # comment1
# comment2
diff --git a/test/fixtures/mlhs_paren.rb b/test/fixtures/mlhs_paren.rb
index 3a3b777b..25871c39 100644
--- a/test/fixtures/mlhs_paren.rb
+++ b/test/fixtures/mlhs_paren.rb
@@ -1,7 +1,5 @@
%
(foo, bar) = baz
--
-foo, bar = baz
%
foo, (bar, baz) = baz
%
@@ -10,5 +8,3 @@
foo, (bar, baz,) = baz
%
((foo,)) = bar
--
-foo, = bar
diff --git a/test/fixtures/not.rb b/test/fixtures/not.rb
index 6c0451ae..1f3615d6 100644
--- a/test/fixtures/not.rb
+++ b/test/fixtures/not.rb
@@ -1,5 +1,7 @@
%
not()
+-
+not ()
%
not ()
%
@@ -14,8 +16,6 @@
else
baz
end
--
-foo ? not(bar) : baz
%
if foooooooooooooooooooooooooooooooooooooooooo
not bar
diff --git a/test/fixtures/string_concat.rb b/test/fixtures/string_concat.rb
index 7bb55baf..ea725aa8 100644
--- a/test/fixtures/string_concat.rb
+++ b/test/fixtures/string_concat.rb
@@ -2,3 +2,5 @@
"foo" \
"bar" \
"baz"
+-
+"foo" "bar" "baz"
diff --git a/test/fixtures/string_embexpr.rb b/test/fixtures/string_embexpr.rb
index fd5e8cfc..3ec3421b 100644
--- a/test/fixtures/string_embexpr.rb
+++ b/test/fixtures/string_embexpr.rb
@@ -10,3 +10,11 @@
"#{foo; bar}"
%
"#{if foo; foooooooooooooooooooooooooooooooooooooo; else; barrrrrrrrrrrrrrrr; end}"
+-
+"#{
+ if foo
+ foooooooooooooooooooooooooooooooooooooo
+ else
+ barrrrrrrrrrrrrrrr
+ end
+}"
diff --git a/test/fixtures/undef.rb b/test/fixtures/undef.rb
index 73986b97..446a7c9c 100644
--- a/test/fixtures/undef.rb
+++ b/test/fixtures/undef.rb
@@ -23,3 +23,5 @@
undef foo, bar # comment
%
undef :"foo", :"bar"
+-
+undef foo, bar
diff --git a/test/fixtures_test.rb b/test/fixtures_test.rb
new file mode 100644
index 00000000..bb23a9a0
--- /dev/null
+++ b/test/fixtures_test.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require_relative "test_helper"
+
+# There are a bunch of fixtures defined in test/fixtures. They exercise every
+# possible combination of syntax that leads to variations in the types of nodes.
+# They are used for testing various parts of Syntax Tree, including formatting,
+# serialization, and parsing. This module provides a single each_fixture method
+# that can be used to drive tests on each fixture.
+module Fixtures
+ FIXTURES_3_0_0 = %w[command_def_endless def_endless fndptn rassign rassign_rocket].freeze
+
+ FIXTURES_3_1_0 = %w[pinned_begin var_field_rassign].freeze
+
+ Fixture = Struct.new(:name, :source, :formatted, keyword_init: true)
+
+ def self.each_fixture
+ ruby_version = Gem::Version.new(RUBY_VERSION)
+
+ # First, get a list of the basenames of all of the fixture files.
+ fixtures =
+ Dir[File.expand_path("fixtures/*.rb", __dir__)].map do |filepath|
+ File.basename(filepath, ".rb")
+ end
+
+ # Next, subtract out any fixtures that aren't supported by the current Ruby
+ # version.
+ fixtures -= FIXTURES_3_1_0 if ruby_version < Gem::Version.new("3.1.0")
+ fixtures -= FIXTURES_3_0_0 if ruby_version < Gem::Version.new("3.0.0")
+
+ delimiter = /%(?: # (.+?))?\n/
+ fixtures.each do |fixture|
+ filepath = File.expand_path("fixtures/#{fixture}.rb", __dir__)
+
+ # For each fixture in the fixture file yield a Fixture object.
+ File
+ .readlines(filepath)
+ .slice_before(delimiter)
+ .each_with_index do |source, index|
+ comment = source.shift.match(delimiter)[1]
+ source, formatted = source.join.split("-\n")
+
+ # If there's a comment starting with >= that starts after the % that
+ # delineates the test, then we're going to check if the version
+ # satisfies that constraint.
+ next if comment&.start_with?(">=") && ruby_version < Gem::Version.new(comment.split[1])
+
+ name = :"#{fixture}_#{index}"
+ yield(Fixture.new(name: name, source: source, formatted: formatted || source))
+ end
+ end
+ end
+end
+
+module SyntaxTree
+ class FixturesTest < Minitest::Test
+ Fixtures.each_fixture do |fixture|
+ define_method(:"test_formatted_#{fixture.name}") do
+ options = SyntaxTree.options(print_width: 80)
+ assert_equal(fixture.formatted, SyntaxTree.format(fixture.source, options))
+ end
+ end
+ end
+end
diff --git a/test/formatting_test.rb b/test/formatting_test.rb
deleted file mode 100644
index 5e5f9e9f..00000000
--- a/test/formatting_test.rb
+++ /dev/null
@@ -1,64 +0,0 @@
-# frozen_string_literal: true
-
-require_relative "test_helper"
-
-module SyntaxTree
- class FormattingTest < Minitest::Test
- Fixtures.each_fixture do |fixture|
- define_method(:"test_formatted_#{fixture.name}") do
- assert_equal(fixture.formatted, SyntaxTree.format(fixture.source))
- assert_syntax_tree(SyntaxTree.parse(fixture.source))
- end
- end
-
- def test_format_class_level
- source = "1+1"
-
- assert_equal(
- "1 + 1\n",
- Formatter.format(source, SyntaxTree.parse(source))
- )
- end
-
- def test_stree_ignore
- source = <<~SOURCE
- # stree-ignore
- 1+1
- SOURCE
-
- assert_equal(source, SyntaxTree.format(source))
- end
-
- def test_formatting_with_different_indentation_level
- source = <<~SOURCE
- def foo
- puts "a"
- end
- SOURCE
-
- # Default indentation
- assert_equal(source, SyntaxTree.format(source))
-
- # Level 2
- assert_equal(<<-EXPECTED.chomp, SyntaxTree.format(source, 80, 2).rstrip)
- def foo
- puts "a"
- end
- EXPECTED
-
- # Level 4
- assert_equal(<<-EXPECTED.chomp, SyntaxTree.format(source, 80, 4).rstrip)
- def foo
- puts "a"
- end
- EXPECTED
-
- # Level 6
- assert_equal(<<-EXPECTED.chomp, SyntaxTree.format(source, 80, 6).rstrip)
- def foo
- puts "a"
- end
- EXPECTED
- end
- end
-end
diff --git a/test/idempotency_test.rb b/test/idempotency_test.rb
index 32d9d196..c2c7ba6f 100644
--- a/test/idempotency_test.rb
+++ b/test/idempotency_test.rb
@@ -1,14 +1,14 @@
# frozen_string_literal: true
return if !ENV["CI"] || RUBY_ENGINE == "truffleruby"
+
require_relative "test_helper"
module SyntaxTree
class IdempotencyTest < Minitest::Test
Dir[File.join(RbConfig::CONFIG["libdir"], "**/*.rb")].each do |filepath|
define_method(:"test_#{filepath}") do
- source = SyntaxTree.read(filepath)
- formatted = SyntaxTree.format(source)
+ formatted = SyntaxTree.format_file(filepath)
assert_equal(
formatted,
diff --git a/test/language_server_test.rb b/test/language_server_test.rb
deleted file mode 100644
index 58f8bc6a..00000000
--- a/test/language_server_test.rb
+++ /dev/null
@@ -1,261 +0,0 @@
-# frozen_string_literal: true
-
-require_relative "test_helper"
-require "syntax_tree/language_server"
-
-module SyntaxTree
- # stree-ignore
- class LanguageServerTest < Minitest::Test
- class Initialize
- attr_reader :id
-
- def initialize(id)
- @id = id
- end
-
- def to_hash
- { method: "initialize", id: id }
- end
- end
-
- class Shutdown
- attr_reader :id
-
- def initialize(id)
- @id = id
- end
-
- def to_hash
- { method: "shutdown", id: id }
- end
- end
-
- class TextDocumentDidOpen
- attr_reader :uri, :text
-
- def initialize(uri, text)
- @uri = uri
- @text = text
- end
-
- def to_hash
- {
- method: "textDocument/didOpen",
- params: { textDocument: { uri: uri, text: text } }
- }
- end
- end
-
- class TextDocumentDidChange
- attr_reader :uri, :text
-
- def initialize(uri, text)
- @uri = uri
- @text = text
- end
-
- def to_hash
- {
- method: "textDocument/didChange",
- params: {
- textDocument: { uri: uri },
- contentChanges: [{ text: text }]
- }
- }
- end
- end
-
- class TextDocumentDidClose
- attr_reader :uri
-
- def initialize(uri)
- @uri = uri
- end
-
- def to_hash
- {
- method: "textDocument/didClose",
- params: { textDocument: { uri: uri } }
- }
- end
- end
-
- class TextDocumentFormatting
- attr_reader :id, :uri
-
- def initialize(id, uri)
- @id = id
- @uri = uri
- end
-
- def to_hash
- {
- method: "textDocument/formatting",
- id: id,
- params: { textDocument: { uri: uri } }
- }
- end
- end
-
- def test_formatting
- responses = run_server([
- Initialize.new(1),
- TextDocumentDidOpen.new("file:///path/to/file.rb", "class Foo; end"),
- TextDocumentDidChange.new("file:///path/to/file.rb", "class Bar; end"),
- TextDocumentFormatting.new(2, "file:///path/to/file.rb"),
- TextDocumentDidClose.new("file:///path/to/file.rb"),
- Shutdown.new(3)
- ])
-
- shape = LanguageServer::Request[[
- { id: 1, result: { capabilities: Hash } },
- { id: 2, result: [{ newText: :any }] },
- { id: 3, result: {} }
- ]]
-
- assert_operator(shape, :===, responses)
- assert_equal("class Bar\nend\n", responses.dig(1, :result, 0, :newText))
- end
-
- def test_formatting_ignore
- responses = run_server([
- Initialize.new(1),
- TextDocumentDidOpen.new("file:///path/to/file.rb", "class Foo; end"),
- TextDocumentFormatting.new(2, "file:///path/to/file.rb"),
- Shutdown.new(3)
- ], ignore_files: ["path/**/*.rb"])
-
- shape = LanguageServer::Request[[
- { id: 1, result: { capabilities: Hash } },
- { id: 2, result: :any },
- { id: 3, result: {} }
- ]]
-
- assert_operator(shape, :===, responses)
- assert_nil(responses.dig(1, :result))
- end
-
- def test_formatting_failure
- responses = run_server([
- Initialize.new(1),
- TextDocumentDidOpen.new("file:///path/to/file.rb", "<>"),
- TextDocumentFormatting.new(2, "file:///path/to/file.rb"),
- Shutdown.new(3)
- ])
-
- shape = LanguageServer::Request[[
- { id: 1, result: { capabilities: Hash } },
- { id: 2, result: :any },
- { id: 3, result: {} }
- ]]
-
- assert_operator(shape, :===, responses)
- assert_nil(responses.dig(1, :result))
- end
-
- def test_formatting_print_width
- contents = "#{"a" * 40} + #{"b" * 40}\n"
- responses = run_server([
- Initialize.new(1),
- TextDocumentDidOpen.new("file:///path/to/file.rb", contents),
- TextDocumentFormatting.new(2, "file:///path/to/file.rb"),
- TextDocumentDidClose.new("file:///path/to/file.rb"),
- Shutdown.new(3)
- ], print_width: 100)
-
- shape = LanguageServer::Request[[
- { id: 1, result: { capabilities: Hash } },
- { id: 2, result: [{ newText: :any }] },
- { id: 3, result: {} }
- ]]
-
- assert_operator(shape, :===, responses)
- assert_equal(contents, responses.dig(1, :result, 0, :newText))
- end
-
- def test_reading_file
- Tempfile.open(%w[test- .rb]) do |file|
- file.write("class Foo; end")
- file.rewind
-
- responses = run_server([
- Initialize.new(1),
- TextDocumentFormatting.new(2, "file://#{file.path}"),
- Shutdown.new(3)
- ])
-
- shape = LanguageServer::Request[[
- { id: 1, result: { capabilities: Hash } },
- { id: 2, result: [{ newText: :any }] },
- { id: 3, result: {} }
- ]]
-
- assert_operator(shape, :===, responses)
- assert_equal("class Foo\nend\n", responses.dig(1, :result, 0, :newText))
- end
- end
-
- def test_bogus_request
- assert_raises(ArgumentError) do
- run_server([{ method: "textDocument/bogus" }])
- end
- end
-
- def test_clean_shutdown
- responses = run_server([Initialize.new(1), Shutdown.new(2)])
-
- shape = LanguageServer::Request[[
- { id: 1, result: { capabilities: Hash } },
- { id: 2, result: {} }
- ]]
-
- assert_operator(shape, :===, responses)
- end
-
- def test_file_that_does_not_exist
- responses = run_server([
- Initialize.new(1),
- TextDocumentFormatting.new(2, "file:///path/to/file.rb"),
- Shutdown.new(3)
- ])
-
- shape = LanguageServer::Request[[
- { id: 1, result: { capabilities: Hash } },
- { id: 2, result: :any },
- { id: 3, result: {} }
- ]]
-
- assert_operator(shape, :===, responses)
- end
-
- private
-
- def write(content)
- request = content.to_hash.merge(jsonrpc: "2.0").to_json
- "Content-Length: #{request.bytesize}\r\n\r\n#{request}"
- end
-
- def read(content)
- [].tap do |messages|
- while (headers = content.gets("\r\n\r\n"))
- source = content.read(headers[/Content-Length: (\d+)/i, 1].to_i)
- messages << JSON.parse(source, symbolize_names: true)
- end
- end
- end
-
- def run_server(messages, print_width: DEFAULT_PRINT_WIDTH, ignore_files: [])
- input = StringIO.new(messages.map { |message| write(message) }.join)
- output = StringIO.new
-
- LanguageServer.new(
- input: input,
- output: output,
- print_width: print_width,
- ignore_files: ignore_files
- ).run
-
- read(output.tap(&:rewind))
- end
- end
-end
diff --git a/test/location_test.rb b/test/location_test.rb
deleted file mode 100644
index 26831fb1..00000000
--- a/test/location_test.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-# frozen_string_literal: true
-
-require_relative "test_helper"
-
-module SyntaxTree
- class LocationTest < Minitest::Test
- def test_lines
- location = Location.fixed(line: 1, char: 0, column: 0)
- location = location.to(Location.fixed(line: 3, char: 3, column: 3))
-
- assert_equal(1..3, location.lines)
- end
-
- def test_deconstruct
- location = Location.fixed(line: 1, char: 0, column: 0)
-
- assert_equal(1, location.start_line)
- assert_equal(0, location.start_char)
- assert_equal(0, location.start_column)
- end
-
- def test_deconstruct_keys
- location = Location.fixed(line: 1, char: 0, column: 0)
-
- assert_equal(1, location.start_line)
- end
- end
-end
diff --git a/test/lsp_test.rb b/test/lsp_test.rb
new file mode 100644
index 00000000..29d09de1
--- /dev/null
+++ b/test/lsp_test.rb
@@ -0,0 +1,214 @@
+# frozen_string_literal: true
+
+require_relative "test_helper"
+require "json"
+
+module SyntaxTree
+ # stree-ignore
+ class LSPTest < Minitest::Test
+ def test_formatting
+ responses = run_server([
+ request_initialize(1),
+ request_open("file:///path/to/file.rb", "class Foo; end"),
+ request_change("file:///path/to/file.rb", "class Bar; end"),
+ request_formatting(2, "file:///path/to/file.rb"),
+ request_close("file:///path/to/file.rb"),
+ request_shutdown(3)
+ ])
+
+ new_text = nil
+ assert_pattern do
+ responses => [
+ { id: 1, result: { capabilities: Hash } },
+ { id: 2, result: [{ newText: new_text }] },
+ { id: 3, result: {} }
+ ]
+ end
+
+ assert_equal("class Bar\nend\n", new_text)
+ end
+
+ def test_formatting_ignore
+ responses = run_server([
+ request_initialize(1),
+ request_open("file:///path/to/file.rb", "class Foo; end"),
+ request_formatting(2, "file:///path/to/file.rb"),
+ request_shutdown(3)
+ ], ignore_files: ["path/**/*.rb"])
+
+ result = nil
+ assert_pattern do
+ responses => [
+ { id: 1, result: { capabilities: Hash } },
+ { id: 2, result: },
+ { id: 3, result: {} }
+ ]
+ end
+
+ assert_nil(result)
+ end
+
+ def test_formatting_failure
+ responses = run_server([
+ request_initialize(1),
+ request_open("file:///path/to/file.rb", "<>"),
+ request_formatting(2, "file:///path/to/file.rb"),
+ request_shutdown(3)
+ ])
+
+ result = nil
+ assert_pattern do
+ responses => [
+ { id: 1, result: { capabilities: Hash } },
+ { id: 2, result: },
+ { id: 3, result: {} }
+ ]
+ end
+
+ assert_nil(result)
+ end
+
+ def test_formatting_print_width
+ contents = "#{"a" * 40} + #{"b" * 40}\n"
+ responses = run_server([
+ request_initialize(1),
+ request_open("file:///path/to/file.rb", contents),
+ request_formatting(2, "file:///path/to/file.rb"),
+ request_close("file:///path/to/file.rb"),
+ request_shutdown(3)
+ ], print_width: 100)
+
+ new_text = nil
+ assert_pattern do
+ responses => [
+ { id: 1, result: { capabilities: Hash } },
+ { id: 2, result: [{ newText: new_text }] },
+ { id: 3, result: {} }
+ ]
+ end
+
+ assert_equal(contents, new_text)
+ end
+
+ def test_reading_file
+ Tempfile.open(%w[test- .rb]) do |file|
+ file.write("class Foo; end")
+ file.rewind
+
+ responses = run_server([
+ request_initialize(1),
+ request_formatting(2, "file://#{file.path}"),
+ request_shutdown(3)
+ ])
+
+ new_text = nil
+ assert_pattern do
+ responses => [
+ { id: 1, result: { capabilities: Hash } },
+ { id: 2, result: [{ newText: new_text }] },
+ { id: 3, result: {} }
+ ]
+ end
+
+ assert_equal("class Foo\nend\n", new_text)
+ end
+ end
+
+ def test_bogus_request
+ assert_raises(ArgumentError) do
+ run_server([{ method: "textDocument/bogus" }])
+ end
+ end
+
+ def test_clean_shutdown
+ responses = run_server([request_initialize(1), request_shutdown(2)])
+
+ assert_pattern do
+ responses => [
+ { id: 1, result: { capabilities: Hash } },
+ { id: 2, result: {} }
+ ]
+ end
+ end
+
+ def test_file_that_does_not_exist
+ responses = run_server([
+ request_initialize(1),
+ request_formatting(2, "file:///path/to/file.rb"),
+ request_shutdown(3)
+ ])
+
+ assert_pattern do
+ responses => [
+ { id: 1, result: { capabilities: Hash } },
+ { id: 2, result: _ },
+ { id: 3, result: {} }
+ ]
+ end
+ end
+
+ private
+
+ def request_initialize(id)
+ { method: "initialize", id: id }
+ end
+
+ def request_shutdown(id)
+ { method: "shutdown", id: id }
+ end
+
+ def request_open(uri, text)
+ {
+ method: "textDocument/didOpen",
+ params: { textDocument: { uri: uri, text: text } }
+ }
+ end
+
+ def request_change(uri, text)
+ {
+ method: "textDocument/didChange",
+ params: {
+ textDocument: { uri: uri },
+ contentChanges: [{ text: text }]
+ }
+ }
+ end
+
+ def request_close(uri)
+ {
+ method: "textDocument/didClose",
+ params: { textDocument: { uri: uri } }
+ }
+ end
+
+ def request_formatting(id, uri)
+ {
+ method: "textDocument/formatting",
+ id: id,
+ params: { textDocument: { uri: uri } }
+ }
+ end
+
+ def run_server(messages, print_width: :default, ignore_files: [])
+ input = StringIO.new
+ output = StringIO.new
+
+ messages.each do |message|
+ request = JSON.generate(message.merge(jsonrpc: "2.0"))
+ input.write("Content-Length: #{request.bytesize}\r\n\r\n#{request}")
+ end
+
+ input.rewind
+ options = SyntaxTree.options(print_width: print_width)
+ LSP.new(input, output, options: options, ignore_files: ignore_files).run
+ output.rewind
+
+ results = []
+ while (headers = output.gets("\r\n\r\n"))
+ body = output.read(Integer(headers[/Content-Length: (\d+)/i, 1]))
+ results << JSON.parse(body, symbolize_names: true)
+ end
+ results
+ end
+ end
+end
diff --git a/test/node_test.rb b/test/node_test.rb
deleted file mode 100644
index 41f6271d..00000000
--- a/test/node_test.rb
+++ /dev/null
@@ -1,1450 +0,0 @@
-# frozen_string_literal: true
-
-require_relative "test_helper"
-
-module SyntaxTree
- class NodeTest < Minitest::Test
- def self.guard_version(version)
- yield if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new(version)
- end
-
- def test_BEGIN
- assert_node(BEGINBlock, "BEGIN {}")
- end
-
- def test_CHAR
- assert_node(CHAR, "?a")
- end
-
- def test_END
- assert_node(ENDBlock, "END {}")
- end
-
- def test___end__
- source = <<~SOURCE
- a + 1
- __END__
- content
- SOURCE
-
- at = location(lines: 2..2, chars: 6..14)
- assert_node(EndContent, source, at: at)
- end
-
- def test_alias
- assert_node(AliasNode, "alias left right")
- end
-
- def test_aref
- assert_node(ARef, "collection[index]")
- end
-
- def test_aref_field
- source = "collection[index] = value"
-
- at = location(chars: 0..17)
- assert_node(ARefField, source, at: at, &:target)
- end
-
- def test_arg_paren
- source = "method(argument)"
-
- at = location(chars: 6..16)
- assert_node(ArgParen, source, at: at, &:arguments)
- end
-
- def test_arg_paren_heredoc
- source = <<~SOURCE
- method(<<~ARGUMENT)
- value
- ARGUMENT
- SOURCE
-
- at = location(lines: 1..3, chars: 6..37)
- assert_node(ArgParen, source, at: at, &:arguments)
- end
-
- def test_args
- source = "method(first, second, third)"
-
- at = location(chars: 7..27)
- assert_node(Args, source, at: at) { |node| node.arguments.arguments }
- end
-
- def test_arg_block
- source = "method(argument, &block)"
-
- at = location(chars: 17..23)
- assert_node(ArgBlock, source, at: at) do |node|
- node.arguments.arguments.parts[1]
- end
- end
-
- guard_version("3.1.0") do
- def test_arg_block_anonymous
- source = <<~SOURCE
- def method(&)
- child_method(&)
- end
- SOURCE
-
- at = location(lines: 2..2, chars: 29..30)
- assert_node(ArgBlock, source, at: at) do |node|
- node.bodystmt.statements.body.first.arguments.arguments.parts[0]
- end
- end
- end
-
- def test_arg_star
- source = "method(prefix, *arguments, suffix)"
-
- at = location(chars: 15..25)
- assert_node(ArgStar, source, at: at) do |node|
- node.arguments.arguments.parts[1]
- end
- end
-
- guard_version("2.7.3") do
- def test_args_forward
- source = <<~SOURCE
- def get(...)
- request(:GET, ...)
- end
- SOURCE
-
- at = location(lines: 2..2, chars: 29..32)
- assert_node(ArgsForward, source, at: at) do |node|
- node.bodystmt.statements.body.first.arguments.arguments.parts.last
- end
- end
- end
-
- def test_array
- assert_node(ArrayLiteral, "[1]")
- end
-
- def test_aryptn
- source = <<~SOURCE
- case [1, 2, 3]
- in Container[Integer, *, Integer]
- 'matched'
- end
- SOURCE
-
- at = location(lines: 2..2, chars: 18..48)
- assert_node(AryPtn, source, at: at) { |node| node.consequent.pattern }
- end
-
- def test_assign
- assert_node(Assign, "variable = value")
- end
-
- def test_assoc
- source = "{ key1: value1, key2: value2 }"
-
- at = location(chars: 2..14)
- assert_node(Assoc, source, at: at) { |node| node.assocs.first }
- end
-
- guard_version("3.1.0") do
- def test_assoc_no_value
- source = "{ key1:, key2: }"
-
- at = location(chars: 2..7)
- assert_node(Assoc, source, at: at) { |node| node.assocs.first }
- end
- end
-
- def test_assoc_splat
- source = "{ **pairs }"
-
- at = location(chars: 2..9)
- assert_node(AssocSplat, source, at: at) { |node| node.assocs.first }
- end
-
- def test_backref
- assert_node(Backref, "$1")
- end
-
- def test_backtick
- at = location(chars: 4..5)
- assert_node(Backtick, "def `() end", at: at, &:name)
- end
-
- def test_bare_assoc_hash
- source = "method(key1: value1, key2: value2)"
-
- at = location(chars: 7..33)
- assert_node(BareAssocHash, source, at: at) do |node|
- node.arguments.arguments.parts.first
- end
- end
-
- guard_version("3.1.0") do
- def test_pinned_begin
- source = <<~SOURCE
- case value
- in ^(expression)
- end
- SOURCE
-
- at = location(lines: 2..2, chars: 14..27, columns: 3..16)
- assert_node(PinnedBegin, source, at: at) do |node|
- node.consequent.pattern
- end
- end
- end
-
- def test_begin
- source = <<~SOURCE
- begin
- value
- end
- SOURCE
-
- assert_node(Begin, source)
- end
-
- def test_begin_clauses
- source = <<~SOURCE
- begin
- begun
- rescue
- rescued
- else
- elsed
- ensure
- ensured
- end
- SOURCE
-
- assert_node(Begin, source)
- end
-
- def test_binary
- assert_node(Binary, "collection << value")
- end
-
- def test_block_var
- source = <<~SOURCE
- method do |positional, optional = value, keyword:, █ local|
- end
- SOURCE
-
- at = location(chars: 10..65)
- assert_node(BlockVar, source, at: at) { |node| node.block.block_var }
- end
-
- def test_blockarg
- source = "def method(&block); end"
-
- at = location(chars: 11..17)
- assert_node(BlockArg, source, at: at) do |node|
- node.params.contents.block
- end
- end
-
- guard_version("3.1.0") do
- def test_blockarg_anonymous
- source = "def method(&); end"
-
- at = location(chars: 11..12)
- assert_node(BlockArg, source, at: at) do |node|
- node.params.contents.block
- end
- end
- end
-
- def test_bodystmt
- source = <<~SOURCE
- begin
- begun
- rescue
- rescued
- else
- elsed
- ensure
- ensured
- end
- SOURCE
-
- at = location(lines: 2..9, chars: 5..64)
- assert_node(BodyStmt, source, at: at, &:bodystmt)
- end
-
- def test_brace_block
- source = "method { |variable| variable + 1 }"
-
- at = location(chars: 7..34)
- assert_node(BlockNode, source, at: at, &:block)
- end
-
- def test_break
- at = location(chars: 6..17)
- assert_node(Break, "tap { break value }", at: at) do |node|
- node.block.bodystmt.body.first
- end
- end
-
- def test_call
- assert_node(CallNode, "receiver.message")
- end
-
- def test_case
- source = <<~SOURCE
- case value
- when 1
- "one"
- end
- SOURCE
-
- assert_node(Case, source)
- end
-
- guard_version("3.0.0") do
- def test_rassign_in
- assert_node(RAssign, "value in pattern")
- end
-
- def test_rassign_rocket
- assert_node(RAssign, "value => pattern")
- end
- end
-
- def test_class
- assert_node(ClassDeclaration, "class Child < Parent; end")
- end
-
- def test_command
- assert_node(Command, "method argument")
- end
-
- def test_command_call
- assert_node(CommandCall, "object.method argument")
- end
-
- def test_comment
- assert_node(Comment, "# comment", at: location(chars: 0..8))
- end
-
- # This test is to ensure that comments get parsed and printed properly in
- # all of the visitors. We do this by checking against a node that we're sure
- # will have comments attached to it in order to exercise all of the various
- # comments methods on the visitors.
- def test_comment_attached
- source = <<~SOURCE
- def method # comment
- end
- SOURCE
-
- at = location(chars: 10..10)
- assert_node(Params, source, at: at, &:params)
- end
-
- def test_const
- assert_node(Const, "Constant", &:value)
- end
-
- def test_const_path_field
- source = "object::Const = value"
-
- at = location(chars: 0..13)
- assert_node(ConstPathField, source, at: at, &:target)
- end
-
- def test_const_path_ref
- assert_node(ConstPathRef, "object::Const")
- end
-
- def test_const_ref
- source = "class Container; end"
-
- at = location(chars: 6..15)
- assert_node(ConstRef, source, at: at, &:constant)
- end
-
- def test_cvar
- assert_node(CVar, "@@variable", &:value)
- end
-
- def test_def
- assert_node(DefNode, "def method(param) result end")
- end
-
- def test_def_paramless
- source = <<~SOURCE
- def method
- end
- SOURCE
-
- assert_node(DefNode, source)
- end
-
- guard_version("3.0.0") do
- def test_def_endless
- assert_node(DefNode, "def method = result")
- end
- end
-
- guard_version("3.1.0") do
- def test_def_endless_command
- assert_node(DefNode, "def method = result argument")
- end
- end
-
- def test_defined
- assert_node(Defined, "defined?(variable)")
- end
-
- def test_defs
- assert_node(DefNode, "def object.method(param) result end")
- end
-
- def test_defs_paramless
- source = <<~SOURCE
- def object.method
- end
- SOURCE
-
- assert_node(DefNode, source)
- end
-
- def test_do_block
- source = "method do |variable| variable + 1 end"
-
- at = location(chars: 7..37)
- assert_node(BlockNode, source, at: at, &:block)
- end
-
- def test_dot2
- assert_node(RangeNode, "1..3")
- end
-
- def test_dot3
- assert_node(RangeNode, "1...3")
- end
-
- def test_dyna_symbol
- assert_node(DynaSymbol, ':"#{variable}"')
- end
-
- def test_dyna_symbol_hash_key
- source = '{ "#{key}": value }'
-
- at = location(chars: 2..11)
- assert_node(DynaSymbol, source, at: at) { |node| node.assocs.first.key }
- end
-
- def test_else
- source = <<~SOURCE
- if value
- else
- end
- SOURCE
-
- at = location(lines: 2..3, chars: 9..17)
- assert_node(Else, source, at: at, &:consequent)
- end
-
- def test_elsif
- source = <<~SOURCE
- if first
- elsif second
- else
- end
- SOURCE
-
- at = location(lines: 2..4, chars: 9..30)
- assert_node(Elsif, source, at: at, &:consequent)
- end
-
- def test_embdoc
- source = <<~SOURCE
- =begin
- first line
- second line
- =end
- SOURCE
-
- assert_node(EmbDoc, source)
- end
-
- def test_ensure
- source = <<~SOURCE
- begin
- ensure
- end
- SOURCE
-
- at = location(lines: 2..3, chars: 6..16)
- assert_node(Ensure, source, at: at) { |node| node.bodystmt.ensure_clause }
- end
-
- def test_excessed_comma
- source = "proc { |x,| }"
-
- at = location(chars: 9..10)
- assert_node(ExcessedComma, source, at: at) do |node|
- node.block.block_var.params.rest
- end
- end
-
- def test_fcall
- assert_node(CallNode, "method(argument)")
- end
-
- def test_field
- source = "object.variable = value"
-
- at = location(chars: 0..15)
- assert_node(Field, source, at: at, &:target)
- end
-
- def test_float_literal
- assert_node(FloatLiteral, "1.0")
- end
-
- guard_version("3.0.0") do
- def test_fndptn
- source = <<~SOURCE
- case value
- in Container[*, 7, *]
- end
- SOURCE
-
- at = location(lines: 2..2, chars: 14..32)
- assert_node(FndPtn, source, at: at) { |node| node.consequent.pattern }
- end
- end
-
- def test_for
- assert_node(For, "for value in list do end")
- end
-
- def test_gvar
- assert_node(GVar, "$variable", &:value)
- end
-
- def test_hash
- assert_node(HashLiteral, "{ key => value }")
- end
-
- def test_heredoc
- source = <<~SOURCE
- <<~HEREDOC
- contents
- HEREDOC
- SOURCE
-
- at = location(lines: 1..3, chars: 0..30)
- assert_node(Heredoc, source, at: at)
- end
-
- def test_heredoc_beg
- source = <<~SOURCE
- <<~HEREDOC
- contents
- HEREDOC
- SOURCE
-
- at = location(chars: 0..10)
- assert_node(HeredocBeg, source, at: at, &:beginning)
- end
-
- def test_heredoc_end
- source = <<~SOURCE
- <<~HEREDOC
- contents
- HEREDOC
- SOURCE
-
- at = location(lines: 3..3, chars: 22..30, columns: 0..8)
- assert_node(HeredocEnd, source, at: at, &:ending)
- end
-
- def test_hshptn
- source = <<~SOURCE
- case value
- in Container[key:, **keys]
- end
- SOURCE
-
- at = location(lines: 2..2, chars: 14..36)
- assert_node(HshPtn, source, at: at) { |node| node.consequent.pattern }
- end
-
- def test_ident
- assert_node(Ident, "value", &:value)
- end
-
- def test_if
- assert_node(IfNode, "if value then else end")
- end
-
- def test_if_op
- assert_node(IfOp, "value ? true : false")
- end
-
- def test_if_mod
- assert_node(IfNode, "expression if predicate")
- end
-
- def test_imaginary
- assert_node(Imaginary, "1i")
- end
-
- def test_in
- source = <<~SOURCE
- case value
- in first
- in second
- end
- SOURCE
-
- at = location(lines: 2..4, chars: 11..33)
- assert_node(In, source, at: at, &:consequent)
- end
-
- def test_int
- assert_node(Int, "1")
- end
-
- def test_ivar
- assert_node(IVar, "@variable", &:value)
- end
-
- def test_kw
- at = location(chars: 1..3)
- assert_node(Kw, ":if", at: at, &:value)
- end
-
- def test_kwrest_param
- source = "def method(**kwargs) end"
-
- at = location(chars: 11..19)
- assert_node(KwRestParam, source, at: at) do |node|
- node.params.contents.keyword_rest
- end
- end
-
- def test_label
- source = "{ key: value }"
-
- at = location(chars: 2..6)
- assert_node(Label, source, at: at) { |node| node.assocs.first.key }
- end
-
- def test_lambda
- source = "->(value) { value * 2 }"
-
- assert_node(Lambda, source)
- end
-
- def test_lambda_do
- source = "->(value) do value * 2 end"
-
- assert_node(Lambda, source)
- end
-
- def test_lbrace
- source = "method {}"
-
- at = location(chars: 7..8)
- assert_node(LBrace, source, at: at) { |node| node.block.opening }
- end
-
- def test_lparen
- source = "(1 + 1)"
-
- at = location(chars: 0..1)
- assert_node(LParen, source, at: at, &:lparen)
- end
-
- def test_massign
- assert_node(MAssign, "first, second, third = value")
- end
-
- def test_method_add_block
- assert_node(MethodAddBlock, "method {}")
- end
-
- def test_mlhs
- source = "left, right = value"
-
- at = location(chars: 0..11)
- assert_node(MLHS, source, at: at, &:target)
- end
-
- def test_mlhs_add_post
- source = "left, *middle, right = values"
-
- at = location(chars: 0..20)
- assert_node(MLHS, source, at: at, &:target)
- end
-
- def test_mlhs_paren
- source = "(left, right) = value"
-
- at = location(chars: 0..13)
- assert_node(MLHSParen, source, at: at, &:target)
- end
-
- def test_module
- source = <<~SOURCE
- module Container
- end
- SOURCE
-
- assert_node(ModuleDeclaration, source)
- end
-
- def test_mrhs
- source = "values = first, second, third"
-
- at = location(chars: 9..29)
- assert_node(MRHS, source, at: at, &:value)
- end
-
- def test_mrhs_add_star
- source = "values = first, *rest"
-
- at = location(chars: 9..21)
- assert_node(MRHS, source, at: at, &:value)
- end
-
- def test_next
- at = location(chars: 6..17)
- assert_node(Next, "tap { next(value) }", at: at) do |node|
- node.block.bodystmt.body.first
- end
- end
-
- def test_op
- at = location(chars: 4..5)
- assert_node(Op, "def +(value) end", at: at, &:name)
- end
-
- def test_opassign
- assert_node(OpAssign, "variable += value")
- end
-
- def test_params
- source = <<~SOURCE
- def method(
- one, two,
- three = 3, four = 4,
- *five,
- six:, seven: 7,
- **eight,
- &nine
- ) end
- SOURCE
-
- at = location(lines: 2..7, chars: 11..93)
- assert_node(Params, source, at: at) { |node| node.params.contents }
- end
-
- def test_params_posts
- source = "def method(*rest, post) end"
-
- at = location(chars: 11..22)
- assert_node(Params, source, at: at) { |node| node.params.contents }
- end
-
- def test_paren
- assert_node(Paren, "(1 + 2)")
- end
-
- def test_period
- at = location(chars: 6..7)
- assert_node(Period, "object.method", at: at, &:operator)
- end
-
- def test_program
- parser = SyntaxTree::Parser.new("variable")
- program = parser.parse
- refute(parser.error?)
-
- statements = program.statements.body
- assert_equal 1, statements.size
- assert_kind_of(VCall, statements.first)
-
- assert_kind_of(Program, program)
- assert_equal(location(chars: 0..8), program.location)
- end
-
- def test_qsymbols
- assert_node(QSymbols, "%i[one two three]")
- end
-
- def test_qwords
- assert_node(QWords, "%w[one two three]")
- end
-
- def test_rational
- assert_node(RationalLiteral, "1r")
- end
-
- def test_redo
- assert_node(Redo, "tap { redo }", at: location(chars: 6..10)) do |node|
- node.block.bodystmt.body.first
- end
- end
-
- def test_regexp_literal
- assert_node(RegexpLiteral, "/abc/")
- end
-
- def test_rescue_ex
- source = <<~SOURCE
- begin
- rescue Exception => exception
- end
- SOURCE
-
- at = location(lines: 2..2, chars: 13..35)
- assert_node(RescueEx, source, at: at) do |node|
- node.bodystmt.rescue_clause.exception
- end
- end
-
- def test_rescue
- source = <<~SOURCE
- begin
- rescue First
- rescue Second, Third
- rescue *Fourth
- end
- SOURCE
-
- at = location(lines: 2..5, chars: 6..58)
- assert_node(Rescue, source, at: at) { |node| node.bodystmt.rescue_clause }
- end
-
- def test_rescue_mod
- assert_node(RescueMod, "expression rescue value")
- end
-
- def test_rest_param
- source = "def method(*rest) end"
-
- at = location(chars: 11..16)
- assert_node(RestParam, source, at: at) do |node|
- node.params.contents.rest
- end
- end
-
- def test_retry
- at = location(chars: 15..20)
- assert_node(Retry, "begin; rescue; retry; end", at: at) do |node|
- node.bodystmt.rescue_clause.statements.body.first
- end
- end
-
- def test_return
- assert_node(ReturnNode, "return value")
- end
-
- def test_return0
- assert_node(ReturnNode, "return")
- end
-
- def test_sclass
- assert_node(SClass, "class << self; end")
- end
-
- def test_statements
- at = location(chars: 1..6)
- assert_node(Statements, "(value)", at: at, &:contents)
- end
-
- def test_string_concat
- source = <<~SOURCE
- 'left' \
- 'right'
- SOURCE
-
- assert_node(StringConcat, source)
- end
-
- def test_string_dvar
- at = location(chars: 1..11)
- assert_node(StringDVar, '"#@variable"', at: at) do |node|
- node.parts.first
- end
- end
-
- def test_string_embexpr
- source = '"#{variable}"'
-
- at = location(chars: 1..12)
- assert_node(StringEmbExpr, source, at: at) { |node| node.parts.first }
- end
-
- def test_string_literal
- assert_node(StringLiteral, "\"string\"")
- end
-
- def test_super
- assert_node(Super, "super value")
- end
-
- def test_symbol_literal
- assert_node(SymbolLiteral, ":symbol")
- end
-
- def test_symbols
- assert_node(Symbols, "%I[one two three]")
- end
-
- def test_top_const_field
- source = "::Constant = value"
-
- at = location(chars: 0..10)
- assert_node(TopConstField, source, at: at, &:target)
- end
-
- def test_top_const_ref
- assert_node(TopConstRef, "::Constant")
- end
-
- def test_tstring_content
- source = "\"string\""
-
- at = location(chars: 1..7)
- assert_node(TStringContent, source, at: at) { |node| node.parts.first }
- end
-
- def test_not
- assert_node(Not, "not(value)")
- end
-
- def test_unary
- assert_node(Unary, "+value")
- end
-
- def test_undef
- assert_node(Undef, "undef value")
- end
-
- def test_unless
- assert_node(UnlessNode, "unless value then else end")
- end
-
- def test_unless_mod
- assert_node(UnlessNode, "expression unless predicate")
- end
-
- def test_until
- assert_node(UntilNode, "until value do end")
- end
-
- def test_until_mod
- assert_node(UntilNode, "expression until predicate")
- end
-
- def test_var_alias
- assert_node(AliasNode, "alias $new $old")
- end
-
- def test_var_field
- at = location(chars: 0..8)
- assert_node(VarField, "variable = value", at: at, &:target)
- end
-
- guard_version("3.1.0") do
- def test_pinned_var_ref
- source = "bar = 1; foo in ^bar"
- at = location(chars: 16..20)
-
- assert_node(PinnedVarRef, source, at: at, &:pattern)
- end
- end
-
- def test_var_ref
- assert_node(VarRef, "true")
- end
-
- def test_vcall
- assert_node(VCall, "variable")
- end
-
- def test_void_stmt
- assert_node(VoidStmt, ";;", at: location(chars: 0..0))
- end
-
- def test_when
- source = <<~SOURCE
- case value
- when one then :one
- when two then :two
- end
- SOURCE
-
- at = location(lines: 2..4, chars: 11..52)
- assert_node(When, source, at: at, &:consequent)
- end
-
- def test_while
- assert_node(WhileNode, "while value do end")
- end
-
- def test_while_mod
- assert_node(WhileNode, "expression while predicate")
- end
-
- def test_word
- at = location(chars: 3..7)
- assert_node(Word, "%W[word]", at: at) { |node| node.elements.first }
- end
-
- def test_words
- assert_node(Words, "%W[one two three]")
- end
-
- def test_xstring_literal
- assert_node(XStringLiteral, "`ls`")
- end
-
- def test_xstring_heredoc
- source = <<~SOURCE
- <<~`HEREDOC`
- ls
- HEREDOC
- SOURCE
-
- at = location(lines: 1..3, chars: 0..26)
- assert_node(Heredoc, source, at: at)
- end
-
- def test_yield
- at = location(lines: 2..2, chars: 10..21)
- assert_node(YieldNode, "def foo\n yield value\nend\n", at: at) do |node|
- node.bodystmt.statements.body.first
- end
- end
-
- def test_yield0
- at = location(lines: 2..2, chars: 10..15)
- assert_node(YieldNode, "def foo\n yield\nend\n", at: at) do |node|
- node.bodystmt.statements.body.first
- end
- end
-
- def test_zsuper
- assert_node(ZSuper, "super")
- end
-
- def test_column_positions
- source = <<~SOURCE
- puts 'Hello'
- puts 'Goodbye'
- SOURCE
-
- at = location(lines: 2..2, chars: 13..27, columns: 0..14)
- assert_node(Command, source, at: at)
- end
-
- def test_multibyte_column_positions
- source = <<~SOURCE
- puts "Congrats"
- puts "🎉 🎉"
- SOURCE
-
- at = location(lines: 2..2, chars: 16..26, columns: 0..10)
- assert_node(Command, source, at: at)
- end
-
- def test_root_class_raises_not_implemented_errors
- {
- accept: [nil],
- child_nodes: [],
- deconstruct: [],
- deconstruct_keys: [[]],
- format: [nil]
- }.each do |method, arguments|
- assert_raises(NotImplementedError) do
- Node.new.public_send(method, *arguments)
- end
- end
- end
-
- def test_arity_no_args
- source = <<~SOURCE
- def foo
- end
- SOURCE
-
- at = location(chars: 0..11, columns: 0..3, lines: 1..2)
- assert_node(DefNode, source, at: at) do |node|
- assert_equal(0..0, node.arity)
- node
- end
- end
-
- def test_arity_positionals
- source = <<~SOURCE
- def foo(a, b = 1)
- end
- SOURCE
-
- at = location(chars: 0..21, columns: 0..3, lines: 1..2)
- assert_node(DefNode, source, at: at) do |node|
- assert_equal(1..2, node.arity)
- node
- end
- end
-
- def test_arity_rest
- source = <<~SOURCE
- def foo(a, *b)
- end
- SOURCE
-
- at = location(chars: 0..18, columns: 0..3, lines: 1..2)
- assert_node(DefNode, source, at: at) do |node|
- assert_equal(1.., node.arity)
- node
- end
- end
-
- def test_arity_keyword_rest
- source = <<~SOURCE
- def foo(a, **b)
- end
- SOURCE
-
- at = location(chars: 0..19, columns: 0..3, lines: 1..2)
- assert_node(DefNode, source, at: at) do |node|
- assert_equal(1.., node.arity)
- node
- end
- end
-
- def test_arity_keywords
- source = <<~SOURCE
- def foo(a:, b: 1)
- end
- SOURCE
-
- at = location(chars: 0..21, columns: 0..3, lines: 1..2)
- assert_node(DefNode, source, at: at) do |node|
- assert_equal(1..2, node.arity)
- node
- end
- end
-
- def test_arity_mixed
- source = <<~SOURCE
- def foo(a, b = 1, c:, d: 2)
- end
- SOURCE
-
- at = location(chars: 0..31, columns: 0..3, lines: 1..2)
- assert_node(DefNode, source, at: at) do |node|
- assert_equal(2..4, node.arity)
- node
- end
- end
-
- guard_version("2.7.3") do
- def test_arity_arg_forward
- source = <<~SOURCE
- def foo(...)
- end
- SOURCE
-
- at = location(chars: 0..16, columns: 0..3, lines: 1..2)
- assert_node(DefNode, source, at: at) do |node|
- assert_equal(0.., node.arity)
- node
- end
- end
- end
-
- guard_version("3.0.0") do
- def test_arity_positional_and_arg_forward
- source = <<~SOURCE
- def foo(a, ...)
- end
- SOURCE
-
- at = location(chars: 0..19, columns: 0..3, lines: 1..2)
- assert_node(DefNode, source, at: at) do |node|
- assert_equal(1.., node.arity)
- node
- end
- end
- end
-
- def test_arity_no_parenthesis
- source = <<~SOURCE
- def foo a, b = 1
- end
- SOURCE
-
- at = location(chars: 0..20, columns: 0..3, lines: 1..2)
- assert_node(DefNode, source, at: at) do |node|
- assert_equal(1..2, node.arity)
- node
- end
- end
-
- def test_block_arity_positionals
- source = <<~SOURCE
- [].each do |a, b, c|
- end
- SOURCE
-
- at = location(chars: 8..24, columns: 8..3, lines: 1..2)
- assert_node(BlockNode, source, at: at) do |node|
- block = node.block
- assert_equal(3..3, block.arity)
- block
- end
- end
-
- def test_block_arity_with_optional
- source = <<~SOURCE
- [].each do |a, b = 1|
- end
- SOURCE
-
- at = location(chars: 8..25, columns: 8..3, lines: 1..2)
- assert_node(BlockNode, source, at: at) do |node|
- block = node.block
- assert_equal(1..2, block.arity)
- block
- end
- end
-
- def test_block_arity_with_optional_keyword
- source = <<~SOURCE
- [].each do |a, b: 2|
- end
- SOURCE
-
- at = location(chars: 8..24, columns: 8..3, lines: 1..2)
- assert_node(BlockNode, source, at: at) do |node|
- block = node.block
- assert_equal(1..2, block.arity)
- block
- end
- end
-
- def test_call_node_arity_positional_arguments
- source = <<~SOURCE
- foo(1, 2, 3)
- SOURCE
-
- at = location(chars: 0..12, columns: 0..3, lines: 1..1)
- assert_node(CallNode, source, at: at) do |node|
- assert_equal(3, node.arity)
- node
- end
- end
-
- def test_call_node_arity_keyword_arguments
- source = <<~SOURCE
- foo(bar, something: 123)
- SOURCE
-
- at = location(chars: 0..24, columns: 0..24, lines: 1..1)
- assert_node(CallNode, source, at: at) do |node|
- assert_equal(2, node.arity)
- node
- end
- end
-
- def test_call_node_arity_splat_arguments
- source = <<~SOURCE
- foo(*bar)
- SOURCE
-
- at = location(chars: 0..9, columns: 0..9, lines: 1..1)
- assert_node(CallNode, source, at: at) do |node|
- assert_equal(Float::INFINITY, node.arity)
- node
- end
- end
-
- def test_call_node_arity_keyword_rest_arguments
- source = <<~SOURCE
- foo(**bar)
- SOURCE
-
- at = location(chars: 0..10, columns: 0..10, lines: 1..1)
- assert_node(CallNode, source, at: at) do |node|
- assert_equal(Float::INFINITY, node.arity)
- node
- end
- end
-
- guard_version("2.7.3") do
- def test_call_node_arity_arg_forward_arguments
- source = <<~SOURCE
- def foo(...)
- bar(...)
- end
- SOURCE
-
- at = location(chars: 15..23, columns: 2..10, lines: 2..2)
- assert_node(CallNode, source, at: at) do |node|
- call = node.bodystmt.statements.body.first
- assert_equal(Float::INFINITY, call.arity)
- call
- end
- end
- end
-
- def test_command_arity_positional_arguments
- source = <<~SOURCE
- foo 1, 2, 3
- SOURCE
-
- at = location(chars: 0..11, columns: 0..3, lines: 1..1)
- assert_node(Command, source, at: at) do |node|
- assert_equal(3, node.arity)
- node
- end
- end
-
- def test_command_arity_keyword_arguments
- source = <<~SOURCE
- foo bar, something: 123
- SOURCE
-
- at = location(chars: 0..23, columns: 0..23, lines: 1..1)
- assert_node(Command, source, at: at) do |node|
- assert_equal(2, node.arity)
- node
- end
- end
-
- def test_command_arity_splat_arguments
- source = <<~SOURCE
- foo *bar
- SOURCE
-
- at = location(chars: 0..8, columns: 0..8, lines: 1..1)
- assert_node(Command, source, at: at) do |node|
- assert_equal(Float::INFINITY, node.arity)
- node
- end
- end
-
- def test_command_arity_keyword_rest_arguments
- source = <<~SOURCE
- foo **bar
- SOURCE
-
- at = location(chars: 0..9, columns: 0..9, lines: 1..1)
- assert_node(Command, source, at: at) do |node|
- assert_equal(Float::INFINITY, node.arity)
- node
- end
- end
-
- def test_command_call_arity_positional_arguments
- source = <<~SOURCE
- object.foo 1, 2, 3
- SOURCE
-
- at = location(chars: 0..18, columns: 0..3, lines: 1..1)
- assert_node(CommandCall, source, at: at) do |node|
- assert_equal(3, node.arity)
- node
- end
- end
-
- def test_command_call_arity_keyword_arguments
- source = <<~SOURCE
- object.foo bar, something: 123
- SOURCE
-
- at = location(chars: 0..30, columns: 0..30, lines: 1..1)
- assert_node(CommandCall, source, at: at) do |node|
- assert_equal(2, node.arity)
- node
- end
- end
-
- def test_command_call_arity_splat_arguments
- source = <<~SOURCE
- object.foo *bar
- SOURCE
-
- at = location(chars: 0..15, columns: 0..15, lines: 1..1)
- assert_node(CommandCall, source, at: at) do |node|
- assert_equal(Float::INFINITY, node.arity)
- node
- end
- end
-
- def test_command_call_arity_keyword_rest_arguments
- source = <<~SOURCE
- object.foo **bar
- SOURCE
-
- at = location(chars: 0..16, columns: 0..16, lines: 1..1)
- assert_node(CommandCall, source, at: at) do |node|
- assert_equal(Float::INFINITY, node.arity)
- node
- end
- end
-
- def test_vcall_arity
- source = <<~SOURCE
- foo
- SOURCE
-
- at = location(chars: 0..3, columns: 0..3, lines: 1..1)
- assert_node(VCall, source, at: at) do |node|
- assert_equal(0, node.arity)
- node
- end
- end
-
- private
-
- def location(lines: 1..1, chars: 0..0, columns: 0..0)
- Location.new(
- start_line: lines.begin,
- start_char: chars.begin,
- start_column: columns.begin,
- end_line: lines.end,
- end_char: chars.end,
- end_column: columns.end
- )
- end
-
- def assert_node(kind, source, at: nil)
- at ||=
- location(
- lines: 1..[1, source.count("\n")].max,
- chars: 0..source.chomp.size,
- columns: 0..source.chomp.size
- )
-
- # Parse the example, get the outputted parse tree, and assert that it was
- # able to successfully parse.
- parser = SyntaxTree::Parser.new(source)
- program = parser.parse
- refute(parser.error?)
-
- # Grab the last statement out of the parsed output. If a block is given,
- # then yield that statement so that the test can descend further down the
- # tree to get to the node it is testing.
- node = program.statements.body.last
- node = yield(node) if block_given?
-
- # Assert that the found node is the right type and that it has been found
- # at the expected location.
- assert_kind_of(kind, node)
- assert_equal(at, node.location)
-
- # Finally, test that this node responds to everything it should.
- assert_syntax_tree(node)
- end
- end
-end
diff --git a/test/parser_test.rb b/test/parser_test.rb
deleted file mode 100644
index 169d5b46..00000000
--- a/test/parser_test.rb
+++ /dev/null
@@ -1,126 +0,0 @@
-# frozen_string_literal: true
-
-require_relative "test_helper"
-
-module SyntaxTree
- class ParserTest < Minitest::Test
- def test_parses_ripper_methods
- # First, get a list of all of the dispatched events from ripper.
- events = Ripper::EVENTS
-
- # Next, subtract all of the events that we have explicitly defined.
- events -=
- Parser.private_instance_methods(false).grep(/^on_(\w+)/) { $1.to_sym }
-
- # Next, subtract the list of events that we purposefully skipped.
- events -= %i[
- arg_ambiguous
- assoclist_from_args
- ignored_nl
- ignored_sp
- magic_comment
- nl
- nokw_param
- operator_ambiguous
- semicolon
- sp
- words_sep
- ]
-
- # Finally, assert that we have no remaining events.
- assert_empty(events)
- end
-
- def test_errors_on_missing_token_with_location
- error = assert_raises(Parser::ParseError) { SyntaxTree.parse("f+\"foo") }
- assert_equal(3, error.column)
- end
-
- def test_errors_on_missing_end_with_location
- error = assert_raises(Parser::ParseError) { SyntaxTree.parse("foo do 1") }
- assert_equal(4, error.column)
- end
-
- def test_errors_on_missing_regexp_ending
- error =
- assert_raises(Parser::ParseError) { SyntaxTree.parse("a =~ /foo") }
-
- assert_equal(6, error.column)
- end
-
- def test_errors_on_missing_token_without_location
- assert_raises(Parser::ParseError) { SyntaxTree.parse(":\"foo") }
- end
-
- def test_handles_strings_with_non_terminated_embedded_expressions
- assert_raises(Parser::ParseError) { SyntaxTree.parse('"#{"') }
- end
-
- def test_errors_on_else_missing_two_ends
- assert_raises(Parser::ParseError) { SyntaxTree.parse(<<~RUBY) }
- def foo
- if something
- else
- call do
- end
- RUBY
- end
-
- def test_does_not_choke_on_invalid_characters_in_source_string
- SyntaxTree.parse(<<~RUBY)
- # comment
- # comment
- __END__
- \xC5
- RUBY
- end
-
- def test_lambda_vars_with_parameters_location
- tree = SyntaxTree.parse(<<~RUBY)
- # comment
- # comment
- ->(_i; a) { a }
- RUBY
-
- local_location =
- tree.statements.body.last.params.contents.locals.first.location
-
- assert_equal(3, local_location.start_line)
- assert_equal(3, local_location.end_line)
- assert_equal(7, local_location.start_column)
- assert_equal(8, local_location.end_column)
- end
-
- def test_lambda_vars_location
- tree = SyntaxTree.parse(<<~RUBY)
- # comment
- # comment
- ->(; a) { a }
- RUBY
-
- local_location =
- tree.statements.body.last.params.contents.locals.first.location
-
- assert_equal(3, local_location.start_line)
- assert_equal(3, local_location.end_line)
- assert_equal(5, local_location.start_column)
- assert_equal(6, local_location.end_column)
- end
-
- def test_multiple_lambda_vars_location
- tree = SyntaxTree.parse(<<~RUBY)
- # comment
- # comment
- ->(; a, b, c) { a }
- RUBY
-
- local_location =
- tree.statements.body.last.params.contents.locals.last.location
-
- assert_equal(3, local_location.start_line)
- assert_equal(3, local_location.end_line)
- assert_equal(11, local_location.start_column)
- assert_equal(12, local_location.end_column)
- end
- end
-end
diff --git a/test/plugin/disable_auto_ternary_test.rb b/test/plugin/disable_auto_ternary_test.rb
deleted file mode 100644
index b2af9d35..00000000
--- a/test/plugin/disable_auto_ternary_test.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-# frozen_string_literal: true
-
-require_relative "../test_helper"
-
-module SyntaxTree
- class DisableTernaryTest < Minitest::Test
- def test_short_if_else_unchanged
- assert_format(<<~RUBY)
- if true
- 1
- else
- 2
- end
- RUBY
- end
-
- def test_short_ternary_unchanged
- assert_format("true ? 1 : 2\n")
- end
-
- private
-
- def assert_format(expected, source = expected)
- options = Formatter::Options.new(disable_auto_ternary: true)
- formatter = Formatter.new(source, [], options: options)
- SyntaxTree.parse(source).format(formatter)
-
- formatter.flush
- assert_equal(expected, formatter.output.join)
- end
- end
-end
diff --git a/test/quotes_test.rb b/test/quotes_test.rb
deleted file mode 100644
index 2e2e0243..00000000
--- a/test/quotes_test.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# frozen_string_literal: true
-
-require_relative "test_helper"
-
-module SyntaxTree
- class QuotesTest < Minitest::Test
- def test_normalize
- content = "'aaa' \"bbb\" \\'ccc\\' \\\"ddd\\\""
- enclosing = "\""
-
- result = Quotes.normalize(content, enclosing)
- assert_equal "'aaa' \\\"bbb\\\" \\'ccc\\' \\\"ddd\\\"", result
- end
- end
-end
diff --git a/test/ractor_test.rb b/test/ractor_test.rb
index 40e1eec7..dbf0ea12 100644
--- a/test/ractor_test.rb
+++ b/test/ractor_test.rb
@@ -1,54 +1,40 @@
# frozen_string_literal: true
-# Do not run this test locally, as it messes up coverage.
-return unless ENV["CI"]
+return if !defined?(Ractor) || Gem.win_platform?
-# Don't run this test if we're in a version of Ruby that doesn't have Ractors.
-return unless defined?(Ractor)
-
-# Don't run this version on Ruby 3.0.0. For some reason it just hangs within the
-# main Ractor waiting for this children. Not going to investigate it since it's
-# already been fixed in 3.1.0.
-return if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.1.0")
+# Before Ruby 3.4.0, autoloads could not happen on a non-default Ractor.
+require "prism/parse_result/comments" if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.4.0")
require_relative "test_helper"
module SyntaxTree
class RactorTest < Minitest::Test
- def test_formatting
+ def test_ractors
ractors =
- filepaths.map do |filepath|
- # At the moment we have to parse in the main Ractor because Ripper is
- # not marked as a Ractor-safe extension.
- source = SyntaxTree.read(filepath)
- program = SyntaxTree.parse(source)
-
- with_silenced_warnings do
- Ractor.new(source, program, name: filepath) do |source, program|
- SyntaxTree::Formatter.format(source, program)
- end
+ Dir[File.join(__dir__, "*.rb")].map do |filepath|
+ without_experimental_warnings do
+ Ractor.new(filepath) { |filepath| SyntaxTree.format_file(filepath) }
end
end
- ractors.each { |ractor| assert_kind_of String, ractor.take }
+ ractors.each do |ractor|
+ # Somewhere in the Ruby 3.5.* series, Ractor#take was removed and
+ # Ractor#value was added.
+ value = ractor.respond_to?(:value) ? ractor.value : ractor.take
+ assert_kind_of(String, value)
+ end
end
private
- def filepaths
- Dir.glob(File.expand_path("../lib/syntax_tree/plugin/*.rb", __dir__))
- end
-
- # Ractors still warn about usage, so I'm disabling that warning here just to
- # have clean test output.
- def with_silenced_warnings
- previous = $VERBOSE
+ def without_experimental_warnings
+ previous = Warning[:experimental]
begin
- $VERBOSE = nil
+ Warning[:experimental] = false
yield
ensure
- $VERBOSE = previous
+ Warning[:experimental] = previous
end
end
end
diff --git a/test/rake_test.rb b/test/rake_test.rb
index 90662519..e3ab0e30 100644
--- a/test/rake_test.rb
+++ b/test/rake_test.rb
@@ -1,57 +1,46 @@
# frozen_string_literal: true
require_relative "test_helper"
-require "syntax_tree/rake_tasks"
module SyntaxTree
- module Rake
- class CheckTaskTest < Minitest::Test
- Invocation = Struct.new(:args)
-
- def test_task_command
- assert_raises(NotImplementedError) { Task.new.command }
+ class RakeTest < Minitest::Test
+ def test_check_task
+ source_files = "{app,config,lib}/**/*.rb"
+ print_width = SyntaxTree.options.print_width + 1
+
+ Rake::CheckTask.new do |t|
+ t.source_files = source_files
+ t.print_width = print_width
end
- def test_check_task
- source_files = "{app,config,lib}/**/*.rb"
-
- CheckTask.new do |t|
- t.source_files = source_files
- t.print_width = 100
- t.target_ruby_version = Gem::Version.new("2.6.0")
- end
-
- expected = [
- "check",
- "--print-width=100",
- "--target-ruby-version=2.6.0",
- source_files
- ]
-
- invocation = invoke("stree:check")
- assert_equal(expected, invocation.args)
- end
+ expected = ["check", "--print-width=#{print_width}", source_files]
+ assert_equal(expected, invoke("stree:check"))
+ end
- def test_write_task
- source_files = "{app,config,lib}/**/*.rb"
- WriteTask.new { |t| t.source_files = source_files }
+ def test_write_task
+ source_files = "{app,config,lib}/**/*.rb"
+ trailing_comma = !SyntaxTree.options.trailing_comma
- invocation = invoke("stree:write")
- assert_equal(["write", source_files], invocation.args)
+ Rake::WriteTask.new do |t|
+ t.source_files = source_files
+ t.trailing_comma = trailing_comma
end
- private
+ expected = ["write", "--#{"no-" unless trailing_comma}trailing-comma", source_files]
+ assert_equal(expected, invoke("stree:write"))
+ end
- def invoke(task_name)
- invocation = nil
- stub = ->(args) { invocation = Invocation.new(args) }
+ private
- assert_raises SystemExit do
- SyntaxTree::CLI.stub(:run, stub) { ::Rake::Task[task_name].invoke }
- end
+ def invoke(task_name)
+ invocation = nil
+ stub = ->(args) { invocation = args }
- invocation
+ assert_raises SystemExit do
+ SyntaxTree::CLI.stub(:run, stub) { ::Rake::Task[task_name].invoke }
end
+
+ invocation
end
end
end
diff --git a/test/plugin/single_quotes_test.rb b/test/single_quotes_test.rb
similarity index 60%
rename from test/plugin/single_quotes_test.rb
rename to test/single_quotes_test.rb
index b1359ac7..2c0337dc 100644
--- a/test/plugin/single_quotes_test.rb
+++ b/test/single_quotes_test.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "../test_helper"
+require_relative "test_helper"
module SyntaxTree
class SingleQuotesTest < Minitest::Test
@@ -24,30 +24,19 @@ def test_string_literal_with_interpolation
assert_format("\"\#{foo}\"\n")
end
- def test_dyna_symbol
- assert_format(":'symbol'\n", ":\"symbol\"")
- end
-
def test_single_quote_in_string
assert_format("\"str'ing\"\n")
end
def test_label
- assert_format(
- "{ foo => foo, :'bar' => bar }\n",
- "{ foo => foo, \"bar\": bar }"
- )
+ assert_format("{ foo => foo, :bar => bar }\n", "{ foo => foo, \"bar\": bar }")
end
private
def assert_format(expected, source = expected)
- options = Formatter::Options.new(quote: "'")
- formatter = Formatter.new(source, [], options: options)
- SyntaxTree.parse(source).format(formatter)
-
- formatter.flush
- assert_equal(expected, formatter.output.join)
+ options = SyntaxTree.options(preferred_quote: "'")
+ assert_equal(expected, SyntaxTree.format(source, options))
end
end
end
diff --git a/test/syntax_tree_test.rb b/test/syntax_tree_test.rb
index 27aa6851..aa6bd3ac 100644
--- a/test/syntax_tree_test.rb
+++ b/test/syntax_tree_test.rb
@@ -4,50 +4,43 @@
module SyntaxTree
class SyntaxTreeTest < Minitest::Test
- def test_empty
- void_stmt = SyntaxTree.parse("").statements.body.first
- assert_kind_of(VoidStmt, void_stmt)
+ def test_configure
+ default_print_width = SyntaxTree.options.print_width
+ SyntaxTree.configure { |config| config.print_width = default_print_width }
end
- def test_multibyte
- assign = SyntaxTree.parse("🎉 + 🎉").statements.body.first
- assert_equal(5, assign.location.end_char)
+ def test_format
+ assert_equal("1 + 1\n", SyntaxTree.format("1+1"))
end
- def test_next_statement_start
- source = <<~SOURCE
- def method # comment
- expression
- end
- SOURCE
-
- bodystmt = SyntaxTree.parse(source).statements.body.first.bodystmt
- assert_equal(20, bodystmt.start_char)
- end
-
- def test_parse_error
- assert_raises(Parser::ParseError) { SyntaxTree.parse("<>") }
- end
-
- def test_marshalable
- node = SyntaxTree.parse("1 + 2")
- assert_operator(node, :===, Marshal.load(Marshal.dump(node)))
+ def test_format_file
+ assert_kind_of(String, SyntaxTree.format_file(__FILE__))
end
- def test_maxwidth_format
- assert_equal("foo +\n bar\n", SyntaxTree.format("foo + bar", 5))
+ def test_format_print_width
+ options = Options.new(print_width: 5)
+ assert_equal("foo +\n bar\n", SyntaxTree.format("foo + bar", options))
end
- def test_read
- source = SyntaxTree.read(__FILE__)
- assert_equal(Encoding.default_external, source.encoding)
+ def test_format_stree_ignore
+ source = <<~SOURCE
+ # stree-ignore
+ 1+1
+ SOURCE
- source = SyntaxTree.read(File.expand_path("encoded.rb", __dir__))
- assert_equal(Encoding::Shift_JIS, source.encoding)
+ assert_equal(source, SyntaxTree.format(source))
end
def test_version
refute_nil(VERSION)
end
+
+ def test_visit_methods
+ expected = Prism::Visitor.public_instance_methods.grep(/\Avisit_.+_node\z/).sort
+ actual = Prism::Format.public_instance_methods.grep(/\Avisit_.+_node\z/).sort
+
+ assert_empty(expected - actual)
+ assert_empty(actual - expected)
+ end
end
end
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 3a39ae38..f88716ab 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -3,8 +3,6 @@
unless RUBY_ENGINE == "truffleruby"
require "simplecov"
SimpleCov.start do
- add_filter("idempotency_test.rb") unless ENV["CI"]
- add_filter("ractor_test.rb") unless ENV["CI"]
add_group("lib", "lib")
add_group("test", "test")
end
@@ -12,112 +10,6 @@
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
require "syntax_tree"
-require "syntax_tree/cli"
-require "json"
require "tempfile"
-require "pp"
require "minitest/autorun"
-
-module SyntaxTree
- module Assertions
- class Recorder
- attr_reader :called
-
- def initialize
- @called = nil
- end
-
- def method_missing(called, *, **)
- @called = called
- end
- end
-
- private
-
- # This is a special kind of assertion that is going to get loaded into all
- # of test cases. It asserts against a whole bunch of stuff that every node
- # type should be able to handle. It's here so that we can use it in a bunch
- # of tests.
- def assert_syntax_tree(node)
- recorder = Recorder.new
- node.accept(recorder)
-
- visitor = Parser::Visitor.new
- assert_respond_to(visitor, recorder.called)
-
- assert_kind_of(node.class, node.copy)
- assert_operator(node, :===, node)
- assert_kind_of(Array, node.child_nodes)
- assert_kind_of(Array, node.deconstruct)
- assert_kind_of(Hash, node.deconstruct_keys([]))
- end
-
- Minitest::Test.include(self)
- end
-end
-
-# There are a bunch of fixtures defined in test/fixtures. They exercise every
-# possible combination of syntax that leads to variations in the types of nodes.
-# They are used for testing various parts of Syntax Tree, including formatting,
-# serialization, and parsing. This module provides a single each_fixture method
-# that can be used to drive tests on each fixture.
-module Fixtures
- FIXTURES_3_0_0 = %w[
- command_def_endless
- def_endless
- fndptn
- rassign
- rassign_rocket
- ].freeze
-
- FIXTURES_3_1_0 = %w[pinned_begin var_field_rassign].freeze
-
- Fixture = Struct.new(:name, :source, :formatted, keyword_init: true)
-
- def self.each_fixture
- ruby_version = Gem::Version.new(RUBY_VERSION)
-
- # First, get a list of the basenames of all of the fixture files.
- fixtures =
- Dir[File.expand_path("fixtures/*.rb", __dir__)].map do |filepath|
- File.basename(filepath, ".rb")
- end
-
- # Next, subtract out any fixtures that aren't supported by the current Ruby
- # version.
- fixtures -= FIXTURES_3_1_0 if ruby_version < Gem::Version.new("3.1.0")
- fixtures -= FIXTURES_3_0_0 if ruby_version < Gem::Version.new("3.0.0")
-
- delimiter = /%(?: # (.+?))?\n/
- fixtures.each do |fixture|
- filepath = File.expand_path("fixtures/#{fixture}.rb", __dir__)
-
- # For each fixture in the fixture file yield a Fixture object.
- File
- .readlines(filepath)
- .slice_before(delimiter)
- .each_with_index do |source, index|
- comment = source.shift.match(delimiter)[1]
- source, formatted = source.join.split("-\n")
-
- # If there's a comment starting with >= that starts after the % that
- # delineates the test, then we're going to check if the version
- # satisfies that constraint.
- if comment&.start_with?(">=") &&
- ruby_version < Gem::Version.new(comment.split[1])
- next
- end
-
- name = :"#{fixture}_#{index}"
- yield(
- Fixture.new(
- name: name,
- source: source,
- formatted: formatted || source
- )
- )
- end
- end
- end
-end
diff --git a/test/plugin/trailing_comma_test.rb b/test/trailing_comma_test.rb
similarity index 84%
rename from test/plugin/trailing_comma_test.rb
rename to test/trailing_comma_test.rb
index 7f6e49a8..89e8b13f 100644
--- a/test/plugin/trailing_comma_test.rb
+++ b/test/trailing_comma_test.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "../test_helper"
+require_relative "test_helper"
module SyntaxTree
class TrailingCommaTest < Minitest::Test
@@ -80,12 +80,8 @@ def test_hash_literal_break
private
def assert_format(expected, source = expected)
- options = Formatter::Options.new(trailing_comma: true)
- formatter = Formatter.new(source, [], options: options)
- SyntaxTree.parse(source).format(formatter)
-
- formatter.flush
- assert_equal(expected, formatter.output.join)
+ options = SyntaxTree.options(print_width: 80, trailing_comma: true)
+ assert_equal(expected, SyntaxTree.format(source, options))
end
end
end