diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..b24bb2da --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +bin/* linguist-language=Ruby diff --git a/.rubocop.yml b/.rubocop.yml index 27efc39a..6c9be677 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -7,7 +7,7 @@ AllCops: SuggestExtensions: false TargetRubyVersion: 2.7 Exclude: - - '{bin,coverage,pkg,test/fixtures,vendor,tmp}/**/*' + - '{.git,.github,bin,coverage,pkg,test/fixtures,vendor,tmp}/**/*' - test.rb Layout/LineLength: diff --git a/CHANGELOG.md b/CHANGELOG.md index 45a06c13..034fe2b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,30 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a ## [Unreleased] +## [5.0.0] - 2022-11-09 + +### Added + +- Every node now implements the `#copy(**)` method, which provides a copy of the node with the given attributes replaced. +- Every node now implements the `#===(other)` method, which checks if the given node matches the current node for all attributes except for comments and location. +- There is a new `SyntaxTree::Visitor::MutationVisitor` and its convenience method `SyntaxTree.mutation` which can be used to mutate a syntax tree. For details on how to use this visitor, check the README. + +### Changed + +- Nodes no longer have a `comments:` keyword on their initializers. By default, they initialize to an empty array. If you were previously passing comments into the initializer, you should now create the node first, then call `node.comments.concat` to add your comments. +- A lot of nodes have been folded into other nodes to make it easier to interact with the AST. This means that a lot of visit methods have been removed from the visitor and a lot of class definitions are no longer present. This also means that the nodes that received more function now have additional methods or fields to be able to differentiate them. Note that none of these changes have resulted in different formatting. The changes are listed below: + - `IfMod`, `UnlessMod`, `WhileMod`, `UntilMod` have been folded into `IfNode`, `UnlessNode`, `WhileNode`, and `UntilNode`. Each of the nodes now have a `modifier?` method to tell if it was originally in the modifier form. Consequently, the `visit_if_mod`, `visit_unless_mod`, `visit_while_mod`, and `visit_until_mod` methods have been removed from the visitor. + - `VarAlias` is no longer a node, and the `Alias` node has been renamed. They have been folded into the `AliasNode` node. The `AliasNode` node now has a `var_alias?` method to tell you if it is aliasing a global variable. Consequently, the `visit_var_alias` method has been removed from the visitor interface. If you were previously using this method, you should now use `visit_alias` instead. + - `Yield0` is no longer a node, and the `Yield` node has been renamed. They has been folded into the `YieldNode` node. The `YieldNode` node can now have its `arguments` field be `nil`. Consequently, the `visit_yield0` method has been removed from the visitor interface. If you were previously using this method, you should now use `visit_yield` instead. + - `FCall` is no longer a node, and the `Call` node has been renamed. They have been folded into the `CallNode` node. The `CallNode` node can now have its `receiver` and `operator` fields be `nil`. Consequently, the `visit_fcall` method has been removed from the visitor interface. If you were previously using this method, you should now use `visit_call` instead. + - `Dot2` and `Dot3` are no longer nodes. Instead they have become a single new `RangeNode` node. This node looks the same as `Dot2` and `Dot3`, except that it additionally has an `operator` field that contains the operator that created the node. Consequently, the `visit_dot2` and `visit_dot3` methods have been removed from the visitor interface. If you were previously using these methods, you should now use `visit_range` instead. + - `Def`, `DefEndless`, and `Defs` have been folded into the `DefNode` node. The `DefNode` node now has the `target` and `operator` fields which originally came from `Defs` which can both be `nil`. It also now has an `endless?` method on it to tell if the original node was found in the endless form. Finally the `bodystmt` field can now either be a `BodyStmt` as it was or any other kind of node since that was the body of the `DefEndless` node. The `visit_defs` and `visit_def_endless` methods on the visitor have therefore been removed. + - `DoBlock` and `BraceBlock` have now been folded into a `BlockNode` node. The `BlockNode` node now has a `keywords?` method on it that returns true if the block was constructed with the `do`..`end` keywords. The `visit_do_block` and `visit_brace_block` methods on the visitor have therefore been removed and replaced with the `visit_block` method. + - `Return0` is no longer a node, and the `Return` node has been renamed. They have been folded into the `ReturnNode` node. The `ReturnNode` node can now have its `arguments` field be `nil`. Consequently, the `visit_return0` method has been removed from the visitor interface. If you were previously using this method, you should now use `visit_return` instead. +- The `ArgsForward`, `Redo`, `Retry`, and `ZSuper` nodes no longer have `value` fields associated with them (which were always string literals corresponding to the keyword being used). +- The `Command` and `CommandCall` nodes now has `block` attributes on them. These attributes are used in the place where you would previously have had a `MethodAddBlock` structure. Where before the `MethodAddBlock` would have the command and block as its two children, you now just have one command node with the `block` attribute set to the `Block` node. +- Previously the formatting options were defined on an unfrozen hash called `SyntaxTree::Formatter::OPTIONS`. It was globally mutable, which made it impossible to reference from within a Ractor. As such, it has now been replaced with `SyntaxTree::Formatter::Options.new` which creates a new options object instance that can be modified without impacting global state. As a part of this change, formatting can now be performed from within a non-main Ractor. In order to check if the `plugin/single_quotes` plugin has been loaded, check if `SyntaxTree::Formatter::SINGLE_QUOTES` is defined. In order to check if the `plugin/trailing_comma` plugin has been loaded, check if `SyntaxTree::Formatter::TRAILING_COMMA` is defined. + ## [4.3.0] - 2022-10-28 ### Added @@ -426,7 +450,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a - 🎉 Initial release! 🎉 -[unreleased]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v4.3.0...HEAD +[unreleased]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v5.0.0...HEAD +[5.0.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v4.3.0...v5.0.0 [4.3.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v4.2.0...v4.3.0 [4.2.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v4.1.0...v4.2.0 [4.1.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v4.0.2...v4.1.0 diff --git a/Gemfile.lock b/Gemfile.lock index 25f461c6..d9067ba9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,8 +1,8 @@ PATH remote: . specs: - syntax_tree (4.3.0) - prettier_print (>= 1.0.2) + syntax_tree (5.0.0) + prettier_print (>= 1.1.0) GEM remote: https://rubygems.org/ @@ -14,12 +14,12 @@ GEM parallel (1.22.1) parser (3.1.2.1) ast (~> 2.4.1) - prettier_print (1.0.2) + prettier_print (1.1.0) rainbow (3.1.1) rake (13.0.6) regexp_parser (2.6.0) rexml (3.2.5) - rubocop (1.37.1) + rubocop (1.38.0) json (~> 2.3) parallel (~> 1.10) parser (>= 3.1.2.1) diff --git a/README.md b/README.md index 368c9361..0f1b626a 100644 --- a/README.md +++ b/README.md @@ -27,23 +27,29 @@ It is built with only standard library dependencies. It additionally ships with - [SyntaxTree.read(filepath)](#syntaxtreereadfilepath) - [SyntaxTree.parse(source)](#syntaxtreeparsesource) - [SyntaxTree.format(source)](#syntaxtreeformatsource) + - [SyntaxTree.mutation(&block)](#syntaxtreemutationblock) - [SyntaxTree.search(source, query, &block)](#syntaxtreesearchsource-query-block) - [Nodes](#nodes) - [child_nodes](#child_nodes) + - [copy(**attrs)](#copyattrs) - [Pattern matching](#pattern-matching) - [pretty_print(q)](#pretty_printq) - [to_json(*opts)](#to_jsonopts) - [format(q)](#formatq) + - [===(other)](#other) - [construct_keys](#construct_keys) - [Visitor](#visitor) - [visit_method](#visit_method) - [BasicVisitor](#basicvisitor) + - [MutationVisitor](#mutationvisitor) + - [WithEnvironment](#withenvironment) - [Language server](#language-server) - [textDocument/formatting](#textdocumentformatting) - [textDocument/inlayHint](#textdocumentinlayhint) - [syntaxTree/visualizing](#syntaxtreevisualizing) -- [Plugins](#plugins) - - [Customization](#customization) +- [Customization](#customization) + - [Ignoring code](#ignoring-code) + - [Plugins](#plugins) - [Languages](#languages) - [Integration](#integration) - [Rake](#rake) @@ -332,6 +338,10 @@ This function takes an input string containing Ruby code and returns the syntax This function takes an input string containing Ruby code, parses it into its underlying syntax tree, and formats it back out to a string. You can optionally pass a second argument to this method as well that is the maximum width to print. It defaults to `80`. +### SyntaxTree.mutation(&block) + +This function yields a new mutation visitor to the block, and then returns the initialized visitor. It's effectively a shortcut for creating a `SyntaxTree::Visitor::MutationVisitor` without having to remember the class name. For more information on that visitor, see the definition below. + ### SyntaxTree.search(source, query, &block) This function takes an input string containing Ruby code, an input string containing a valid Ruby `in` clause expression that can be used to match against nodes in the tree (can be generated using `stree expr`, `stree match`, or `Node#construct_keys`), and a block. Each node that matches the given query will be yielded to the block. The block will receive the node as its only argument. @@ -350,6 +360,20 @@ program.child_nodes.first.child_nodes.first # => (binary (int "1") :+ (int "1")) ``` +### copy(**attrs) + +This method returns a copy of the node, with the given attributes replaced. + +```ruby +program = SyntaxTree.parse("1 + 1") + +binary = program.statements.body.first +# => (binary (int "1") + (int "1")) + +binary.copy(operator: :-) +# => (binary (int "1") - (int "1")) +``` + ### Pattern matching Pattern matching is another way to descend the tree which is more specific than using `child_nodes`. Using Ruby's built-in pattern matching, you can extract the same information but be as specific about your constraints as you like. For example, with minimal constraints: @@ -407,6 +431,18 @@ formatter.output.join # => "1 + 1" ``` +### ===(other) + +Every node responds to `===`, which is used to check if the given other node matches all of the attributes of the current node except for location and comments. For example: + +```ruby +program1 = SyntaxTree.parse("1 + 1") +program2 = SyntaxTree.parse("1 + 1") + +program1 === program2 +# => true +``` + ### construct_keys Every node responds to `construct_keys`, which will return a string that contains a Ruby pattern-matching expression that could be used to match against the current node. It's meant to be used in tooling and through the CLI mostly. @@ -495,6 +531,42 @@ end The visitor defined above will error out unless it's only visiting a `SyntaxTree::Int` node. This is useful in a couple of ways, e.g., if you're trying to define a visitor to handle the whole tree but it's currently a work-in-progress. +### MutationVisitor + +The `MutationVisitor` is a visitor that can be used to mutate the tree. It works by defining a default `visit_*` method that returns a copy of the given node with all of its attributes visited. This new node will replace the old node in the tree. Typically, you use the `#mutate` method on it to define mutations using patterns. For example: + +```ruby +# Create a new visitor +visitor = SyntaxTree::Visitor::MutationVisitor.new + +# Specify that it should mutate If nodes with assignments in their predicates +visitor.mutate("IfNode[predicate: Assign | OpAssign]") do |node| + # Get the existing If's predicate node + predicate = node.predicate + + # Create a new predicate node that wraps the existing predicate node + # in parentheses + predicate = + SyntaxTree::Paren.new( + lparen: SyntaxTree::LParen.default, + contents: predicate, + location: predicate.location + ) + + # Return a copy of this node with the new predicate + node.copy(predicate: predicate) +end + +source = "if a = 1; end" +program = SyntaxTree.parse(source) + +SyntaxTree::Formatter.format(source, program) +# => "if a = 1\nend\n" + +SyntaxTree::Formatter.format(source, program.accept(visitor)) +# => "if (a = 1)\nend\n" +``` + ### WithEnvironment The `WithEnvironment` module can be included in visitors to automatically keep track of local variables and arguments @@ -506,13 +578,13 @@ class MyVisitor < Visitor include WithEnvironment def visit_ident(node) - # find_local will return a Local for any local variables or arguments present in the current environment or nil if - # the identifier is not a local + # find_local will return a Local for any local variables or arguments + # present in the current environment or nil if the identifier is not a local local = current_environment.find_local(node) - puts local.type # print the type of the local (:variable or :argument) - puts local.definitions # print the array of locations where this local is defined - puts local.usages # print the array of locations where this local occurs + puts local.type # the type of the local (:variable or :argument) + puts local.definitions # the array of locations where this local is defined + puts local.usages # the array of locations where this local occurs end end ``` @@ -549,18 +621,45 @@ Implicity, the `2 * 3` is going to be executed first because the `*` operator ha The language server additionally includes this custom request to return a textual representation of the syntax tree underlying the source code of a file. Language server clients can use this to (for example) open an additional tab with this information displayed. -## Plugins +## Customization + +There are multiple ways to customize Syntax Tree's behavior when parsing and formatting code. You can ignore certain sections of the source code, you can register plugins to provide custom formatting behavior, and you can register additional languages to be parsed and formatted. + +### Ignoring code + +To ignore a section of source code, you can use a special `# stree-ignore` comment. This comment should be placed immediately above the code that you want to ignore. For example: + +```ruby +numbers = [ + 10000, + 20000, + 30000 +] +``` + +Normally the snippet above would be formatted as `numbers = [10_000, 20_000, 30_000]`. However, sometimes you want to keep the original formatting to improve readability or maintainability. In that case, you can put the ignore comment before it, as in: + +```ruby +# stree-ignore +numbers = [ + 10000, + 20000, + 30000 +] +``` + +Now when Syntax Tree goes to format that code, it will copy the source code exactly as it is, including the newlines and indentation. -You can register additional customization and additional languages that can flow through the same CLI with Syntax Tree's plugin system. When invoking the CLI, you pass through the list of plugins with the `--plugins` options to the commands that accept them. They should be a comma-delimited list. When the CLI first starts, it will require the files corresponding to those names. +### Plugins -### Customization +You can register additional customization that can flow through the same CLI with Syntax Tree's plugin system. When invoking the CLI, you pass through the list of plugins with the `--plugins` options to the commands that accept them. They should be a comma-delimited list. When the CLI first starts, it will require the files corresponding to those names. -To register additional customization, define a file somewhere in your load path named `syntax_tree/my_plugin`. Then when invoking the CLI, you will pass `--plugins=my_plugin`. To require multiple, separate them by a comma. In this way, you can modify Syntax Tree however you would like. Some plugins ship with Syntax Tree itself. They are: +To register plugins, define a file somewhere in your load path named `syntax_tree/my_plugin`. Then when invoking the CLI, you will pass `--plugins=my_plugin`. To require multiple, separate them by a comma. In this way, you can modify Syntax Tree however you would like. Some plugins ship with Syntax Tree itself. They are: * `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. -If you're using Syntax Tree as a library, you should require those files directly. +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 diff --git a/lib/syntax_tree.rb b/lib/syntax_tree.rb index df2f43a9..c2cb3484 100644 --- a/lib/syntax_tree.rb +++ b/lib/syntax_tree.rb @@ -16,6 +16,7 @@ require_relative "syntax_tree/visitor/field_visitor" require_relative "syntax_tree/visitor/json_visitor" require_relative "syntax_tree/visitor/match_visitor" +require_relative "syntax_tree/visitor/mutation_visitor" require_relative "syntax_tree/visitor/pretty_print_visitor" require_relative "syntax_tree/visitor/environment" require_relative "syntax_tree/visitor/with_environment" @@ -53,14 +54,25 @@ def self.parse(source) end # Parses the given source and returns the formatted source. - def self.format(source, maxwidth = DEFAULT_PRINT_WIDTH) - formatter = Formatter.new(source, [], maxwidth) + def self.format( + source, + maxwidth = DEFAULT_PRINT_WIDTH, + options: Formatter::Options.new + ) + formatter = Formatter.new(source, [], maxwidth, options: options) parse(source).format(formatter) formatter.flush formatter.output.join end + # A convenience method for creating a new mutation visitor. + def self.mutation + visitor = Visitor::MutationVisitor.new + yield visitor + visitor + end + # Returns the source from the given filepath taking into account any potential # magic encoding comments. def self.read(filepath) diff --git a/lib/syntax_tree/cli.rb b/lib/syntax_tree/cli.rb index 62e8ab68..3975df18 100644 --- a/lib/syntax_tree/cli.rb +++ b/lib/syntax_tree/cli.rb @@ -131,9 +131,14 @@ class UnformattedError < StandardError def run(item) source = item.source - if source != item.handler.format(source, options.print_width) - raise UnformattedError - end + formatted = + item.handler.format( + source, + options.print_width, + options: options.formatter_options + ) + + raise UnformattedError if source != formatted rescue StandardError warn("[#{Color.yellow("warn")}] #{item.filepath}") raise @@ -156,13 +161,23 @@ class NonIdempotentFormatError < StandardError def run(item) handler = item.handler - warning = "[#{Color.yellow("warn")}] #{item.filepath}" - formatted = handler.format(item.source, options.print_width) - if formatted != handler.format(formatted, options.print_width) - raise NonIdempotentFormatError - end + formatted = + handler.format( + item.source, + options.print_width, + options: options.formatter_options + ) + + double_formatted = + handler.format( + formatted, + options.print_width, + options: options.formatter_options + ) + + raise NonIdempotentFormatError if formatted != double_formatted rescue StandardError warn(warning) raise @@ -182,7 +197,9 @@ class Doc < Action def run(item) source = item.source - formatter = Formatter.new(source, []) + formatter_options = options.formatter_options + formatter = Formatter.new(source, [], options: formatter_options) + item.handler.parse(source).format(formatter) pp formatter.groups.first end @@ -206,7 +223,14 @@ def run(item) # An action of the CLI that formats the input source and prints it out. class Format < Action def run(item) - puts item.handler.format(item.source, options.print_width) + formatted = + item.handler.format( + item.source, + options.print_width, + options: options.formatter_options + ) + + puts formatted end end @@ -273,7 +297,13 @@ def run(item) start = Time.now source = item.source - formatted = item.handler.format(source, options.print_width) + formatted = + item.handler.format( + source, + options.print_width, + options: options.formatter_options + ) + File.write(filepath, formatted) if item.writable? color = source == formatted ? Color.gray(filepath) : filepath @@ -347,20 +377,16 @@ class Options :plugins, :print_width, :scripts, - :target_ruby_version + :formatter_options - def initialize(print_width: DEFAULT_PRINT_WIDTH) + def initialize @ignore_files = [] @plugins = [] - @print_width = print_width + @print_width = DEFAULT_PRINT_WIDTH @scripts = [] - @target_ruby_version = nil + @formatter_options = Formatter::Options.new end - # TODO: This function causes a couple of side-effects that I really don't - # like to have here. It mutates the global state by requiring the plugins, - # and mutates the global options hash by adding the target ruby version. - # That should be done on a config-by-config basis, not here. def parse(arguments) parser.parse!(arguments) end @@ -404,8 +430,10 @@ def parser # 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 = Gem::Version.new(version) - Formatter::OPTIONS[:target_ruby_version] = @target_ruby_version + @formatter_options = + Formatter::Options.new( + target_ruby_version: Formatter::SemanticVersion.new(version) + ) end end end @@ -497,10 +525,14 @@ def run(argv) Dir .glob(pattern) .each do |filepath| - if File.readable?(filepath) && - options.ignore_files.none? { File.fnmatch?(_1, filepath) } - queue << FileItem.new(filepath) - end + # Skip past invalid filepaths by default. + next unless File.readable?(filepath) + + # Skip past any ignored filepaths. + next if options.ignore_files.any? { File.fnmatch(_1, filepath) } + + # Otherwise, a new file item for the given filepath to the list. + queue << FileItem.new(filepath) end end diff --git a/lib/syntax_tree/formatter.rb b/lib/syntax_tree/formatter.rb index f878490c..d5d251c6 100644 --- a/lib/syntax_tree/formatter.rb +++ b/lib/syntax_tree/formatter.rb @@ -4,21 +4,63 @@ 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. - # - # Note that we're keeping this in a global-ish hash instead of just - # overriding methods on classes so that other plugins can reference this if - # necessary. For example, the RBS plugin references the quote style. - OPTIONS = { - quote: "\"", - trailing_comma: false, - target_ruby_version: Gem::Version.new(RUBY_VERSION) - } + class Options + attr_reader :quote, :trailing_comma, :target_ruby_version + + def initialize( + quote: :default, + trailing_comma: :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 + + @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 @@ -30,22 +72,16 @@ class Formatter < PrettierPrint attr_reader :quote, :trailing_comma, :target_ruby_version alias trailing_comma? trailing_comma - def initialize( - source, - *args, - quote: OPTIONS[:quote], - trailing_comma: OPTIONS[:trailing_comma], - target_ruby_version: OPTIONS[:target_ruby_version] - ) + def initialize(source, *args, options: Options.new) super(*args) @source = source @stack = [] - # Memoizing these values per formatter to make access faster. - @quote = quote - @trailing_comma = trailing_comma - @target_ruby_version = target_ruby_version + # Memoizing these values to make access faster. + @quote = options.quote + @trailing_comma = options.trailing_comma + @target_ruby_version = options.target_ruby_version end def self.format(source, node) diff --git a/lib/syntax_tree/node.rb b/lib/syntax_tree/node.rb index aa133b7f..f32789a3 100644 --- a/lib/syntax_tree/node.rb +++ b/lib/syntax_tree/node.rb @@ -83,6 +83,20 @@ def self.fixed(line:, char:, column:) 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 @@ -127,6 +141,18 @@ def construct_keys 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. @@ -146,11 +172,11 @@ class BEGINBlock < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(lbrace:, statements:, location:, comments: []) + def initialize(lbrace:, statements:, location:) @lbrace = lbrace @statements = statements @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -161,6 +187,18 @@ 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) @@ -184,6 +222,11 @@ def format(q) 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. @@ -199,10 +242,10 @@ class CHAR < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(value:, location:, comments: []) + def initialize(value:, location:) @value = value @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -213,6 +256,17 @@ 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) @@ -228,6 +282,10 @@ def format(q) 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 @@ -249,11 +307,11 @@ class ENDBlock < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(lbrace:, statements:, location:, comments: []) + def initialize(lbrace:, statements:, location:) @lbrace = lbrace @statements = statements @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -264,6 +322,18 @@ 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) @@ -287,6 +357,11 @@ def format(q) 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 @@ -305,10 +380,10 @@ class EndContent < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(value:, location:, comments: []) + def initialize(value:, location:) @value = value @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -319,6 +394,17 @@ 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) @@ -342,6 +428,10 @@ def format(q) 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 @@ -355,11 +445,12 @@ def format(q) # can either provide bare words (like the example above) or you can provide # symbols (note that this includes dynamic symbols like # :"left-#{middle}-right"). - class Alias < Node + class 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 - # [DynaSymbol | SymbolLiteral] the argument being passed to alias + # [Backref | DynaSymbol | GVar | SymbolLiteral] the argument being passed + # to alias attr_reader :argument def initialize(argument) @@ -383,20 +474,20 @@ def format(q) end end - # [DynaSymbol | SymbolLiteral] the new name of the method + # [DynaSymbol | GVar | SymbolLiteral] the new name of the method attr_reader :left - # [DynaSymbol | SymbolLiteral] the old name of the method + # [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:, comments: []) + def initialize(left:, right:, location:) @left = left @right = right @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -407,6 +498,18 @@ 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) @@ -428,6 +531,14 @@ def format(q) end end end + + def ===(other) + other.is_a?(AliasNode) && left === other.left && right === other.right + end + + def var_alias? + left.is_a?(GVar) + end end # ARef represents when you're pulling a value out of a collection at a @@ -453,11 +564,11 @@ class ARef < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(collection:, index:, location:, comments: []) + def initialize(collection:, index:, location:) @collection = collection @index = index @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -468,6 +579,18 @@ 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) @@ -495,6 +618,11 @@ def format(q) 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. @@ -514,11 +642,11 @@ class ARefField < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(collection:, index:, location:, comments: []) + def initialize(collection:, index:, location:) @collection = collection @index = index @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -529,6 +657,18 @@ 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) @@ -556,6 +696,11 @@ def format(q) 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 @@ -577,10 +722,10 @@ class ArgParen < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(arguments:, location:, comments: []) + def initialize(arguments:, location:) @arguments = arguments @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -591,6 +736,17 @@ 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) @@ -615,6 +771,10 @@ def format(q) q.text(")") end + def ===(other) + other.is_a?(ArgParen) && arguments === other.arguments + end + private def trailing_comma? @@ -650,10 +810,10 @@ class Args < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(parts:, location:, comments: []) + def initialize(parts:, location:) @parts = parts @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -664,6 +824,17 @@ 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) @@ -673,6 +844,10 @@ def deconstruct_keys(_keys) def format(q) q.seplist(parts) { |part| q.format(part) } end + + def ===(other) + other.is_a?(Args) && ArrayMatch.call(parts, other.parts) + end end # ArgBlock represents using a block operator on an expression. @@ -686,10 +861,10 @@ class ArgBlock < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(value:, location:, comments: []) + def initialize(value:, location:) @value = value @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -700,6 +875,17 @@ 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) @@ -710,6 +896,10 @@ 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. @@ -723,10 +913,10 @@ class ArgStar < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(value:, location:, comments: []) + def initialize(value:, location:) @value = value @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -737,6 +927,17 @@ 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) @@ -747,6 +948,10 @@ 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 @@ -767,16 +972,12 @@ def format(q) # The ArgsForward node appears in both the caller (the request method calls) # and the callee (the get and post definitions). class ArgsForward < Node - # [String] the value of the operator - attr_reader :value - # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(value:, location:, comments: []) - @value = value + def initialize(location:) @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -787,14 +988,25 @@ 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) - { value: value, location: location, comments: comments } + { location: location, comments: comments } end def format(q) - q.text(value) + q.text("...") + end + + def ===(other) + other.is_a?(ArgsForward) end end @@ -814,7 +1026,7 @@ def call(q) end end - BREAKABLE_SPACE_SEPARATOR = BreakableSpaceSeparator.new + BREAKABLE_SPACE_SEPARATOR = BreakableSpaceSeparator.new.freeze # Formats an array of multiple simple string literals into the %w syntax. class QWordsFormatter @@ -955,11 +1167,11 @@ def format(q) # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(lbracket:, contents:, location:, comments: []) + def initialize(lbracket:, contents:, location:) @lbracket = lbracket @contents = contents @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -970,6 +1182,18 @@ 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) @@ -1018,6 +1242,11 @@ def format(q) end end + def ===(other) + other.is_a?(ArrayLiteral) && lbracket === other.lbracket && + contents === other.contents + end + private def qwords? @@ -1119,20 +1348,13 @@ def format(q) # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize( - constant:, - requireds:, - rest:, - posts:, - location:, - comments: [] - ) + def initialize(constant:, requireds:, rest:, posts:, location:) @constant = constant @requireds = requireds @rest = rest @posts = posts @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -1143,6 +1365,26 @@ 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) @@ -1173,6 +1415,12 @@ def format(q) 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. @@ -1182,7 +1430,7 @@ def self.skip_indent?(value) when ArrayLiteral, HashLiteral, Heredoc, Lambda, QSymbols, QWords, Symbols, Words true - when Call + when CallNode skip_indent?(value.receiver) when DynaSymbol value.quote.start_with?("%s") @@ -1209,11 +1457,11 @@ class Assign < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(target:, value:, location:, comments: []) + def initialize(target:, value:, location:) @target = target @value = value @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -1224,6 +1472,18 @@ 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) @@ -1247,6 +1507,10 @@ def format(q) end end + def ===(other) + other.is_a?(Assign) && target === other.target && value === other.value + end + private def skip_indent? @@ -1260,7 +1524,7 @@ def skip_indent? # # { key1: value1, key2: value2 } # - # In the above example, the would be two AssocNew nodes. + # In the above example, the would be two Assoc nodes. class Assoc < Node # [untyped] the key of this pair attr_reader :key @@ -1271,11 +1535,11 @@ class Assoc < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(key:, value:, location:, comments: []) + def initialize(key:, value:, location:) @key = key @value = value @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -1286,6 +1550,18 @@ 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) @@ -1300,6 +1576,10 @@ def format(q) end end + def ===(other) + other.is_a?(Assoc) && key === other.key && value === other.value + end + private def format_contents(q) @@ -1330,10 +1610,10 @@ class AssocSplat < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(value:, location:, comments: []) + def initialize(value:, location:) @value = value @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -1344,6 +1624,17 @@ 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) @@ -1354,6 +1645,10 @@ def format(q) q.text("**") q.format(value) end + + def ===(other) + other.is_a?(AssocSplat) && value === other.value + end end # Backref represents a global variable referencing a matched value. It comes @@ -1368,10 +1663,10 @@ class Backref < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(value:, location:, comments: []) + def initialize(value:, location:) @value = value @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -1382,6 +1677,17 @@ 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) @@ -1391,6 +1697,10 @@ def deconstruct_keys(_keys) 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 @@ -1403,10 +1713,10 @@ class Backtick < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(value:, location:, comments: []) + def initialize(value:, location:) @value = value @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -1417,6 +1727,17 @@ 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) @@ -1426,6 +1747,10 @@ def deconstruct_keys(_keys) 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 @@ -1434,7 +1759,7 @@ def format(q) module HashKeyFormatter # Formats the keys of a hash literal using labels. class Labels - LABEL = /\A[A-Za-z_](\w*[\w!?])?\z/ + LABEL = /\A[A-Za-z_](\w*[\w!?])?\z/.freeze def format_key(q, key) case key @@ -1515,10 +1840,10 @@ class BareAssocHash < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(assocs:, location:, comments: []) + def initialize(assocs:, location:) @assocs = assocs @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -1529,6 +1854,17 @@ 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) @@ -1539,6 +1875,10 @@ 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 ||= HashKeyFormatter.for(self)).format_key(q, key) end @@ -1557,10 +1897,10 @@ class Begin < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(bodystmt:, location:, comments: []) + def initialize(bodystmt:, location:) @bodystmt = bodystmt @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -1571,6 +1911,17 @@ 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) @@ -1590,6 +1941,10 @@ def format(q) 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. @@ -1605,10 +1960,10 @@ class PinnedBegin < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(statement:, location:, comments: []) + def initialize(statement:, location:) @statement = statement @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -1619,6 +1974,17 @@ 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) @@ -1638,6 +2004,10 @@ def format(q) 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 @@ -1677,12 +2047,12 @@ def name # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(left:, operator:, right:, location:, comments: []) + def initialize(left:, operator:, right:, location:) @left = left @operator = operator @right = right @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -1693,6 +2063,19 @@ 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) @@ -1726,6 +2109,11 @@ def format(q) 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 @@ -1745,11 +2133,11 @@ class BlockVar < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(params:, locals:, location:, comments: []) + def initialize(params:, locals:, location:) @params = params @locals = locals @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -1760,6 +2148,18 @@ 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) @@ -1776,7 +2176,7 @@ def call(q) # We'll keep a single instance of this separator around for all block vars # to cut down on allocations. - SEPARATOR = Separator.new + SEPARATOR = Separator.new.freeze def format(q) q.text("|") @@ -1790,6 +2190,11 @@ def format(q) end q.text("|") end + + def ===(other) + other.is_a?(BlockVar) && params === other.params && + ArrayMatch.call(locals, other.locals) + end end # BlockArg represents declaring a block parameter on a method definition. @@ -1803,10 +2208,10 @@ class BlockArg < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(name:, location:, comments: []) + def initialize(name:, location:) @name = name @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -1817,6 +2222,17 @@ 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) @@ -1827,6 +2243,10 @@ 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 @@ -1857,8 +2277,7 @@ def initialize( else_keyword:, else_clause:, ensure_clause:, - location:, - comments: [] + location: ) @statements = statements @rescue_clause = rescue_clause @@ -1866,7 +2285,7 @@ def initialize( @else_clause = else_clause @ensure_clause = ensure_clause @location = location - @comments = comments + @comments = [] end def bind(start_char, start_column, end_char, end_column) @@ -1911,12 +2330,35 @@ 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, @@ -1955,244 +2397,42 @@ def format(q) end end end - end - - # Responsible for formatting either a BraceBlock or a DoBlock. - class BlockFormatter - # 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 + 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 - # [BraceBlock | DoBlock] the block node to be formatted - attr_reader :node - - # [LBrace | Keyword] the node that opens the block - attr_reader :block_open - - # [String] the string that closes the block - attr_reader :block_close + # Formats either a Break, Next, or Return node. + class FlowControlFormatter + # [String] the keyword to print + attr_reader :keyword - # [BodyStmt | Statements] the statements inside the block - attr_reader :statements + # [Break | Next | Return] the node being formatted + attr_reader :node - def initialize(node, block_open, block_close, statements) + def initialize(keyword, node) + @keyword = keyword @node = node - @block_open = block_open - @block_close = block_close - @statements = statements end def format(q) - # If this is nested anywhere inside of a Command or CommandCall node, then - # we can't change which operators we're using for the bounds of the block. - break_opening, break_closing, flat_opening, flat_closing = - if unchangeable_bounds?(q) - [block_open.value, block_close, block_open.value, block_close] - elsif forced_do_end_bounds?(q) - %w[do end do end] - elsif forced_brace_bounds?(q) - %w[{ } { }] - else - %w[do end { }] - end - - # If the receiver of this block a Command or CommandCall node, then there - # are no parentheses around the arguments to that command, so we need to - # break the block. - case q.parent.call - when Command, CommandCall - q.break_parent - format_break(q, break_opening, break_closing) + # 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 - .if_break { format_break(q, break_opening, break_closing) } - .if_flat { format_flat(q, flat_opening, flat_closing) } - end - end + q.text(keyword) - 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, Return, 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 If, IfMod, IfOp, Unless, UnlessMod, While, WhileMod, Until, - UntilMod - return true if parent.predicate == previous - end - - previous = parent - false - end - end - - def format_break(q, opening, closing) - q.text(" ") - q.format(BlockOpenFormatter.new(opening, block_open), stackable: false) - - if node.block_var - q.text(" ") - q.format(node.block_var) - end - - unless statements.empty? - q.indent do - q.breakable_space - q.format(statements) - end - end - - q.breakable_space - q.text(closing) - end - - def format_flat(q, opening, closing) - q.text(" ") - q.format(BlockOpenFormatter.new(opening, block_open), stackable: false) - - if node.block_var - q.breakable_space - q.format(node.block_var) - q.breakable_space - end - - if statements.empty? - q.text(" ") if opening == "do" - else - q.breakable_space unless node.block_var - q.format(statements) - q.breakable_space - end - - q.text(closing) - end - end - - # BraceBlock represents passing a block to a method call using the { } - # operators. - # - # method { |variable| variable + 1 } - # - class BraceBlock < Node - # [LBrace] the left brace that opens this block - attr_reader :lbrace - - # [nil | BlockVar] the optional set of parameters to the block - attr_reader :block_var - - # [Statements] the list of expressions to evaluate within the block - attr_reader :statements - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(lbrace:, block_var:, statements:, location:, comments: []) - @lbrace = lbrace - @block_var = block_var - @statements = statements - @location = location - @comments = comments - end - - def accept(visitor) - visitor.visit_brace_block(self) - end - - def child_nodes - [lbrace, block_var, statements] - end - - alias deconstruct child_nodes - - def deconstruct_keys(_keys) - { - lbrace: lbrace, - block_var: block_var, - statements: statements, - location: location, - comments: comments - } - end - - def format(q) - BlockFormatter.new(self, lbrace, "}", statements).format(q) - end - 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) - q.group do - q.text(keyword) - - parts = node.arguments.parts - length = parts.length + parts = node.arguments.parts + length = parts.length if length == 0 # Here there are no arguments at all, so we're not going to print @@ -2371,10 +2611,10 @@ class Break < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(arguments:, location:, comments: []) + def initialize(arguments:, location:) @arguments = arguments @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -2385,6 +2625,17 @@ 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) @@ -2394,6 +2645,10 @@ def deconstruct_keys(_keys) 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 @@ -2452,17 +2707,29 @@ def format(q) # longer at a chainable node. loop do case (child = children.last) - when Call + when CallNode case (receiver = child.receiver) - when Call - children << receiver + when CallNode + if receiver.receiver.nil? + break + else + children << receiver + end when MethodAddBlock - receiver.call.is_a?(Call) ? children << receiver : break + if receiver.call.is_a?(CallNode) && !receiver.call.receiver.nil? + children << receiver + else + break + end else break end when MethodAddBlock - child.call.is_a?(Call) ? children << child.call : break + if child.call.is_a?(CallNode) && !child.call.receiver.nil? + children << child.call + else + break + end else break end @@ -2476,13 +2743,13 @@ def format(q) # https://github.com/prettier/plugin-ruby/issues/863. parents = q.parents.take(4) if (parent = parents[2]) - # If we're at a do_block, 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?(DoBlock) + # 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) && parent.call.is_a?(FCall) && - parent.call.value.value == "sig" + if parent.is_a?(MethodAddBlock) && parent.call.is_a?(CallNode) && + parent.call.message.value == "sig" threshold = 2 end end @@ -2505,13 +2772,13 @@ def format_chain(q, children) empty_except_last = children .drop(1) - .all? { |child| child.is_a?(Call) && child.arguments.nil? } + .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) + q.format(children.last.receiver) if children.last.receiver q.group do if attach_directly?(children.last) @@ -2526,8 +2793,8 @@ def format_chain(q, children) skip_operator = false while (child = children.pop) - if child.is_a?(Call) - if child.receiver.is_a?(Call) && + if child.is_a?(CallNode) + if child.receiver.is_a?(CallNode) && (child.receiver.message != :call) && (child.receiver.message.value == "where") && (child.message.value == "not") @@ -2554,7 +2821,8 @@ def format_chain(q, children) # 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?(Call) && last_child.message.comments.any? + if last_child.is_a?(CallNode) && last_child.message.comments.any? && + last_child.operator q.format(CallOperatorFormatter.new(last_child.operator)) skip_operator = true else @@ -2570,7 +2838,7 @@ def format_chain(q, children) if empty_except_last case node - when Call + when CallNode node.format_arguments(q) when MethodAddBlock q.format(node.block) @@ -2582,10 +2850,10 @@ def self.chained?(node) return false if ENV["STREE_FAST_FORMAT"] case node - when Call - true + when CallNode + !node.receiver.nil? when MethodAddBlock - node.call.is_a?(Call) + node.call.is_a?(CallNode) && !node.call.receiver.nil? else false end @@ -2598,7 +2866,8 @@ def self.chained?(node) # format it separately here. def attach_directly?(node) case node.receiver - when ArrayLiteral, HashLiteral, Heredoc, If, Unless, XStringLiteral + when ArrayLiteral, HashLiteral, Heredoc, IfNode, UnlessNode, + XStringLiteral true else false @@ -2614,9 +2883,9 @@ def format_child( ) # First, format the actual contents of the child. case child - when Call + when CallNode q.group do - unless skip_operator + if !skip_operator && child.operator q.format(CallOperatorFormatter.new(child.operator)) end q.format(child.message) if child.message != :call @@ -2639,15 +2908,15 @@ def format_child( end end - # Call represents a method call. + # CallNode represents a method call. # # receiver.message # - class Call < Node - # [untyped] the receiver of the method call + class CallNode < Node + # [nil | untyped] the receiver of the method call attr_reader :receiver - # [:"::" | Op | Period] the operator being used to send the message + # [nil | :"::" | Op | Period] the operator being used to send the message attr_reader :operator # [:call | Backtick | Const | Ident | Op] the message being sent @@ -2659,20 +2928,13 @@ class Call < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize( - receiver:, - operator:, - message:, - arguments:, - location:, - comments: [] - ) + def initialize(receiver:, operator:, message:, arguments:, location:) @receiver = receiver @operator = operator @message = message @arguments = arguments @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -2688,6 +2950,26 @@ def child_nodes ] 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) @@ -2702,23 +2984,44 @@ def deconstruct_keys(_keys) 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?(receiver) && - !CallChainFormatter.chained?(q.parent) - q.group do - q - .if_break { CallChainFormatter.new(self).format(q) } - .if_flat { format_contents(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 - format_contents(q) + q.format(message) + + if arguments.is_a?(ArgParen) && arguments.arguments.nil? && + !message.is_a?(Const) + # If you're using an explicit set of parentheses on something that + # looks like a constant, then we need to match that in order to + # maintain valid Ruby. For example, you could do something like Foo(), + # on which we would need to keep the parentheses to make it look like + # a method call. + else + q.format(arguments) + end 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. + # nothing. def format_arguments(q) case arguments when ArgParen @@ -2782,12 +3085,12 @@ class Case < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(keyword:, value:, consequent:, location:, comments: []) + def initialize(keyword:, value:, consequent:, location:) @keyword = keyword @value = value @consequent = consequent @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -2798,6 +3101,19 @@ 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) @@ -2826,6 +3142,11 @@ def format(q) 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. @@ -2847,12 +3168,12 @@ class RAssign < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(value:, operator:, pattern:, location:, comments: []) + def initialize(value:, operator:, pattern:, location:) @value = value @operator = operator @pattern = pattern @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -2863,6 +3184,19 @@ 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) @@ -2895,6 +3229,11 @@ def format(q) 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. @@ -2943,12 +3282,12 @@ class ClassDeclaration < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(constant:, superclass:, bodystmt:, location:, comments: []) + def initialize(constant:, superclass:, bodystmt:, location:) @constant = constant @superclass = superclass @bodystmt = bodystmt @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -2959,6 +3298,19 @@ 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) @@ -2993,6 +3345,11 @@ def format(q) end end + def ===(other) + other.is_a?(ClassDeclaration) && constant === other.constant && + superclass === other.superclass && bodystmt === other.bodystmt + end + private def format_declaration(q) @@ -3026,11 +3383,19 @@ 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 @@ -3046,14 +3411,18 @@ class Command < Node # [Args] the arguments being sent with the message attr_reader :arguments + # [nil | Block] 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:, location:, comments: []) + def initialize(message:, arguments:, block:, location:) @message = message @arguments = arguments + @block = block @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -3061,7 +3430,20 @@ def accept(visitor) end def child_nodes - [message, arguments] + [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 @@ -3070,6 +3452,7 @@ def deconstruct_keys(_keys) { message: message, arguments: arguments, + block: block, location: location, comments: comments } @@ -3080,6 +3463,13 @@ def format(q) 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 private @@ -3094,7 +3484,7 @@ def align(q, node, &block) part = parts.first case part - when Def, Defs, DefEndless + when DefNode q.text(" ") yield when IfOp @@ -3135,6 +3525,9 @@ class CommandCall < Node # [nil | Args] the arguments going along with the message attr_reader :arguments + # [nil | Block] the block associated with this method call + attr_reader :block + # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments @@ -3143,15 +3536,16 @@ def initialize( operator:, message:, arguments:, - location:, - comments: [] + block:, + location: ) @receiver = receiver @operator = operator @message = message @arguments = arguments + @block = block @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -3159,7 +3553,29 @@ def accept(visitor) end def child_nodes - [receiver, message, arguments] + [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 @@ -3170,6 +3586,7 @@ def deconstruct_keys(_keys) operator: operator, message: message, arguments: arguments, + block: block, location: location, comments: comments } @@ -3210,6 +3627,14 @@ def format(q) 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 private @@ -3291,6 +3716,14 @@ 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) @@ -3300,6 +3733,10 @@ def deconstruct_keys(_keys) 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 @@ -3323,10 +3760,10 @@ class Const < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(value:, location:, comments: []) + def initialize(value:, location:) @value = value @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -3337,6 +3774,17 @@ 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) @@ -3346,6 +3794,10 @@ def deconstruct_keys(_keys) 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 @@ -3364,11 +3816,11 @@ class ConstPathField < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(parent:, constant:, location:, comments: []) + def initialize(parent:, constant:, location:) @parent = parent @constant = constant @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -3379,6 +3831,18 @@ 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) @@ -3395,6 +3859,11 @@ def format(q) 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. @@ -3411,11 +3880,11 @@ class ConstPathRef < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(parent:, constant:, location:, comments: []) + def initialize(parent:, constant:, location:) @parent = parent @constant = constant @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -3426,6 +3895,18 @@ 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) @@ -3442,6 +3923,11 @@ def format(q) 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 @@ -3457,10 +3943,10 @@ class ConstRef < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(constant:, location:, comments: []) + def initialize(constant:, location:) @constant = constant @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -3471,6 +3957,17 @@ 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) @@ -3480,6 +3977,10 @@ def deconstruct_keys(_keys) 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. @@ -3493,10 +3994,10 @@ class CVar < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(value:, location:, comments: []) + def initialize(value:, location:) @value = value @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -3507,6 +4008,17 @@ 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) @@ -3516,31 +4028,44 @@ def deconstruct_keys(_keys) 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 Def < Node + class DefNode < Node + # [nil | untyped] 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 - # [Params | Paren] the parameter declaration for the method + # [nil | Params | Paren] the parameter declaration for the method attr_reader :params - # [BodyStmt] the expressions to be executed by the method + # [BodyStmt | untyped] 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(name:, params:, bodystmt:, location:, comments: []) + def initialize(target:, operator:, name:, params:, bodystmt:, location:) + @target = target + @operator = operator @name = name @params = params @bodystmt = bodystmt @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -3548,13 +4073,37 @@ def accept(visitor) end def child_nodes - [name, params, bodystmt] + [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, @@ -3567,114 +4116,55 @@ def format(q) q.group do q.group do q.text("def ") + + if target + q.format(target) + q.format(CallOperatorFormatter.new(operator), stackable: false) + end + q.format(name) - if !params.is_a?(Params) || !params.empty? || params.comments.any? + case params + when Paren q.format(params) + when Params + q.format(params) if !params.empty? || params.comments.any? end end - unless bodystmt.empty? - q.indent do - q.breakable_force - q.format(bodystmt) + 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 - end - q.breakable_force - q.text("end") + q.breakable_force + q.text("end") + end end end - end - - # DefEndless represents defining a single-line method since Ruby 3.0+. - # - # def method = result - # - class DefEndless < Node - # [untyped] the target where the method is being defined - attr_reader :target - - # [Op | Period] the operator being used to declare the method - attr_reader :operator - - # [Backtick | Const | Ident | Kw | Op] the name of the method - attr_reader :name - - # [nil | Params | Paren] the parameter declaration for the method - attr_reader :paren - - # [untyped] the expression to be executed by the method - attr_reader :statement - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize( - target:, - operator:, - name:, - paren:, - statement:, - location:, - comments: [] - ) - @target = target - @operator = operator - @name = name - @paren = paren - @statement = statement - @location = location - @comments = comments - end - - def accept(visitor) - visitor.visit_def_endless(self) - end - def child_nodes - [target, operator, name, paren, statement] + def ===(other) + other.is_a?(DefNode) && target === other.target && + operator === other.operator && name === other.name && + params === other.params && bodystmt === other.bodystmt end - alias deconstruct child_nodes - - def deconstruct_keys(_keys) - { - target: target, - operator: operator, - name: name, - paren: paren, - statement: statement, - location: location, - comments: comments - } - end - - def format(q) - q.group do - q.text("def ") - - if target - q.format(target) - q.format(CallOperatorFormatter.new(operator), stackable: false) - end - - q.format(name) - - if paren - params = paren - params = params.contents if params.is_a?(Paren) - q.format(paren) unless params.empty? - end - - q.text(" =") - q.group do - q.indent do - q.breakable_space - q.format(statement) - end - end - 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 end @@ -3690,10 +4180,10 @@ class Defined < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(value:, location:, comments: []) + def initialize(value:, location:) @value = value @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -3704,6 +4194,17 @@ 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) @@ -3721,65 +4222,90 @@ def format(q) end q.text(")") end + + def ===(other) + other.is_a?(Defined) && value === other.value + end end - # Defs represents defining a singleton method on an object. + # Block represents passing a block to a method call using the +do+ and +end+ + # keywords or the +{+ and +}+ operators. # - # def object.method(param) result end + # method do |value| + # end # - class Defs < Node - # [untyped] the target where the method is being defined - attr_reader :target + # 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 - # [Op | Period] the operator being used to declare the method - attr_reader :operator + # [LBrace | Keyword] the node that is being represented + attr_reader :node - # [Backtick | Const | Ident | Kw | Op] the name of the method - attr_reader :name + def initialize(text, node) + @text = text + @node = node + end - # [Params | Paren] the parameter declaration for the method - attr_reader :params + 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] the expressions to be executed by the method + # [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( - target:, - operator:, - name:, - params:, - bodystmt:, - location:, - comments: [] - ) - @target = target - @operator = operator - @name = name - @params = params + def initialize(opening:, block_var:, bodystmt:, location:) + @opening = opening + @block_var = block_var @bodystmt = bodystmt @location = location - @comments = comments + @comments = [] end def accept(visitor) - visitor.visit_defs(self) + visitor.visit_block(self) end def child_nodes - [target, operator, name, params, bodystmt] + [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) { - target: target, - operator: operator, - name: name, - params: params, + opening: opening, + block_var: block_var, bodystmt: bodystmt, location: location, comments: comments @@ -3787,115 +4313,139 @@ def deconstruct_keys(_keys) end def format(q) - q.group do - q.group do - q.text("def ") - q.format(target) - q.format(CallOperatorFormatter.new(operator), stackable: false) - q.format(name) - - if !params.is_a?(Params) || !params.empty? || params.comments.any? - q.format(params) - end + # 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 - unless bodystmt.empty? - q.indent do - q.breakable_force - q.format(bodystmt) - 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 Command, CommandCall + q.break_parent + format_break(q, break_opening, break_closing) + return + end - q.breakable_force - q.text("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 - end - # DoBlock represents passing a block to a method call using the +do+ and +end+ - # keywords. - # - # method do |value| - # end - # - class DoBlock < Node - # [Kw] the do keyword that opens this block - attr_reader :keyword - - # [nil | BlockVar] the optional variable declaration within this block - attr_reader :block_var - - # [BodyStmt] the expressions to be executed within this block - attr_reader :bodystmt - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments + def ===(other) + other.is_a?(BlockNode) && opening === other.opening && + block_var === other.block_var && bodystmt === other.bodystmt + end - def initialize(keyword:, block_var:, bodystmt:, location:, comments: []) - @keyword = keyword - @block_var = block_var - @bodystmt = bodystmt - @location = location - @comments = comments + def keywords? + opening.is_a?(Kw) end - def accept(visitor) - visitor.visit_do_block(self) + 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 - def child_nodes - [keyword, block_var, bodystmt] + # 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 - alias deconstruct child_nodes + # 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 - def deconstruct_keys(_keys) - { - keyword: keyword, - block_var: block_var, - bodystmt: bodystmt, - location: location, - comments: comments - } + previous = parent + false + end end - def format(q) - BlockFormatter.new(self, keyword, "end", bodystmt).format(q) - end - end + def format_break(q, break_opening, break_closing) + q.text(" ") + q.format(BlockOpenFormatter.new(break_opening, opening), stackable: false) - # Responsible for formatting Dot2 and Dot3 nodes. - class DotFormatter - # [String] the operator to display - attr_reader :operator + if block_var + q.text(" ") + q.format(block_var) + end - # [Dot2 | Dot3] the node that is being formatter - attr_reader :node + unless bodystmt.empty? + q.indent do + q.breakable_space + q.format(bodystmt) + end + end - def initialize(operator, node) - @operator = operator - @node = node + q.breakable_space + q.text(break_closing) end - def format(q) - left = node.left - right = node.right + def format_flat(q, flat_opening, flat_closing) + q.text(" ") + q.format(BlockOpenFormatter.new(flat_opening, opening), stackable: false) - q.format(left) if left + if block_var + q.breakable_space + q.format(block_var) + q.breakable_space + end - case q.parent - when If, IfMod, Unless, UnlessMod - q.text(" #{operator} ") + if bodystmt.empty? + q.text(" ") if flat_opening == "do" else - q.text(operator) + q.breakable_space unless block_var + q.format(bodystmt) + q.breakable_space end - q.format(right) if right + q.text(flat_closing) end end - # Dot2 represents using the .. operator between two expressions. Usually this - # is to create a range object. + # RangeNode represents using the .. or the ... operator between two + # expressions. Usually this is to create a range object. # # 1..2 # @@ -3905,87 +4455,76 @@ def format(q) # end # # One of the sides of the expression may be nil, but not both. - class Dot2 < Node + class RangeNode < Node # [nil | untyped] the left side of the expression attr_reader :left + # [Op] the operator used for this range + attr_reader :operator + # [nil | untyped] the right side of the expression attr_reader :right # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(left:, right:, location:, comments: []) + def initialize(left:, operator:, right:, location:) @left = left + @operator = operator @right = right @location = location - @comments = comments + @comments = [] end def accept(visitor) - visitor.visit_dot2(self) + visitor.visit_range(self) end def child_nodes [left, right] end - alias deconstruct child_nodes - - def deconstruct_keys(_keys) - { left: left, right: right, location: location, comments: comments } - end + def 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 + ) - def format(q) - DotFormatter.new("..", self).format(q) + node.comments.concat(comments.map(&:copy)) + node end - end - # Dot3 represents using the ... operator between two expressions. Usually this - # is to create a range object. It's effectively the same event as the Dot2 - # node but with this operator you're asking Ruby to omit the final value. - # - # 1...2 - # - # Like Dot2 it can also be used to create a flip-flop. - # - # if value == 5 ... value == 10 - # end - # - # One of the sides of the expression may be nil, but not both. - class Dot3 < Node - # [nil | untyped] the left side of the expression - attr_reader :left - - # [nil | untyped] the right side of the expression - attr_reader :right - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(left:, right:, location:, comments: []) - @left = left - @right = right - @location = location - @comments = comments - end + alias deconstruct child_nodes - def accept(visitor) - visitor.visit_dot3(self) + def deconstruct_keys(_keys) + { + left: left, + operator: operator, + right: right, + location: location, + comments: comments + } end - def child_nodes - [left, right] - end + def format(q) + q.format(left) if left - alias deconstruct child_nodes + case q.parent + when IfNode, UnlessNode + q.text(" #{operator.value} ") + else + q.text(operator.value) + end - def deconstruct_keys(_keys) - { left: left, right: right, location: location, comments: comments } + q.format(right) if right end - def format(q) - DotFormatter.new("...", self).format(q) + def ===(other) + other.is_a?(RangeNode) && left === other.left && + operator === other.operator && right === other.right end end @@ -4050,11 +4589,11 @@ class DynaSymbol < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(parts:, quote:, location:, comments: []) + def initialize(parts:, quote:, location:) @parts = parts @quote = quote @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -4065,6 +4604,18 @@ 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) @@ -4100,6 +4651,11 @@ def format(q) 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 @@ -4161,11 +4717,11 @@ class Else < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(keyword:, statements:, location:, comments: []) + def initialize(keyword:, statements:, location:) @keyword = keyword @statements = statements @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -4176,6 +4732,18 @@ 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) @@ -4199,6 +4767,11 @@ def format(q) 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. @@ -4220,18 +4793,12 @@ class Elsif < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize( - predicate:, - statements:, - consequent:, - location:, - comments: [] - ) + def initialize(predicate:, statements:, consequent:, location:) @predicate = predicate @statements = statements @consequent = consequent @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -4242,6 +4809,19 @@ 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) @@ -4276,6 +4856,11 @@ def format(q) 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. @@ -4314,6 +4899,13 @@ 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) @@ -4324,6 +4916,10 @@ def format(q) q.trim 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 @@ -4349,11 +4945,22 @@ 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 @@ -4379,11 +4986,22 @@ 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, @@ -4411,11 +5029,22 @@ 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 @@ -4435,11 +5064,11 @@ class Ensure < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(keyword:, statements:, location:, comments: []) + def initialize(keyword:, statements:, location:) @keyword = keyword @statements = statements @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -4450,6 +5079,18 @@ 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) @@ -4471,6 +5112,11 @@ def format(q) 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 @@ -4490,10 +5136,10 @@ class ExcessedComma < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(value:, location:, comments: []) + def initialize(value:, location:) @value = value @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -4504,72 +5150,29 @@ def child_nodes [] end - alias deconstruct child_nodes - - def deconstruct_keys(_keys) - { value: value, location: location, comments: comments } - end - - def format(q) - q.text(value) - end - end - - # FCall represents the piece of a method call that comes before any arguments - # (i.e., just the name of the method). It is used in places where the parser - # is sure that it is a method call and not potentially a local variable. - # - # method(argument) - # - # In the above example, it's referring to the +method+ segment. - class FCall < Node - # [Const | Ident] the name of the method - attr_reader :value - - # [nil | ArgParen | Args] the arguments to the method call - attr_reader :arguments - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(value:, arguments:, location:, comments: []) - @value = value - @arguments = arguments - @location = location - @comments = comments - end + def copy(value: nil, location: nil) + node = + ExcessedComma.new( + value: value || self.value, + location: location || self.location + ) - def accept(visitor) - visitor.visit_fcall(self) - end - - def child_nodes - [value, arguments] + node.comments.concat(comments.map(&:copy)) + node end alias deconstruct child_nodes def deconstruct_keys(_keys) - { - value: value, - arguments: arguments, - location: location, - comments: comments - } + { value: value, location: location, comments: comments } end def format(q) - q.format(value) + q.text(value) + end - if arguments.is_a?(ArgParen) && arguments.arguments.nil? && - !value.is_a?(Const) - # If you're using an explicit set of parentheses on something that looks - # like a constant, then we need to match that in order to maintain valid - # Ruby. For example, you could do something like Foo(), on which we - # would need to keep the parentheses to make it look like a method call. - else - q.format(arguments) - end + def ===(other) + other.is_a?(ExcessedComma) && value === other.value end end @@ -4591,12 +5194,12 @@ class Field < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(parent:, operator:, name:, location:, comments: []) + def initialize(parent:, operator:, name:, location:) @parent = parent @operator = operator @name = name @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -4607,6 +5210,19 @@ def child_nodes [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) @@ -4626,6 +5242,11 @@ def format(q) 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. @@ -4639,10 +5260,10 @@ class FloatLiteral < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(value:, location:, comments: []) + def initialize(value:, location:) @value = value @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -4653,6 +5274,17 @@ 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) @@ -4662,6 +5294,10 @@ def deconstruct_keys(_keys) 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 @@ -4688,13 +5324,13 @@ class FndPtn < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(constant:, left:, values:, right:, location:, comments: []) + def initialize(constant:, left:, values:, right:, location:) @constant = constant @left = left @values = values @right = right @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -4705,6 +5341,20 @@ 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) @@ -4742,6 +5392,12 @@ def format(q) 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. @@ -4763,12 +5419,12 @@ class For < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(index:, collection:, statements:, location:, comments: []) + def initialize(index:, collection:, statements:, location:) @index = index @collection = collection @statements = statements @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -4779,6 +5435,19 @@ 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) @@ -4809,6 +5478,11 @@ def format(q) 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. @@ -4822,10 +5496,10 @@ class GVar < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(value:, location:, comments: []) + def initialize(value:, location:) @value = value @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -4836,6 +5510,17 @@ 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) @@ -4845,6 +5530,10 @@ def deconstruct_keys(_keys) def format(q) q.text(value) end + + def ===(other) + other.is_a?(GVar) && value === other.value + end end # HashLiteral represents a hash literal. @@ -4881,17 +5570,17 @@ def format(q) # [LBrace] the left brace that opens this hash attr_reader :lbrace - # [Array[ AssocNew | AssocSplat ]] the optional contents of the hash + # [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:, comments: []) + def initialize(lbrace:, assocs:, location:) @lbrace = lbrace @assocs = assocs @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -4902,6 +5591,18 @@ def child_nodes [lbrace] + 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) @@ -4916,6 +5617,11 @@ def format(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 @@ -4974,20 +5680,13 @@ class Heredoc < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize( - beginning:, - ending: nil, - dedent: 0, - parts: [], - location:, - comments: [] - ) + def initialize(beginning:, ending: nil, dedent: 0, parts: [], location:) @beginning = beginning @ending = ending @dedent = dedent @parts = parts @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -4998,6 +5697,19 @@ 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) @@ -5012,7 +5724,8 @@ def deconstruct_keys(_keys) # 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) + SEPARATOR = + PrettierPrint::Breakable.new(" ", 1, indent: false, force: true).freeze def format(q) q.group do @@ -5048,6 +5761,11 @@ def format(q) 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. @@ -5064,10 +5782,10 @@ class HeredocBeg < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(value:, location:, comments: []) + def initialize(value:, location:) @value = value @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -5078,6 +5796,17 @@ 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) @@ -5087,6 +5816,10 @@ def deconstruct_keys(_keys) 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. @@ -5103,10 +5836,10 @@ class HeredocEnd < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(value:, location:, comments: []) + def initialize(value:, location:) @value = value @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -5117,6 +5850,17 @@ 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) @@ -5126,6 +5870,10 @@ def deconstruct_keys(_keys) 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+ @@ -5195,12 +5943,12 @@ def format(q) # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(constant:, keywords:, keyword_rest:, location:, comments: []) + def initialize(constant:, keywords:, keyword_rest:, location:) @constant = constant @keywords = keywords @keyword_rest = keyword_rest @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -5211,6 +5959,19 @@ 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) @@ -5266,7 +6027,7 @@ def format(q) format_contents(q, parts, nested) end - if q.target_ruby_version < Gem::Version.new("2.7.3") + if q.target_ruby_version < Formatter::SemanticVersion.new("2.7.3") q.text(" }") else q.breakable_space @@ -5275,6 +6036,15 @@ def format(q) 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) @@ -5305,10 +6075,10 @@ class Ident < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(value:, location:, comments: []) + def initialize(value:, location:) @value = value @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -5319,6 +6089,17 @@ 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) @@ -5328,6 +6109,10 @@ def deconstruct_keys(_keys) 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 @@ -5398,10 +6183,10 @@ def call(q, node) # and default instead to breaking them into multiple lines. def ternaryable?(statement) case statement - when Alias, Assign, Break, Command, CommandCall, Heredoc, If, IfMod, - IfOp, Lambda, MAssign, Next, OpAssign, RescueMod, Return, Return0, - Super, Undef, Unless, UnlessMod, Until, UntilMod, VarAlias, - VoidStmt, While, WhileMod, Yield, Yield0, ZSuper + when AliasNode, Assign, Break, Command, CommandCall, 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 @@ -5432,41 +6217,64 @@ def initialize(keyword, node) end def format(q) - # 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 node.modifier? + statement = node.statements.body[0] - # 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) } + 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 - 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) + # 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 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) } @@ -5537,7 +6345,7 @@ def contains_conditional? return false if statements.length != 1 case statements.first - when If, IfMod, IfOp, Unless, UnlessMod + when IfNode, IfOp, UnlessNode true else false @@ -5550,31 +6358,25 @@ def contains_conditional? # if predicate # end # - class If < Node + class IfNode < Node # [untyped] the expression to be checked attr_reader :predicate # [Statements] the expressions to be executed attr_reader :statements - # [nil, Elsif, Else] the next clause in the chain + # [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:, - comments: [] - ) + def initialize(predicate:, statements:, consequent:, location:) @predicate = predicate @statements = statements @consequent = consequent @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -5585,6 +6387,19 @@ 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) @@ -5600,6 +6415,16 @@ def deconstruct_keys(_keys) 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. @@ -5619,12 +6444,12 @@ class IfOp < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(predicate:, truthy:, falsy:, location:, comments: []) + def initialize(predicate:, truthy:, falsy:, location:) @predicate = predicate @truthy = truthy @falsy = falsy @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -5635,6 +6460,19 @@ 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) @@ -5649,10 +6487,9 @@ def deconstruct_keys(_keys) def format(q) force_flat = [ - Alias, Assign, Break, Command, CommandCall, Heredoc, If, IfMod, IfOp, - Lambda, MAssign, Next, OpAssign, RescueMod, Return, Return0, Super, - Undef, Unless, UnlessMod, UntilMod, VarAlias, VoidStmt, WhileMod, Yield, - Yield0, ZSuper + 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) || @@ -5664,6 +6501,11 @@ def format(q) 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) @@ -5704,94 +6546,6 @@ def format_flat(q) end end - # Formats an IfMod or UnlessMod node. - class ConditionalModFormatter - # [String] the keyword associated with this conditional - attr_reader :keyword - - # [IfMod | UnlessMod] the node that is being formatted - attr_reader :node - - def initialize(keyword, node) - @keyword = keyword - @node = node - end - - def format(q) - if ContainsAssignment.call(node.statement) || q.parent.is_a?(In) - q.group { format_flat(q) } - else - q.group { q.if_break { format_break(q) }.if_flat { format_flat(q) } } - end - end - - private - - def format_break(q) - q.text("#{keyword} ") - q.nest(keyword.length + 1) { q.format(node.predicate) } - q.indent do - q.breakable_space - q.format(node.statement) - end - q.breakable_space - q.text("end") - end - - def format_flat(q) - Parentheses.flat(q) do - q.format(node.statement) - q.text(" #{keyword} ") - q.format(node.predicate) - end - end - end - - # IfMod represents the modifier form of an +if+ statement. - # - # expression if predicate - # - class IfMod < Node - # [untyped] the expression to be executed - attr_reader :statement - - # [untyped] the expression to be checked - attr_reader :predicate - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(statement:, predicate:, location:, comments: []) - @statement = statement - @predicate = predicate - @location = location - @comments = comments - end - - def accept(visitor) - visitor.visit_if_mod(self) - end - - def child_nodes - [statement, predicate] - end - - alias deconstruct child_nodes - - def deconstruct_keys(_keys) - { - statement: statement, - predicate: predicate, - location: location, - comments: comments - } - end - - def format(q) - ConditionalModFormatter.new("if", self).format(q) - end - end - # Imaginary represents an imaginary number literal. # # 1i @@ -5803,10 +6557,10 @@ class Imaginary < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(value:, location:, comments: []) + def initialize(value:, location:) @value = value @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -5817,6 +6571,17 @@ 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) @@ -5826,6 +6591,10 @@ def deconstruct_keys(_keys) 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 @@ -5848,12 +6617,12 @@ class In < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(pattern:, statements:, consequent:, location:, comments: []) + def initialize(pattern:, statements:, consequent:, location:) @pattern = pattern @statements = statements @consequent = consequent @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -5864,6 +6633,19 @@ 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) @@ -5896,6 +6678,11 @@ def format(q) 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. @@ -5909,10 +6696,10 @@ class Int < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(value:, location:, comments: []) + def initialize(value:, location:) @value = value @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -5923,6 +6710,14 @@ 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) @@ -5940,6 +6735,10 @@ def format(q) q.text(value) end end + + def ===(other) + other.is_a?(Int) && value === other.value + end end # IVar represents an instance variable literal. @@ -5953,10 +6752,10 @@ class IVar < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(value:, location:, comments: []) + def initialize(value:, location:) @value = value @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -5967,6 +6766,17 @@ 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) @@ -5976,6 +6786,10 @@ def deconstruct_keys(_keys) 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 @@ -6001,11 +6815,11 @@ class Kw < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(value:, location:, comments: []) + def initialize(value:, location:) @value = value @name = value.to_sym @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -6016,6 +6830,14 @@ 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) @@ -6025,6 +6847,10 @@ def deconstruct_keys(_keys) 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 @@ -6039,10 +6865,10 @@ class KwRestParam < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(name:, location:, comments: []) + def initialize(name:, location:) @name = name @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -6053,6 +6879,17 @@ 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) @@ -6063,6 +6900,10 @@ 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 @@ -6085,10 +6926,10 @@ class Label < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(value:, location:, comments: []) + def initialize(value:, location:) @value = value @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -6099,6 +6940,17 @@ 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) @@ -6108,6 +6960,10 @@ def deconstruct_keys(_keys) 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. @@ -6134,11 +6990,22 @@ 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). @@ -6155,11 +7022,11 @@ class Lambda < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(params:, statements:, location:, comments: []) + def initialize(params:, statements:, location:) @params = params @statements = statements @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -6170,6 +7037,18 @@ 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) @@ -6243,6 +7122,11 @@ def format(q) 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 @@ -6263,11 +7147,11 @@ class LambdaVar < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(params:, locals:, location:, comments: []) + def initialize(params:, locals:, location:) @params = params @locals = locals @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -6278,6 +7162,18 @@ 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) @@ -6296,6 +7192,11 @@ def format(q) 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., {. @@ -6306,10 +7207,10 @@ class LBrace < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(value:, location:, comments: []) + def initialize(value:, location:) @value = value @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -6320,6 +7221,17 @@ 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) @@ -6329,6 +7241,19 @@ def deconstruct_keys(_keys) 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., [. @@ -6339,10 +7264,10 @@ class LBracket < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(value:, location:, comments: []) + def initialize(value:, location:) @value = value @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -6353,6 +7278,17 @@ 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) @@ -6362,6 +7298,19 @@ def deconstruct_keys(_keys) 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., (. @@ -6372,10 +7321,10 @@ class LParen < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(value:, location:, comments: []) + def initialize(value:, location:) @value = value @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -6386,6 +7335,17 @@ 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) @@ -6395,6 +7355,19 @@ def deconstruct_keys(_keys) 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 @@ -6421,11 +7394,11 @@ class MAssign < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(target:, value:, location:, comments: []) + def initialize(target:, value:, location:) @target = target @value = value @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -6436,6 +7409,18 @@ 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) @@ -6452,6 +7437,10 @@ def format(q) 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. @@ -6459,20 +7448,20 @@ def format(q) # method {} # class MethodAddBlock < Node - # [Call | Command | CommandCall | FCall] the method call + # [Call | Command | CommandCall] the method call attr_reader :call - # [BraceBlock | DoBlock] the block being sent with the method call + # [Block] 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:, comments: []) + def initialize(call:, block:, location:) @call = call @block = block @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -6483,6 +7472,18 @@ 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) @@ -6505,6 +7506,11 @@ def format(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) @@ -6529,11 +7535,11 @@ class MLHS < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(parts:, comma: false, location:, comments: []) + def initialize(parts:, comma: false, location:) @parts = parts @comma = comma @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -6544,6 +7550,18 @@ 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) @@ -6554,6 +7572,11 @@ 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 @@ -6573,11 +7596,11 @@ class MLHSParen < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(contents:, comma: false, location:, comments: []) + def initialize(contents:, comma: false, location:) @contents = contents @comma = comma @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -6588,6 +7611,17 @@ 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) @@ -6614,6 +7648,10 @@ def format(q) q.text(")") end end + + def ===(other) + other.is_a?(MLHSParen) && contents === other.contents + end end # ModuleDeclaration represents defining a module using the +module+ keyword. @@ -6631,11 +7669,11 @@ class ModuleDeclaration < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(constant:, bodystmt:, location:, comments: []) + def initialize(constant:, bodystmt:, location:) @constant = constant @bodystmt = bodystmt @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -6646,6 +7684,18 @@ 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) @@ -6679,6 +7729,11 @@ def format(q) end end + def ===(other) + other.is_a?(ModuleDeclaration) && constant === other.constant && + bodystmt === other.bodystmt + end + private def format_declaration(q) @@ -6701,10 +7756,10 @@ class MRHS < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(parts:, location:, comments: []) + def initialize(parts:, location:) @parts = parts @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -6715,6 +7770,17 @@ 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) @@ -6724,6 +7790,10 @@ def deconstruct_keys(_keys) 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. @@ -6750,10 +7820,10 @@ class Next < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(arguments:, location:, comments: []) + def initialize(arguments:, location:) @arguments = arguments @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -6764,6 +7834,17 @@ 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) @@ -6773,6 +7854,10 @@ def deconstruct_keys(_keys) 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. @@ -6790,11 +7875,11 @@ class Op < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(value:, location:, comments: []) + def initialize(value:, location:) @value = value @name = value.to_sym @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -6805,6 +7890,14 @@ 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) @@ -6814,6 +7907,10 @@ def deconstruct_keys(_keys) 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 @@ -6835,12 +7932,12 @@ class OpAssign < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(target:, operator:, value:, location:, comments: []) + def initialize(target:, operator:, value:, location:) @target = target @operator = operator @value = value @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -6851,6 +7948,19 @@ 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) @@ -6881,6 +7991,11 @@ def format(q) end end + def ===(other) + other.is_a?(OpAssign) && target === other.target && + operator === other.operator && value === other.value + end + private def skip_indent? @@ -6926,7 +8041,7 @@ module Parentheses Assign, Assoc, Binary, - Call, + CallNode, Defined, MAssign, OpAssign @@ -7070,8 +8185,7 @@ def initialize( keywords: [], keyword_rest: nil, block: nil, - location:, - comments: [] + location: ) @requireds = requireds @optionals = optionals @@ -7081,7 +8195,7 @@ def initialize( @keyword_rest = keyword_rest @block = block @location = location - @comments = comments + @comments = [] end # Params nodes are the most complicated in the tree. Occasionally you want @@ -7109,6 +8223,32 @@ def child_nodes ] 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) @@ -7145,8 +8285,7 @@ def format(q) return end - case q.parent - when Def, Defs, DefEndless + if q.parent.is_a?(DefNode) q.nest(0) do q.text("(") q.group do @@ -7163,6 +8302,20 @@ def format(q) 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 + private def format_contents(q, parts) @@ -7187,11 +8340,11 @@ class Paren < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(lparen:, contents:, location:, comments: []) + def initialize(lparen:, contents:, location:) @lparen = lparen @contents = contents @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -7202,6 +8355,18 @@ 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) @@ -7228,6 +8393,11 @@ def format(q) 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 @@ -7239,10 +8409,10 @@ class Period < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(value:, location:, comments: []) + def initialize(value:, location:) @value = value @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -7253,6 +8423,17 @@ 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) @@ -7262,6 +8443,10 @@ def deconstruct_keys(_keys) def format(q) q.text(value) end + + def ===(other) + other.is_a?(Period) && value === other.value + end end # Program represents the overall syntax tree. @@ -7272,10 +8457,10 @@ class Program < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(statements:, location:, comments: []) + def initialize(statements:, location:) @statements = statements @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -7286,6 +8471,17 @@ 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) @@ -7300,6 +8496,10 @@ def format(q) # 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. @@ -7316,11 +8516,11 @@ class QSymbols < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(beginning:, elements:, location:, comments: []) + def initialize(beginning:, elements:, location:) @beginning = beginning @elements = elements @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -7331,6 +8531,18 @@ 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) @@ -7363,6 +8575,11 @@ def format(q) 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. @@ -7389,11 +8606,22 @@ 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. @@ -7410,11 +8638,11 @@ class QWords < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(beginning:, elements:, location:, comments: []) + def initialize(beginning:, elements:, location:) @beginning = beginning @elements = elements @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -7425,6 +8653,14 @@ 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) @@ -7457,6 +8693,11 @@ def format(q) 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. @@ -7483,11 +8724,22 @@ 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. @@ -7501,10 +8753,10 @@ class RationalLiteral < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(value:, location:, comments: []) + def initialize(value:, location:) @value = value @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -7515,6 +8767,17 @@ 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) @@ -7524,6 +8787,10 @@ def deconstruct_keys(_keys) 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., +++. @@ -7544,11 +8811,22 @@ 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., +]+. @@ -7569,11 +8847,22 @@ 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. @@ -7581,16 +8870,12 @@ def deconstruct_keys(_keys) # redo # class Redo < Node - # [String] the value of the keyword - attr_reader :value - # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(value:, location:, comments: []) - @value = value + def initialize(location:) @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -7601,14 +8886,25 @@ 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) - { value: value, location: location, comments: comments } + { location: location, comments: comments } end def format(q) - q.text(value) + q.text("redo") + end + + def ===(other) + other.is_a?(Redo) end end @@ -7640,11 +8936,24 @@ 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. @@ -7673,11 +8982,22 @@ 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. @@ -7707,11 +9027,22 @@ 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. @@ -7732,12 +9063,12 @@ class RegexpLiteral < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(beginning:, ending:, parts:, location:, comments: []) + def initialize(beginning:, ending:, parts:, location:) @beginning = beginning @ending = ending @parts = parts @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -7748,6 +9079,19 @@ 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) @@ -7801,6 +9145,12 @@ def format(q) 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 @@ -7843,11 +9193,11 @@ class RescueEx < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(exceptions:, variable:, location:, comments: []) + def initialize(exceptions:, variable:, location:) @exceptions = exceptions @variable = variable @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -7858,6 +9208,18 @@ 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) @@ -7882,6 +9244,11 @@ def format(q) 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. @@ -7906,20 +9273,13 @@ class Rescue < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize( - keyword:, - exception:, - statements:, - consequent:, - location:, - comments: [] - ) + def initialize(keyword:, exception:, statements:, consequent:, location:) @keyword = keyword @exception = exception @statements = statements @consequent = consequent @location = location - @comments = comments + @comments = [] end def bind_end(end_char, end_column) @@ -7952,6 +9312,26 @@ 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) @@ -7988,6 +9368,12 @@ def format(q) 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. @@ -8004,11 +9390,11 @@ class RescueMod < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(statement:, value:, location:, comments: []) + def initialize(statement:, value:, location:) @statement = statement @value = value @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -8019,6 +9405,18 @@ 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) @@ -8047,6 +9445,11 @@ def format(q) 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 @@ -8061,10 +9464,10 @@ class RestParam < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(name:, location:, comments: []) + def initialize(name:, location:) @name = name @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -8075,6 +9478,17 @@ 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) @@ -8085,6 +9499,10 @@ 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. @@ -8092,16 +9510,12 @@ def format(q) # retry # class Retry < Node - # [String] the value of the keyword - attr_reader :value - # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(value:, location:, comments: []) - @value = value + def initialize(location:) @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -8112,14 +9526,25 @@ 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) - { value: value, location: location, comments: comments } + { location: location, comments: comments } end def format(q) - q.text(value) + q.text("retry") + end + + def ===(other) + other.is_a?(Retry) end end @@ -8127,17 +9552,17 @@ def format(q) # # return value # - class Return < Node - # [Args] the arguments being passed to the keyword + 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:, comments: []) + def initialize(arguments:, location:) @arguments = arguments @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -8148,6 +9573,17 @@ 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) @@ -8157,41 +9593,9 @@ def deconstruct_keys(_keys) def format(q) FlowControlFormatter.new("return", self).format(q) end - end - - # Return0 represents the bare +return+ keyword with no arguments. - # - # return - # - class Return0 < Node - # [String] the value of the keyword - attr_reader :value - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(value:, location:, comments: []) - @value = value - @location = location - @comments = comments - end - - def accept(visitor) - visitor.visit_return0(self) - end - - def child_nodes - [] - end - - alias deconstruct child_nodes - - def deconstruct_keys(_keys) - { value: value, location: location, comments: comments } - end - def format(q) - q.text(value) + def ===(other) + other.is_a?(ReturnNode) && arguments === other.arguments end end @@ -8213,11 +9617,22 @@ 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 @@ -8237,11 +9652,11 @@ class SClass < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(target:, bodystmt:, location:, comments: []) + def initialize(target:, bodystmt:, location:) @target = target @bodystmt = bodystmt @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -8252,6 +9667,18 @@ 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) @@ -8275,6 +9702,11 @@ def format(q) 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. @@ -8294,11 +9726,11 @@ class Statements < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(parser, body:, location:, comments: []) + def initialize(parser, body:, location:) @parser = parser @body = body @location = location - @comments = comments + @comments = [] end def bind(start_char, start_column, end_char, end_column) @@ -8356,6 +9788,18 @@ def child_nodes body end + def copy(body: nil, location: nil) + node = + Statements.new( + parser, + body: body || self.body, + location: location || self.location + ) + + node.comments.concat(comments.map(&:copy)) + node + end + alias deconstruct child_nodes def deconstruct_keys(_keys) @@ -8410,6 +9854,10 @@ def format(q) 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 @@ -8475,11 +9923,22 @@ 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 end # StringConcat represents concatenating two strings together using a backward @@ -8498,11 +9957,11 @@ class StringConcat < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(left:, right:, location:, comments: []) + def initialize(left:, right:, location:) @left = left @right = right @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -8513,6 +9972,18 @@ 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) @@ -8529,6 +10000,10 @@ def format(q) 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. @@ -8544,18 +10019,29 @@ class StringDVar < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(variable:, location:, comments: []) + def initialize(variable:, location:) @variable = variable @location = location - @comments = comments + @comments = [] end def accept(visitor) visitor.visit_string_dvar(self) end - def child_nodes - [variable] + 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 @@ -8569,6 +10055,10 @@ def format(q) 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 @@ -8584,10 +10074,10 @@ class StringEmbExpr < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(statements:, location:, comments: []) + def initialize(statements:, location:) @statements = statements @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -8598,6 +10088,17 @@ 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) @@ -8629,6 +10130,10 @@ def format(q) end end end + + def ===(other) + other.is_a?(StringEmbExpr) && statements === other.statements + end end # StringLiteral represents a string literal. @@ -8646,11 +10151,11 @@ class StringLiteral < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(parts:, quote:, location:, comments: []) + def initialize(parts:, quote:, location:) @parts = parts @quote = quote @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -8661,6 +10166,18 @@ 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) @@ -8707,6 +10224,11 @@ def format(q) 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 @@ -8721,10 +10243,10 @@ class Super < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(arguments:, location:, comments: []) + def initialize(arguments:, location:) @arguments = arguments @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -8735,6 +10257,17 @@ 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) @@ -8753,6 +10286,10 @@ def format(q) end end end + + def ===(other) + other.is_a?(Super) && arguments === other.arguments + end end # SymBeg represents the beginning of a symbol literal. @@ -8788,11 +10325,22 @@ 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 @@ -8818,11 +10366,22 @@ 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 @@ -8838,10 +10397,10 @@ class SymbolLiteral < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(value:, location:, comments: []) + def initialize(value:, location:) @value = value @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -8852,6 +10411,17 @@ 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) @@ -8862,6 +10432,10 @@ def format(q) q.text(":") q.format(value) end + + def ===(other) + other.is_a?(SymbolLiteral) && value === other.value + end end # Symbols represents a symbol array literal with interpolation. @@ -8878,11 +10452,11 @@ class Symbols < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(beginning:, elements:, location:, comments: []) + def initialize(beginning:, elements:, location:) @beginning = beginning @elements = elements @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -8893,6 +10467,14 @@ 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) @@ -8925,6 +10507,11 @@ def format(q) 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 @@ -8952,11 +10539,22 @@ 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. @@ -8981,11 +10579,22 @@ 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 @@ -9011,11 +10620,22 @@ 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 @@ -9031,10 +10651,10 @@ class TopConstField < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(constant:, location:, comments: []) + def initialize(constant:, location:) @constant = constant @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -9045,6 +10665,17 @@ 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) @@ -9055,6 +10686,10 @@ 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 @@ -9069,10 +10704,10 @@ class TopConstRef < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(constant:, location:, comments: []) + def initialize(constant:, location:) @constant = constant @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -9083,6 +10718,17 @@ 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) @@ -9093,6 +10739,10 @@ 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. @@ -9122,11 +10772,22 @@ 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 @@ -9144,10 +10805,10 @@ class TStringContent < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(value:, location:, comments: []) + def initialize(value:, location:) @value = value @location = location - @comments = comments + @comments = [] end def match?(pattern) @@ -9162,6 +10823,17 @@ 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) @@ -9171,6 +10843,10 @@ def deconstruct_keys(_keys) 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. @@ -9200,11 +10876,22 @@ 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. @@ -9222,11 +10909,11 @@ class Not < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(statement:, parentheses:, location:, comments: []) + def initialize(statement:, parentheses:, location:) @statement = statement @parentheses = parentheses @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -9237,6 +10924,18 @@ 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) @@ -9258,7 +10957,7 @@ def format(q) else grandparent = q.grandparent ternary = - (grandparent.is_a?(If) || grandparent.is_a?(Unless)) && + (grandparent.is_a?(IfNode) || grandparent.is_a?(UnlessNode)) && Ternaryable.call(q, grandparent) if ternary @@ -9271,6 +10970,11 @@ def format(q) 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 @@ -9288,11 +10992,11 @@ class Unary < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(operator:, statement:, location:, comments: []) + def initialize(operator:, statement:, location:) @operator = operator @statement = statement @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -9303,6 +11007,18 @@ 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) @@ -9318,6 +11034,11 @@ 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. @@ -9355,10 +11076,10 @@ def format(q) # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(symbols:, location:, comments: []) + def initialize(symbols:, location:) @symbols = symbols @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -9369,6 +11090,17 @@ 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) @@ -9386,6 +11118,10 @@ def format(q) 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. @@ -9393,31 +11129,25 @@ def format(q) # unless predicate # end # - class Unless < Node + class UnlessNode < Node # [untyped] the expression to be checked attr_reader :predicate # [Statements] the expressions to be executed attr_reader :statements - # [nil, Elsif, Else] the next clause in the chain + # [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:, - comments: [] - ) + def initialize(predicate:, statements:, consequent:, location:) @predicate = predicate @statements = statements @consequent = consequent @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -9428,6 +11158,19 @@ 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) @@ -9443,199 +11186,29 @@ def deconstruct_keys(_keys) def format(q) ConditionalFormatter.new("unless", self).format(q) end - end - - # UnlessMod represents the modifier form of an +unless+ statement. - # - # expression unless predicate - # - class UnlessMod < Node - # [untyped] the expression to be executed - attr_reader :statement - - # [untyped] the expression to be checked - attr_reader :predicate - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(statement:, predicate:, location:, comments: []) - @statement = statement - @predicate = predicate - @location = location - @comments = comments - end - - def accept(visitor) - visitor.visit_unless_mod(self) - end - - def child_nodes - [statement, predicate] - end - alias deconstruct child_nodes - - def deconstruct_keys(_keys) - { - statement: statement, - predicate: predicate, - location: location, - comments: comments - } + def ===(other) + other.is_a?(UnlessNode) && predicate === other.predicate && + statements === other.statements && consequent === other.consequent end - def format(q) - ConditionalModFormatter.new("unless", self).format(q) + # 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, UntilMod, While, or WhileMod node. + # Formats an Until or While node. class LoopFormatter # [String] the name of the keyword used for this loop attr_reader :keyword - # [Until | UntilMod | While | WhileMod] the node that is being formatted + # [Until | While] the node that is being formatted attr_reader :node - # [untyped] the statements associated with the node - attr_reader :statements - - def initialize(keyword, node, statements) - @keyword = keyword - @node = node - @statements = statements - end - - def format(q) - if ContainsAssignment.call(node.predicate) - format_break(q) - q.break_parent - return - end - - q.group do - q - .if_break { format_break(q) } - .if_flat do - Parentheses.flat(q) do - q.format(statements) - q.text(" #{keyword} ") - q.format(node.predicate) - end - end - end - end - - private - - def format_break(q) - q.text("#{keyword} ") - q.nest(keyword.length + 1) { q.format(node.predicate) } - q.indent do - q.breakable_empty - q.format(statements) - end - q.breakable_empty - q.text("end") - end - end - - # Until represents an +until+ loop. - # - # until predicate - # end - # - class Until < Node - # [untyped] the expression to be checked - attr_reader :predicate - - # [Statements] the expressions to be executed - attr_reader :statements - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(predicate:, statements:, location:, comments: []) - @predicate = predicate - @statements = statements - @location = location - @comments = comments - end - - def accept(visitor) - visitor.visit_until(self) - end - - def child_nodes - [predicate, statements] - end - - alias deconstruct child_nodes - - def deconstruct_keys(_keys) - { - predicate: predicate, - statements: statements, - location: location, - comments: comments - } - end - - def format(q) - if statements.empty? - keyword = "until " - - q.group do - q.text(keyword) - q.nest(keyword.length) { q.format(predicate) } - q.breakable_force - q.text("end") - end - else - LoopFormatter.new("until", self, statements).format(q) - end - end - end - - # UntilMod represents the modifier form of a +until+ loop. - # - # expression until predicate - # - class UntilMod < Node - # [untyped] the expression to be executed - attr_reader :statement - - # [untyped] the expression to be checked - attr_reader :predicate - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(statement:, predicate:, location:, comments: []) - @statement = statement - @predicate = predicate - @location = location - @comments = comments - end - - def accept(visitor) - visitor.visit_until_mod(self) - end - - def child_nodes - [statement, predicate] - end - - alias deconstruct child_nodes - - def deconstruct_keys(_keys) - { - statement: statement, - predicate: predicate, - location: location, - comments: comments - } + def initialize(keyword, node) + @keyword = keyword + @node = node end def format(q) @@ -9645,67 +11218,122 @@ def format(q) # # begin # foo - # end until bar + # 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 until foo + # foo = bar while foo # - if statement.is_a?(Begin) || ContainsAssignment.call(statement) + if node.modifier? && (statement = node.statements.body.first) && + (statement.is_a?(Begin) || ContainsAssignment.call(statement)) q.format(statement) - q.text(" until ") - q.format(predicate) + 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 - LoopFormatter.new("until", self, statement).format(q) + 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 - # VarAlias represents when you're using the +alias+ keyword with global - # variable arguments. + # Until represents an +until+ loop. # - # alias $new $old + # until predicate + # end # - class VarAlias < Node - # [GVar] the new alias of the variable - attr_reader :left + class UntilNode < Node + # [untyped] the expression to be checked + attr_reader :predicate - # [Backref | GVar] the current name of the variable to be aliased - attr_reader :right + # [Statements] the expressions to be executed + attr_reader :statements # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(left:, right:, location:, comments: []) - @left = left - @right = right + def initialize(predicate:, statements:, location:) + @predicate = predicate + @statements = statements @location = location - @comments = comments + @comments = [] end def accept(visitor) - visitor.visit_var_alias(self) + visitor.visit_until(self) end def child_nodes - [left, right] + [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) - { left: left, right: right, location: location, comments: comments } + { + predicate: predicate, + statements: statements, + location: location, + comments: comments + } end def format(q) - keyword = "alias " + LoopFormatter.new("until", self).format(q) + end - q.text(keyword) - q.format(left) - q.text(" ") - q.format(right) + 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 @@ -9722,10 +11350,10 @@ class VarField < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(value:, location:, comments: []) + def initialize(value:, location:) @value = value @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -9736,6 +11364,17 @@ def child_nodes [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) @@ -9749,6 +11388,10 @@ def format(q) q.format(value) end end + + def ===(other) + other.is_a?(VarField) && value === other.value + end end # VarRef represents a variable reference. @@ -9766,10 +11409,10 @@ class VarRef < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(value:, location:, comments: []) + def initialize(value:, location:) @value = value @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -9780,6 +11423,17 @@ 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) @@ -9790,6 +11444,10 @@ 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, @@ -9830,10 +11488,10 @@ class PinnedVarRef < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(value:, location:, comments: []) + def initialize(value:, location:) @value = value @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -9844,6 +11502,17 @@ 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) @@ -9856,6 +11525,10 @@ def format(q) 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 @@ -9870,10 +11543,10 @@ class VCall < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(value:, location:, comments: []) + def initialize(value:, location:) @value = value @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -9884,6 +11557,17 @@ 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) @@ -9894,6 +11578,10 @@ 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 @@ -9910,9 +11598,9 @@ class VoidStmt < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(location:, comments: []) + def initialize(location:) @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -9923,6 +11611,13 @@ 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) @@ -9931,6 +11626,10 @@ def deconstruct_keys(_keys) def format(q) end + + def ===(other) + other.is_a?(VoidStmt) + end end # When represents a +when+ clause in a +case+ chain. @@ -9952,18 +11651,12 @@ class When < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize( - arguments:, - statements:, - consequent:, - location:, - comments: [] - ) + def initialize(arguments:, statements:, consequent:, location:) @arguments = arguments @statements = statements @consequent = consequent @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -9974,6 +11667,19 @@ 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) @@ -10000,7 +11706,7 @@ def call(q) # 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 + SEPARATOR = Separator.new.freeze def format(q) keyword = "when " @@ -10019,9 +11725,7 @@ def format(q) # last argument to the predicate is and endless range, then you are # forced to use the "then" keyword to make it parse properly. last = arguments.parts.last - if (last.is_a?(Dot2) || last.is_a?(Dot3)) && !last.right - q.text(" then") - end + q.text(" then") if last.is_a?(RangeNode) && !last.right end end @@ -10038,6 +11742,11 @@ def format(q) 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. @@ -10045,7 +11754,7 @@ def format(q) # while predicate # end # - class While < Node + class WhileNode < Node # [untyped] the expression to be checked attr_reader :predicate @@ -10055,11 +11764,11 @@ class While < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(predicate:, statements:, location:, comments: []) + def initialize(predicate:, statements:, location:) @predicate = predicate @statements = statements @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -10070,6 +11779,18 @@ 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) @@ -10082,83 +11803,16 @@ def deconstruct_keys(_keys) end def format(q) - if statements.empty? - keyword = "while " - - q.group do - q.text(keyword) - q.nest(keyword.length) { q.format(predicate) } - q.breakable_force - q.text("end") - end - else - LoopFormatter.new("while", self, statements).format(q) - end - end - end - - # WhileMod represents the modifier form of a +while+ loop. - # - # expression while predicate - # - class WhileMod < Node - # [untyped] the expression to be executed - attr_reader :statement - - # [untyped] the expression to be checked - attr_reader :predicate - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(statement:, predicate:, location:, comments: []) - @statement = statement - @predicate = predicate - @location = location - @comments = comments - end - - def accept(visitor) - visitor.visit_while_mod(self) - end - - def child_nodes - [statement, predicate] + LoopFormatter.new("while", self).format(q) end - alias deconstruct child_nodes - - def deconstruct_keys(_keys) - { - statement: statement, - predicate: predicate, - location: location, - comments: comments - } + def ===(other) + other.is_a?(WhileNode) && predicate === other.predicate && + statements === other.statements end - def format(q) - # If we're in the modifier form and we're modifying a `begin`, then this - # is a special case where we need to explicitly use the modifier form - # because otherwise the semantic meaning changes. This looks like: - # - # begin - # foo - # end while bar - # - # Also, if the statement of the modifier includes an assignment, then we - # can't know for certain that it won't impact the predicate, so we need to - # force it to stay as it is. This looks like: - # - # foo = bar while foo - # - if statement.is_a?(Begin) || ContainsAssignment.call(statement) - q.format(statement) - q.text(" while ") - q.format(predicate) - else - LoopFormatter.new("while", self, statement).format(q) - end + def modifier? + predicate.location.start_char > statements.location.start_char end end @@ -10177,10 +11831,10 @@ class Word < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(parts:, location:, comments: []) + def initialize(parts:, location:) @parts = parts @location = location - @comments = comments + @comments = [] end def match?(pattern) @@ -10195,6 +11849,17 @@ 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) @@ -10204,6 +11869,10 @@ def deconstruct_keys(_keys) 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. @@ -10220,11 +11889,11 @@ class Words < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(beginning:, elements:, location:, comments: []) + def initialize(beginning:, elements:, location:) @beginning = beginning @elements = elements @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -10235,6 +11904,14 @@ 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) @@ -10267,6 +11944,11 @@ def format(q) 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 @@ -10294,11 +11976,22 @@ 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. @@ -10323,11 +12016,22 @@ 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. @@ -10342,10 +12046,10 @@ class XStringLiteral < Node # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(parts:, location:, comments: []) + def initialize(parts:, location:) @parts = parts @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -10356,6 +12060,17 @@ 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) @@ -10367,23 +12082,27 @@ def format(q) 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 Yield < Node - # [Args | Paren] the arguments passed to the yield + 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:, comments: []) + def initialize(arguments:, location:) @arguments = arguments @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -10394,6 +12113,17 @@ 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) @@ -10401,6 +12131,11 @@ def deconstruct_keys(_keys) end def format(q) + if arguments.nil? + q.text("yield") + return + end + q.group do q.text("yield") @@ -10417,41 +12152,9 @@ def format(q) end end end - end - - # Yield0 represents the bare +yield+ keyword with no arguments. - # - # yield - # - class Yield0 < Node - # [String] the value of the keyword - attr_reader :value - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(value:, location:, comments: []) - @value = value - @location = location - @comments = comments - end - - def accept(visitor) - visitor.visit_yield0(self) - end - - def child_nodes - [] - end - - alias deconstruct child_nodes - - def deconstruct_keys(_keys) - { value: value, location: location, comments: comments } - end - def format(q) - q.text(value) + def ===(other) + other.is_a?(YieldNode) && arguments === other.arguments end end @@ -10460,16 +12163,12 @@ def format(q) # super # class ZSuper < Node - # [String] the value of the keyword - attr_reader :value - # [Array[ Comment | EmbDoc ]] the comments attached to this node attr_reader :comments - def initialize(value:, location:, comments: []) - @value = value + def initialize(location:) @location = location - @comments = comments + @comments = [] end def accept(visitor) @@ -10480,14 +12179,25 @@ 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) - { value: value, location: location, comments: comments } + { location: location, comments: comments } end def format(q) - q.text(value) + 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 index 61a7ca57..23a3196c 100644 --- a/lib/syntax_tree/parser.rb +++ b/lib/syntax_tree/parser.rb @@ -421,11 +421,11 @@ def on___end__(value) # on_alias: ( # (DynaSymbol | SymbolLiteral) left, # (DynaSymbol | SymbolLiteral) right - # ) -> Alias + # ) -> AliasNode def on_alias(left, right) keyword = consume_keyword(:alias) - Alias.new( + AliasNode.new( left: left, right: right, location: keyword.location.to(right.location) @@ -575,7 +575,7 @@ def on_args_add_star(arguments, argument) def on_args_forward op = consume_operator(:"...") - ArgsForward.new(value: op.value, location: op.location) + ArgsForward.new(location: op.location) end # :call-seq: @@ -920,7 +920,7 @@ def on_bodystmt(statements, rescue_clause, else_clause, ensure_clause) # on_brace_block: ( # (nil | BlockVar) block_var, # Statements statements - # ) -> BraceBlock + # ) -> BlockNode def on_brace_block(block_var, statements) lbrace = consume_token(LBrace) rbrace = consume_token(RBrace) @@ -947,10 +947,10 @@ def on_brace_block(block_var, statements) end_column: rbrace.location.end_column ) - BraceBlock.new( - lbrace: lbrace, + BlockNode.new( + opening: lbrace, block_var: block_var, - statements: statements, + bodystmt: statements, location: location ) end @@ -971,7 +971,7 @@ def on_break(arguments) # untyped receiver, # (:"::" | Op | Period) operator, # (:call | Backtick | Const | Ident | Op) message - # ) -> Call + # ) -> CallNode def on_call(receiver, operator, message) ending = if message != :call @@ -982,7 +982,7 @@ def on_call(receiver, operator, message) receiver end - Call.new( + CallNode.new( receiver: receiver, operator: operator, message: message, @@ -1076,6 +1076,7 @@ def on_command(message, arguments) Command.new( message: message, arguments: arguments, + block: nil, location: message.location.to(arguments.location) ) end @@ -1095,6 +1096,7 @@ def on_command_call(receiver, operator, message, arguments) operator: operator, message: message, arguments: arguments, + block: nil, location: receiver.location.to(ending.location) ) end @@ -1181,7 +1183,7 @@ def on_cvar(value) # (Backtick | Const | Ident | Kw | Op) name, # (nil | Params | Paren) params, # untyped bodystmt - # ) -> Def | DefEndless + # ) -> 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 @@ -1223,7 +1225,9 @@ def on_def(name, params, bodystmt) ending.location.start_column ) - Def.new( + DefNode.new( + target: nil, + operator: nil, name: name, params: params, bodystmt: bodystmt, @@ -1234,12 +1238,12 @@ def on_def(name, params, bodystmt) # the statements list. Before, it was just the individual statement. statement = bodystmt.is_a?(BodyStmt) ? bodystmt.statements : bodystmt - DefEndless.new( + DefNode.new( target: nil, operator: nil, name: name, - paren: params, - statement: statement, + params: params, + bodystmt: statement, location: beginning.location.to(bodystmt.location) ) end @@ -1270,7 +1274,7 @@ def on_defined(value) # (Backtick | Const | Ident | Kw | Op) name, # (Params | Paren) params, # BodyStmt bodystmt - # ) -> Defs + # ) -> 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 @@ -1309,7 +1313,7 @@ def on_defs(target, operator, name, params, bodystmt) ending.location.start_column ) - Defs.new( + DefNode.new( target: target, operator: operator, name: name, @@ -1322,19 +1326,19 @@ def on_defs(target, operator, name, params, bodystmt) # the statements list. Before, it was just the individual statement. statement = bodystmt.is_a?(BodyStmt) ? bodystmt.statements : bodystmt - DefEndless.new( + DefNode.new( target: target, operator: operator, name: name, - paren: params, - statement: statement, + params: params, + bodystmt: statement, location: beginning.location.to(bodystmt.location) ) end end # :call-seq: - # on_do_block: (BlockVar block_var, BodyStmt bodystmt) -> DoBlock + # on_do_block: (BlockVar block_var, BodyStmt bodystmt) -> BlockNode def on_do_block(block_var, bodystmt) beginning = consume_keyword(:do) ending = consume_keyword(:end) @@ -1348,8 +1352,8 @@ def on_do_block(block_var, bodystmt) ending.location.start_column ) - DoBlock.new( - keyword: beginning, + BlockNode.new( + opening: beginning, block_var: block_var, bodystmt: bodystmt, location: beginning.location.to(ending.location) @@ -1357,30 +1361,32 @@ def on_do_block(block_var, bodystmt) end # :call-seq: - # on_dot2: ((nil | untyped) left, (nil | untyped) right) -> Dot2 + # on_dot2: ((nil | untyped) left, (nil | untyped) right) -> RangeNode def on_dot2(left, right) operator = consume_operator(:"..") beginning = left || operator ending = right || operator - Dot2.new( + 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) -> Dot3 + # on_dot3: ((nil | untyped) left, (nil | untyped) right) -> RangeNode def on_dot3(left, right) operator = consume_operator(:"...") beginning = left || operator ending = right || operator - Dot3.new( + RangeNode.new( left: left, + operator: operator, right: right, location: beginning.location.to(ending.location) ) @@ -1608,9 +1614,15 @@ def on_excessed_comma(*) end # :call-seq: - # on_fcall: ((Const | Ident) value) -> FCall + # on_fcall: ((Const | Ident) value) -> CallNode def on_fcall(value) - FCall.new(value: value, arguments: nil, location: value.location) + CallNode.new( + receiver: nil, + operator: nil, + message: value, + arguments: nil, + location: value.location + ) end # :call-seq: @@ -1878,7 +1890,7 @@ def on_ident(value) # untyped predicate, # Statements statements, # (nil | Elsif | Else) consequent - # ) -> If + # ) -> IfNode def on_if(predicate, statements, consequent) beginning = consume_keyword(:if) ending = consequent || consume_keyword(:end) @@ -1891,7 +1903,7 @@ def on_if(predicate, statements, consequent) ending.location.start_column ) - If.new( + IfNode.new( predicate: predicate, statements: statements, consequent: consequent, @@ -1911,13 +1923,15 @@ def on_ifop(predicate, truthy, falsy) end # :call-seq: - # on_if_mod: (untyped predicate, untyped statement) -> IfMod + # on_if_mod: (untyped predicate, untyped statement) -> IfNode def on_if_mod(predicate, statement) consume_keyword(:if) - IfMod.new( - statement: statement, + IfNode.new( predicate: predicate, + statements: + Statements.new(self, body: [statement], location: statement.location), + consequent: nil, location: statement.location.to(predicate.location) ) end @@ -2104,17 +2118,20 @@ def on_lambda(params, statements) location = params.contents.location location = location.to(locals.last.location) if locals.any? - Paren.new( - lparen: params.lparen, - contents: - LambdaVar.new( - params: params.contents, - locals: locals, - location: location - ), - location: params.location, - comments: params.comments - ) + 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 when Params # In this case we've gotten to the <3.2 plain set of parameters. In # this case there cannot be lambda locals, so we will wrap the @@ -2302,37 +2319,42 @@ def on_massign(target, value) # :call-seq: # on_method_add_arg: ( - # (Call | FCall) call, + # CallNode call, # (ArgParen | Args) arguments - # ) -> Call | FCall + # ) -> CallNode def on_method_add_arg(call, arguments) location = call.location location = location.to(arguments.location) if arguments.is_a?(ArgParen) - if call.is_a?(FCall) - FCall.new(value: call.value, arguments: arguments, location: location) - else - Call.new( - receiver: call.receiver, - operator: call.operator, - message: call.message, - arguments: arguments, - location: location - ) - end + CallNode.new( + receiver: call.receiver, + operator: call.operator, + message: call.message, + arguments: arguments, + location: location + ) end # :call-seq: # on_method_add_block: ( - # (Call | Command | CommandCall | FCall) call, - # (BraceBlock | DoBlock) block + # (Call | Command | CommandCall) call, + # Block block # ) -> MethodAddBlock def on_method_add_block(call, block) - MethodAddBlock.new( - call: call, - block: block, - location: call.location.to(block.location) - ) + location = call.location.to(block.location) + + case call + when Command, CommandCall + node = call.copy(block: block, location: location) + node.comments.concat(call.comments) + node + else + MethodAddBlock.new( + call: call, + block: block, + location: call.location.to(block.location) + ) + end end # :call-seq: @@ -2900,7 +2922,7 @@ def on_rbracket(value) def on_redo keyword = consume_keyword(:redo) - Redo.new(value: keyword.value, location: keyword.location) + Redo.new(location: keyword.location) end # :call-seq: @@ -3066,26 +3088,26 @@ def on_rest_param(name) def on_retry keyword = consume_keyword(:retry) - Retry.new(value: keyword.value, location: keyword.location) + Retry.new(location: keyword.location) end # :call-seq: - # on_return: (Args arguments) -> Return + # on_return: (Args arguments) -> ReturnNode def on_return(arguments) keyword = consume_keyword(:return) - Return.new( + ReturnNode.new( arguments: arguments, location: keyword.location.to(arguments.location) ) end # :call-seq: - # on_return0: () -> Return0 + # on_return0: () -> ReturnNode def on_return0 keyword = consume_keyword(:return) - Return0.new(value: keyword.value, location: keyword.location) + ReturnNode.new(arguments: nil, location: keyword.location) end # :call-seq: @@ -3562,7 +3584,7 @@ def on_undef(symbols) # untyped predicate, # Statements statements, # ((nil | Elsif | Else) consequent) - # ) -> Unless + # ) -> UnlessNode def on_unless(predicate, statements, consequent) beginning = consume_keyword(:unless) ending = consequent || consume_keyword(:end) @@ -3575,7 +3597,7 @@ def on_unless(predicate, statements, consequent) ending.location.start_column ) - Unless.new( + UnlessNode.new( predicate: predicate, statements: statements, consequent: consequent, @@ -3584,19 +3606,21 @@ def on_unless(predicate, statements, consequent) end # :call-seq: - # on_unless_mod: (untyped predicate, untyped statement) -> UnlessMod + # on_unless_mod: (untyped predicate, untyped statement) -> UnlessNode def on_unless_mod(predicate, statement) consume_keyword(:unless) - UnlessMod.new( - statement: statement, + UnlessNode.new( predicate: predicate, + statements: + Statements.new(self, body: [statement], location: statement.location), + consequent: nil, location: statement.location.to(predicate.location) ) end # :call-seq: - # on_until: (untyped predicate, Statements statements) -> Until + # on_until: (untyped predicate, Statements statements) -> UntilNode def on_until(predicate, statements) beginning = consume_keyword(:until) ending = consume_keyword(:end) @@ -3618,7 +3642,7 @@ def on_until(predicate, statements) ending.location.start_column ) - Until.new( + UntilNode.new( predicate: predicate, statements: statements, location: beginning.location.to(ending.location) @@ -3626,23 +3650,24 @@ def on_until(predicate, statements) end # :call-seq: - # on_until_mod: (untyped predicate, untyped statement) -> UntilMod + # on_until_mod: (untyped predicate, untyped statement) -> UntilNode def on_until_mod(predicate, statement) consume_keyword(:until) - UntilMod.new( - statement: statement, + UntilNode.new( predicate: predicate, + statements: + Statements.new(self, body: [statement], location: statement.location), location: statement.location.to(predicate.location) ) end # :call-seq: - # on_var_alias: (GVar left, (Backref | GVar) right) -> VarAlias + # on_var_alias: (GVar left, (Backref | GVar) right) -> AliasNode def on_var_alias(left, right) keyword = consume_keyword(:alias) - VarAlias.new( + AliasNode.new( left: left, right: right, location: keyword.location.to(right.location) @@ -3722,7 +3747,7 @@ def on_when(arguments, statements, consequent) end # :call-seq: - # on_while: (untyped predicate, Statements statements) -> While + # on_while: (untyped predicate, Statements statements) -> WhileNode def on_while(predicate, statements) beginning = consume_keyword(:while) ending = consume_keyword(:end) @@ -3744,7 +3769,7 @@ def on_while(predicate, statements) ending.location.start_column ) - While.new( + WhileNode.new( predicate: predicate, statements: statements, location: beginning.location.to(ending.location) @@ -3752,13 +3777,14 @@ def on_while(predicate, statements) end # :call-seq: - # on_while_mod: (untyped predicate, untyped statement) -> WhileMod + # on_while_mod: (untyped predicate, untyped statement) -> WhileNode def on_while_mod(predicate, statement) consume_keyword(:while) - WhileMod.new( - statement: statement, + WhileNode.new( predicate: predicate, + statements: + Statements.new(self, body: [statement], location: statement.location), location: statement.location.to(predicate.location) ) end @@ -3881,22 +3907,22 @@ def on_xstring_literal(xstring) end # :call-seq: - # on_yield: ((Args | Paren) arguments) -> Yield + # on_yield: ((Args | Paren) arguments) -> YieldNode def on_yield(arguments) keyword = consume_keyword(:yield) - Yield.new( + YieldNode.new( arguments: arguments, location: keyword.location.to(arguments.location) ) end # :call-seq: - # on_yield0: () -> Yield0 + # on_yield0: () -> YieldNode def on_yield0 keyword = consume_keyword(:yield) - Yield0.new(value: keyword.value, location: keyword.location) + YieldNode.new(arguments: nil, location: keyword.location) end # :call-seq: @@ -3904,7 +3930,7 @@ def on_yield0 def on_zsuper keyword = consume_keyword(:super) - ZSuper.new(value: keyword.value, location: keyword.location) + ZSuper.new(location: keyword.location) end end end diff --git a/lib/syntax_tree/pattern.rb b/lib/syntax_tree/pattern.rb index 439d573f..ca49c6bf 100644 --- a/lib/syntax_tree/pattern.rb +++ b/lib/syntax_tree/pattern.rb @@ -142,11 +142,11 @@ def compile_binary(node) def compile_const(node) value = node.value - if SyntaxTree.const_defined?(value) + if SyntaxTree.const_defined?(value, false) clazz = SyntaxTree.const_get(value) ->(other) { clazz === other } - elsif Object.const_defined?(value) + elsif Object.const_defined?(value, false) clazz = Object.const_get(value) ->(other) { clazz === other } @@ -179,7 +179,7 @@ def compile_dyna_symbol(node) ->(other) { symbol === other } else - compile_error(root) + compile_error(node) end end diff --git a/lib/syntax_tree/plugin/single_quotes.rb b/lib/syntax_tree/plugin/single_quotes.rb index c6e829e0..c7405e2c 100644 --- a/lib/syntax_tree/plugin/single_quotes.rb +++ b/lib/syntax_tree/plugin/single_quotes.rb @@ -1,3 +1,7 @@ # frozen_string_literal: true -SyntaxTree::Formatter::OPTIONS[:quote] = "'" +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 index 878703c3..1ae2b96d 100644 --- a/lib/syntax_tree/plugin/trailing_comma.rb +++ b/lib/syntax_tree/plugin/trailing_comma.rb @@ -1,3 +1,7 @@ # frozen_string_literal: true -SyntaxTree::Formatter::OPTIONS[:trailing_comma] = true +module SyntaxTree + class Formatter + TRAILING_COMMA = true + end +end diff --git a/lib/syntax_tree/version.rb b/lib/syntax_tree/version.rb index a12c472d..29a413d9 100644 --- a/lib/syntax_tree/version.rb +++ b/lib/syntax_tree/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module SyntaxTree - VERSION = "4.3.0" + VERSION = "5.0.0" end diff --git a/lib/syntax_tree/visitor.rb b/lib/syntax_tree/visitor.rb index e3b52077..eb57acd2 100644 --- a/lib/syntax_tree/visitor.rb +++ b/lib/syntax_tree/visitor.rb @@ -11,7 +11,7 @@ class Visitor < BasicVisitor # Visit an ARefField node. alias visit_aref_field visit_child_nodes - # Visit an Alias node. + # Visit an AliasNode node. alias visit_alias visit_child_nodes # Visit an ArgBlock node. @@ -62,6 +62,9 @@ class Visitor < BasicVisitor # 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 @@ -71,9 +74,6 @@ class Visitor < BasicVisitor # Visit a BodyStmt node. alias visit_bodystmt visit_child_nodes - # Visit a BraceBlock node. - alias visit_brace_block visit_child_nodes - # Visit a Break node. alias visit_break visit_child_nodes @@ -119,24 +119,9 @@ class Visitor < BasicVisitor # Visit a Def node. alias visit_def visit_child_nodes - # Visit a DefEndless node. - alias visit_def_endless visit_child_nodes - # Visit a Defined node. alias visit_defined visit_child_nodes - # Visit a Defs node. - alias visit_defs visit_child_nodes - - # Visit a DoBlock node. - alias visit_do_block visit_child_nodes - - # Visit a Dot2 node. - alias visit_dot2 visit_child_nodes - - # Visit a Dot3 node. - alias visit_dot3 visit_child_nodes - # Visit a DynaSymbol node. alias visit_dyna_symbol visit_child_nodes @@ -167,9 +152,6 @@ class Visitor < BasicVisitor # Visit an ExcessedComma node. alias visit_excessed_comma visit_child_nodes - # Visit a FCall node. - alias visit_fcall visit_child_nodes - # Visit a Field node. alias visit_field visit_child_nodes @@ -203,12 +185,9 @@ class Visitor < BasicVisitor # Visit an Ident node. alias visit_ident visit_child_nodes - # Visit an If node. + # Visit an IfNode node. alias visit_if visit_child_nodes - # Visit an IfMod node. - alias visit_if_mod visit_child_nodes - # Visit an IfOp node. alias visit_if_op visit_child_nodes @@ -311,6 +290,9 @@ class Visitor < BasicVisitor # 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 @@ -356,9 +338,6 @@ class Visitor < BasicVisitor # Visit a Return node. alias visit_return visit_child_nodes - # Visit a Return0 node. - alias visit_return0 visit_child_nodes - # Visit a RParen node. alias visit_rparen visit_child_nodes @@ -428,21 +407,12 @@ class Visitor < BasicVisitor # Visit an Undef node. alias visit_undef visit_child_nodes - # Visit an Unless node. + # Visit an UnlessNode node. alias visit_unless visit_child_nodes - # Visit an UnlessMod node. - alias visit_unless_mod visit_child_nodes - - # Visit an Until node. + # Visit an UntilNode node. alias visit_until visit_child_nodes - # Visit an UntilMod node. - alias visit_until_mod visit_child_nodes - - # Visit a VarAlias node. - alias visit_var_alias visit_child_nodes - # Visit a VarField node. alias visit_var_field visit_child_nodes @@ -458,12 +428,9 @@ class Visitor < BasicVisitor # Visit a When node. alias visit_when visit_child_nodes - # Visit a While node. + # Visit a WhileNode node. alias visit_while visit_child_nodes - # Visit a WhileMod node. - alias visit_while_mod visit_child_nodes - # Visit a Word node. alias visit_word visit_child_nodes @@ -479,12 +446,9 @@ class Visitor < BasicVisitor # Visit a XStringLiteral node. alias visit_xstring_literal visit_child_nodes - # Visit a Yield node. + # Visit a YieldNode node. alias visit_yield visit_child_nodes - # Visit a Yield0 node. - alias visit_yield0 visit_child_nodes - # Visit a ZSuper node. alias visit_zsuper visit_child_nodes diff --git a/lib/syntax_tree/visitor/environment.rb b/lib/syntax_tree/visitor/environment.rb index dfcf0a80..b07a5203 100644 --- a/lib/syntax_tree/visitor/environment.rb +++ b/lib/syntax_tree/visitor/environment.rb @@ -4,10 +4,6 @@ module SyntaxTree # The environment class is used to keep track of local variables and arguments # inside a particular scope class Environment - # [Array[Local]] The local variables and arguments defined in this - # environment - attr_reader :locals - # This class tracks the occurrences of a local variable or argument class Local # [Symbol] The type of the local (e.g. :argument, :variable) @@ -38,6 +34,13 @@ def add_usage(location) end end + # [Array[Local]] The local variables and arguments defined in this + # environment + attr_reader :locals + + # [Environment | nil] The parent environment + attr_reader :parent + # initialize: (Environment | nil parent) -> void def initialize(parent = nil) @locals = {} diff --git a/lib/syntax_tree/visitor/field_visitor.rb b/lib/syntax_tree/visitor/field_visitor.rb index 6c5c6139..6e643e09 100644 --- a/lib/syntax_tree/visitor/field_visitor.rb +++ b/lib/syntax_tree/visitor/field_visitor.rb @@ -103,7 +103,7 @@ def visit_args(node) end def visit_args_forward(node) - visit_token(node, "args_forward") + node(node, "args_forward") { comments(node) } end def visit_array(node) @@ -184,6 +184,14 @@ def visit_binary(node) end end + def visit_block(node) + node(node, "block") do + field("block_var", node.block_var) if node.block_var + field("bodystmt", node.bodystmt) + comments(node) + end + end + def visit_blockarg(node) node(node, "blockarg") do field("name", node.name) if node.name @@ -209,14 +217,6 @@ def visit_bodystmt(node) end end - def visit_brace_block(node) - node(node, "brace_block") do - field("block_var", node.block_var) if node.block_var - field("statements", node.statements) - comments(node) - end - end - def visit_break(node) node(node, "break") do field("arguments", node.arguments) @@ -315,6 +315,8 @@ def visit_cvar(node) def visit_def(node) node(node, "def") do + field("target", node.target) + field("operator", node.operator) field("name", node.name) field("params", node.params) field("bodystmt", node.bodystmt) @@ -322,20 +324,6 @@ def visit_def(node) end end - def visit_def_endless(node) - node(node, "def_endless") do - if node.target - field("target", node.target) - field("operator", node.operator) - end - - field("name", node.name) - field("paren", node.paren) if node.paren - field("statement", node.statement) - comments(node) - end - end - def visit_defined(node) node(node, "defined") do field("value", node.value) @@ -343,41 +331,6 @@ def visit_defined(node) end end - def visit_defs(node) - node(node, "defs") do - field("target", node.target) - field("operator", node.operator) - field("name", node.name) - field("params", node.params) - field("bodystmt", node.bodystmt) - comments(node) - end - end - - def visit_do_block(node) - node(node, "do_block") do - field("block_var", node.block_var) if node.block_var - field("bodystmt", node.bodystmt) - comments(node) - end - end - - def visit_dot2(node) - node(node, "dot2") do - field("left", node.left) if node.left - field("right", node.right) if node.right - comments(node) - end - end - - def visit_dot3(node) - node(node, "dot3") do - field("left", node.left) if node.left - field("right", node.right) if node.right - comments(node) - end - end - def visit_dyna_symbol(node) node(node, "dyna_symbol") do list("parts", node.parts) @@ -435,14 +388,6 @@ def visit_excessed_comma(node) visit_token(node, "excessed_comma") end - def visit_fcall(node) - node(node, "fcall") do - field("value", node.value) - field("arguments", node.arguments) if node.arguments - comments(node) - end - end - def visit_field(node) node(node, "field") do field("parent", node.parent) @@ -523,14 +468,6 @@ def visit_if(node) end end - def visit_if_mod(node) - node(node, "if_mod") do - field("statement", node.statement) - field("predicate", node.predicate) - comments(node) - end - end - def visit_if_op(node) node(node, "if_op") do field("predicate", node.predicate) @@ -747,6 +684,15 @@ def visit_qwords_beg(node) node(node, "qwords_beg") { field("value", node.value) } end + def visit_range(node) + node(node, "range") do + field("left", node.left) if node.left + field("operator", node.operator) + field("right", node.right) if node.right + comments(node) + end + end + def visit_rassign(node) node(node, "rassign") do field("value", node.value) @@ -769,7 +715,7 @@ def visit_rbracket(node) end def visit_redo(node) - visit_token(node, "redo") + node(node, "redo") { comments(node) } end def visit_regexp_beg(node) @@ -825,7 +771,7 @@ def visit_rest_param(node) end def visit_retry(node) - visit_token(node, "retry") + node(node, "retry") { comments(node) } end def visit_return(node) @@ -835,10 +781,6 @@ def visit_return(node) end end - def visit_return0(node) - visit_token(node, "return0") - end - def visit_rparen(node) node(node, "rparen") { field("value", node.value) } end @@ -982,14 +924,6 @@ def visit_unless(node) end end - def visit_unless_mod(node) - node(node, "unless_mod") do - field("statement", node.statement) - field("predicate", node.predicate) - comments(node) - end - end - def visit_until(node) node(node, "until") do field("predicate", node.predicate) @@ -998,22 +932,6 @@ def visit_until(node) end end - def visit_until_mod(node) - node(node, "until_mod") do - field("statement", node.statement) - field("predicate", node.predicate) - comments(node) - end - end - - def visit_var_alias(node) - node(node, "var_alias") do - field("left", node.left) - field("right", node.right) - comments(node) - end - end - def visit_var_field(node) node(node, "var_field") do field("value", node.value) @@ -1056,14 +974,6 @@ def visit_while(node) end end - def visit_while_mod(node) - node(node, "while_mod") do - field("statement", node.statement) - field("predicate", node.predicate) - comments(node) - end - end - def visit_word(node) node(node, "word") do list("parts", node.parts) @@ -1100,12 +1010,8 @@ def visit_yield(node) end end - def visit_yield0(node) - visit_token(node, "yield0") - end - def visit_zsuper(node) - visit_token(node, "zsuper") + node(node, "zsuper") { comments(node) } end def visit___end__(node) diff --git a/lib/syntax_tree/visitor/mutation_visitor.rb b/lib/syntax_tree/visitor/mutation_visitor.rb new file mode 100644 index 00000000..65f8c5ba --- /dev/null +++ b/lib/syntax_tree/visitor/mutation_visitor.rb @@ -0,0 +1,924 @@ +# frozen_string_literal: true + +module SyntaxTree + class Visitor + # This visitor walks through the tree and copies each node as it is being + # visited. This is useful for mutating the tree before it is formatted. + class MutationVisitor < BasicVisitor + attr_reader :mutations + + def initialize + @mutations = [] + end + + # Create a new mutation based on the given query that will mutate the node + # using the given block. The block should return a new node that will take + # the place of the given node in the tree. These blocks frequently make + # use of the `copy` method on nodes to create a new node with the same + # properties as the original node. + def mutate(query, &block) + mutations << [Pattern.new(query).compile, block] + end + + # This is the base visit method for each node in the tree. It first + # creates a copy of the node using the visit_* methods defined below. Then + # it checks each mutation in sequence and calls it if it finds a match. + def visit(node) + return unless node + result = node.accept(self) + + mutations.each do |(pattern, mutation)| + result = mutation.call(result) if pattern.call(result) + end + + result + end + + # Visit a BEGINBlock node. + def visit_BEGIN(node) + node.copy( + lbrace: visit(node.lbrace), + statements: visit(node.statements) + ) + end + + # Visit a CHAR node. + def visit_CHAR(node) + node.copy + end + + # Visit a ENDBlock node. + def visit_END(node) + node.copy( + lbrace: visit(node.lbrace), + statements: visit(node.statements) + ) + end + + # Visit a EndContent node. + def visit___end__(node) + node.copy + end + + # Visit a AliasNode node. + def visit_alias(node) + node.copy(left: visit(node.left), right: visit(node.right)) + end + + # Visit a ARef node. + def visit_aref(node) + node.copy(index: visit(node.index)) + end + + # Visit a ARefField node. + def visit_aref_field(node) + node.copy(index: visit(node.index)) + end + + # Visit a ArgParen node. + def visit_arg_paren(node) + node.copy(arguments: visit(node.arguments)) + end + + # Visit a Args node. + def visit_args(node) + node.copy(parts: visit_all(node.parts)) + end + + # Visit a ArgBlock node. + def visit_arg_block(node) + node.copy(value: visit(node.value)) + end + + # Visit a ArgStar node. + def visit_arg_star(node) + node.copy(value: visit(node.value)) + end + + # Visit a ArgsForward node. + def visit_args_forward(node) + node.copy + end + + # Visit a ArrayLiteral node. + def visit_array(node) + node.copy( + lbracket: visit(node.lbracket), + contents: visit(node.contents) + ) + end + + # Visit a AryPtn node. + def visit_aryptn(node) + node.copy( + constant: visit(node.constant), + requireds: visit_all(node.requireds), + rest: visit(node.rest), + posts: visit_all(node.posts) + ) + end + + # Visit a Assign node. + def visit_assign(node) + node.copy(target: visit(node.target)) + end + + # Visit a Assoc node. + def visit_assoc(node) + node.copy + end + + # Visit a AssocSplat node. + def visit_assoc_splat(node) + node.copy + end + + # Visit a Backref node. + def visit_backref(node) + node.copy + end + + # Visit a Backtick node. + def visit_backtick(node) + node.copy + end + + # Visit a BareAssocHash node. + def visit_bare_assoc_hash(node) + node.copy(assocs: visit_all(node.assocs)) + end + + # Visit a Begin node. + def visit_begin(node) + node.copy(bodystmt: visit(node.bodystmt)) + end + + # Visit a PinnedBegin node. + def visit_pinned_begin(node) + node.copy + end + + # Visit a Binary node. + def visit_binary(node) + node.copy + end + + # Visit a BlockVar node. + def visit_block_var(node) + node.copy(params: visit(node.params), locals: visit_all(node.locals)) + end + + # Visit a BlockArg node. + def visit_blockarg(node) + node.copy(name: visit(node.name)) + end + + # Visit a BodyStmt node. + def visit_bodystmt(node) + node.copy( + statements: visit(node.statements), + rescue_clause: visit(node.rescue_clause), + else_clause: visit(node.else_clause), + ensure_clause: visit(node.ensure_clause) + ) + end + + # Visit a Break node. + def visit_break(node) + node.copy(arguments: visit(node.arguments)) + end + + # Visit a Call node. + def visit_call(node) + node.copy( + receiver: visit(node.receiver), + operator: node.operator == :"::" ? :"::" : visit(node.operator), + message: node.message == :call ? :call : visit(node.message), + arguments: visit(node.arguments) + ) + end + + # Visit a Case node. + def visit_case(node) + node.copy( + keyword: visit(node.keyword), + value: visit(node.value), + consequent: visit(node.consequent) + ) + end + + # Visit a RAssign node. + def visit_rassign(node) + node.copy(operator: visit(node.operator)) + end + + # Visit a ClassDeclaration node. + def visit_class(node) + node.copy( + constant: visit(node.constant), + superclass: visit(node.superclass), + bodystmt: visit(node.bodystmt) + ) + end + + # Visit a Comma node. + def visit_comma(node) + node.copy + end + + # Visit a Command node. + def visit_command(node) + node.copy( + message: visit(node.message), + arguments: visit(node.arguments), + block: visit(node.block) + ) + end + + # Visit a CommandCall node. + def visit_command_call(node) + node.copy( + operator: node.operator == :"::" ? :"::" : visit(node.operator), + message: visit(node.message), + arguments: visit(node.arguments), + block: visit(node.block) + ) + end + + # Visit a Comment node. + def visit_comment(node) + node.copy + end + + # Visit a Const node. + def visit_const(node) + node.copy + end + + # Visit a ConstPathField node. + def visit_const_path_field(node) + node.copy(constant: visit(node.constant)) + end + + # Visit a ConstPathRef node. + def visit_const_path_ref(node) + node.copy(constant: visit(node.constant)) + end + + # Visit a ConstRef node. + def visit_const_ref(node) + node.copy(constant: visit(node.constant)) + end + + # Visit a CVar node. + def visit_cvar(node) + node.copy + end + + # Visit a Def node. + def visit_def(node) + node.copy( + target: visit(node.target), + operator: visit(node.operator), + name: visit(node.name), + params: visit(node.params), + bodystmt: visit(node.bodystmt) + ) + end + + # Visit a Defined node. + def visit_defined(node) + node.copy + end + + # Visit a Block node. + def visit_block(node) + node.copy( + opening: visit(node.opening), + block_var: visit(node.block_var), + bodystmt: visit(node.bodystmt) + ) + end + + # Visit a RangeNode node. + def visit_range(node) + node.copy( + left: visit(node.left), + operator: visit(node.operator), + right: visit(node.right) + ) + end + + # Visit a DynaSymbol node. + def visit_dyna_symbol(node) + node.copy(parts: visit_all(node.parts)) + end + + # Visit a Else node. + def visit_else(node) + node.copy( + keyword: visit(node.keyword), + statements: visit(node.statements) + ) + end + + # Visit a Elsif node. + def visit_elsif(node) + node.copy( + statements: visit(node.statements), + consequent: visit(node.consequent) + ) + end + + # Visit a EmbDoc node. + def visit_embdoc(node) + node.copy + end + + # Visit a EmbExprBeg node. + def visit_embexpr_beg(node) + node.copy + end + + # Visit a EmbExprEnd node. + def visit_embexpr_end(node) + node.copy + end + + # Visit a EmbVar node. + def visit_embvar(node) + node.copy + end + + # Visit a Ensure node. + def visit_ensure(node) + node.copy( + keyword: visit(node.keyword), + statements: visit(node.statements) + ) + end + + # Visit a ExcessedComma node. + def visit_excessed_comma(node) + node.copy + end + + # Visit a Field node. + def visit_field(node) + node.copy( + operator: node.operator == :"::" ? :"::" : visit(node.operator), + name: visit(node.name) + ) + end + + # Visit a FloatLiteral node. + def visit_float(node) + node.copy + end + + # Visit a FndPtn node. + def visit_fndptn(node) + node.copy( + constant: visit(node.constant), + left: visit(node.left), + values: visit_all(node.values), + right: visit(node.right) + ) + end + + # Visit a For node. + def visit_for(node) + node.copy(index: visit(node.index), statements: visit(node.statements)) + end + + # Visit a GVar node. + def visit_gvar(node) + node.copy + end + + # Visit a HashLiteral node. + def visit_hash(node) + node.copy(lbrace: visit(node.lbrace), assocs: visit_all(node.assocs)) + end + + # Visit a Heredoc node. + def visit_heredoc(node) + node.copy( + beginning: visit(node.beginning), + ending: visit(node.ending), + parts: visit_all(node.parts) + ) + end + + # Visit a HeredocBeg node. + def visit_heredoc_beg(node) + node.copy + end + + # Visit a HeredocEnd node. + def visit_heredoc_end(node) + node.copy + end + + # Visit a HshPtn node. + def visit_hshptn(node) + node.copy( + constant: visit(node.constant), + keywords: + node.keywords.map { |label, value| [visit(label), visit(value)] }, + keyword_rest: visit(node.keyword_rest) + ) + end + + # Visit a Ident node. + def visit_ident(node) + node.copy + end + + # Visit a IfNode node. + def visit_if(node) + node.copy( + predicate: visit(node.predicate), + statements: visit(node.statements), + consequent: visit(node.consequent) + ) + end + + # Visit a IfOp node. + def visit_if_op(node) + node.copy + end + + # Visit a Imaginary node. + def visit_imaginary(node) + node.copy + end + + # Visit a In node. + def visit_in(node) + node.copy( + statements: visit(node.statements), + consequent: visit(node.consequent) + ) + end + + # Visit a Int node. + def visit_int(node) + node.copy + end + + # Visit a IVar node. + def visit_ivar(node) + node.copy + end + + # Visit a Kw node. + def visit_kw(node) + node.copy + end + + # Visit a KwRestParam node. + def visit_kwrest_param(node) + node.copy(name: visit(node.name)) + end + + # Visit a Label node. + def visit_label(node) + node.copy + end + + # Visit a LabelEnd node. + def visit_label_end(node) + node.copy + end + + # Visit a Lambda node. + def visit_lambda(node) + node.copy( + params: visit(node.params), + statements: visit(node.statements) + ) + end + + # Visit a LambdaVar node. + def visit_lambda_var(node) + node.copy(params: visit(node.params), locals: visit_all(node.locals)) + end + + # Visit a LBrace node. + def visit_lbrace(node) + node.copy + end + + # Visit a LBracket node. + def visit_lbracket(node) + node.copy + end + + # Visit a LParen node. + def visit_lparen(node) + node.copy + end + + # Visit a MAssign node. + def visit_massign(node) + node.copy(target: visit(node.target)) + end + + # Visit a MethodAddBlock node. + def visit_method_add_block(node) + node.copy(call: visit(node.call), block: visit(node.block)) + end + + # Visit a MLHS node. + def visit_mlhs(node) + node.copy(parts: visit_all(node.parts)) + end + + # Visit a MLHSParen node. + def visit_mlhs_paren(node) + node.copy(contents: visit(node.contents)) + end + + # Visit a ModuleDeclaration node. + def visit_module(node) + node.copy( + constant: visit(node.constant), + bodystmt: visit(node.bodystmt) + ) + end + + # Visit a MRHS node. + def visit_mrhs(node) + node.copy(parts: visit_all(node.parts)) + end + + # Visit a Next node. + def visit_next(node) + node.copy(arguments: visit(node.arguments)) + end + + # Visit a Op node. + def visit_op(node) + node.copy + end + + # Visit a OpAssign node. + def visit_opassign(node) + node.copy(target: visit(node.target), operator: visit(node.operator)) + end + + # Visit a Params node. + def visit_params(node) + node.copy( + requireds: visit_all(node.requireds), + optionals: + node.optionals.map { |ident, value| [visit(ident), visit(value)] }, + rest: visit(node.rest), + posts: visit_all(node.posts), + keywords: + node.keywords.map { |ident, value| [visit(ident), visit(value)] }, + keyword_rest: + node.keyword_rest == :nil ? :nil : visit(node.keyword_rest), + block: visit(node.block) + ) + end + + # Visit a Paren node. + def visit_paren(node) + node.copy(lparen: visit(node.lparen), contents: visit(node.contents)) + end + + # Visit a Period node. + def visit_period(node) + node.copy + end + + # Visit a Program node. + def visit_program(node) + node.copy(statements: visit(node.statements)) + end + + # Visit a QSymbols node. + def visit_qsymbols(node) + node.copy( + beginning: visit(node.beginning), + elements: visit_all(node.elements) + ) + end + + # Visit a QSymbolsBeg node. + def visit_qsymbols_beg(node) + node.copy + end + + # Visit a QWords node. + def visit_qwords(node) + node.copy( + beginning: visit(node.beginning), + elements: visit_all(node.elements) + ) + end + + # Visit a QWordsBeg node. + def visit_qwords_beg(node) + node.copy + end + + # Visit a RationalLiteral node. + def visit_rational(node) + node.copy + end + + # Visit a RBrace node. + def visit_rbrace(node) + node.copy + end + + # Visit a RBracket node. + def visit_rbracket(node) + node.copy + end + + # Visit a Redo node. + def visit_redo(node) + node.copy + end + + # Visit a RegexpContent node. + def visit_regexp_content(node) + node.copy(parts: visit_all(node.parts)) + end + + # Visit a RegexpBeg node. + def visit_regexp_beg(node) + node.copy + end + + # Visit a RegexpEnd node. + def visit_regexp_end(node) + node.copy + end + + # Visit a RegexpLiteral node. + def visit_regexp_literal(node) + node.copy(parts: visit_all(node.parts)) + end + + # Visit a RescueEx node. + def visit_rescue_ex(node) + node.copy(variable: visit(node.variable)) + end + + # Visit a Rescue node. + def visit_rescue(node) + node.copy( + keyword: visit(node.keyword), + exception: visit(node.exception), + statements: visit(node.statements), + consequent: visit(node.consequent) + ) + end + + # Visit a RescueMod node. + def visit_rescue_mod(node) + node.copy + end + + # Visit a RestParam node. + def visit_rest_param(node) + node.copy(name: visit(node.name)) + end + + # Visit a Retry node. + def visit_retry(node) + node.copy + end + + # Visit a Return node. + def visit_return(node) + node.copy(arguments: visit(node.arguments)) + end + + # Visit a RParen node. + def visit_rparen(node) + node.copy + end + + # Visit a SClass node. + def visit_sclass(node) + node.copy(bodystmt: visit(node.bodystmt)) + end + + # Visit a Statements node. + def visit_statements(node) + node.copy(body: visit_all(node.body)) + end + + # Visit a StringContent node. + def visit_string_content(node) + node.copy(parts: visit_all(node.parts)) + end + + # Visit a StringConcat node. + def visit_string_concat(node) + node.copy(left: visit(node.left), right: visit(node.right)) + end + + # Visit a StringDVar node. + def visit_string_dvar(node) + node.copy(variable: visit(node.variable)) + end + + # Visit a StringEmbExpr node. + def visit_string_embexpr(node) + node.copy(statements: visit(node.statements)) + end + + # Visit a StringLiteral node. + def visit_string_literal(node) + node.copy(parts: visit_all(node.parts)) + end + + # Visit a Super node. + def visit_super(node) + node.copy(arguments: visit(node.arguments)) + end + + # Visit a SymBeg node. + def visit_symbeg(node) + node.copy + end + + # Visit a SymbolContent node. + def visit_symbol_content(node) + node.copy(value: visit(node.value)) + end + + # Visit a SymbolLiteral node. + def visit_symbol_literal(node) + node.copy(value: visit(node.value)) + end + + # Visit a Symbols node. + def visit_symbols(node) + node.copy( + beginning: visit(node.beginning), + elements: visit_all(node.elements) + ) + end + + # Visit a SymbolsBeg node. + def visit_symbols_beg(node) + node.copy + end + + # Visit a TLambda node. + def visit_tlambda(node) + node.copy + end + + # Visit a TLamBeg node. + def visit_tlambeg(node) + node.copy + end + + # Visit a TopConstField node. + def visit_top_const_field(node) + node.copy(constant: visit(node.constant)) + end + + # Visit a TopConstRef node. + def visit_top_const_ref(node) + node.copy(constant: visit(node.constant)) + end + + # Visit a TStringBeg node. + def visit_tstring_beg(node) + node.copy + end + + # Visit a TStringContent node. + def visit_tstring_content(node) + node.copy + end + + # Visit a TStringEnd node. + def visit_tstring_end(node) + node.copy + end + + # Visit a Not node. + def visit_not(node) + node.copy(statement: visit(node.statement)) + end + + # Visit a Unary node. + def visit_unary(node) + node.copy + end + + # Visit a Undef node. + def visit_undef(node) + node.copy(symbols: visit_all(node.symbols)) + end + + # Visit a UnlessNode node. + def visit_unless(node) + node.copy( + predicate: visit(node.predicate), + statements: visit(node.statements), + consequent: visit(node.consequent) + ) + end + + # Visit a UntilNode node. + def visit_until(node) + node.copy( + predicate: visit(node.predicate), + statements: visit(node.statements) + ) + end + + # Visit a VarField node. + def visit_var_field(node) + node.copy(value: visit(node.value)) + end + + # Visit a VarRef node. + def visit_var_ref(node) + node.copy(value: visit(node.value)) + end + + # Visit a PinnedVarRef node. + def visit_pinned_var_ref(node) + node.copy(value: visit(node.value)) + end + + # Visit a VCall node. + def visit_vcall(node) + node.copy(value: visit(node.value)) + end + + # Visit a VoidStmt node. + def visit_void_stmt(node) + node.copy + end + + # Visit a When node. + def visit_when(node) + node.copy( + arguments: visit(node.arguments), + statements: visit(node.statements), + consequent: visit(node.consequent) + ) + end + + # Visit a WhileNode node. + def visit_while(node) + node.copy( + predicate: visit(node.predicate), + statements: visit(node.statements) + ) + end + + # Visit a Word node. + def visit_word(node) + node.copy(parts: visit_all(node.parts)) + end + + # Visit a Words node. + def visit_words(node) + node.copy( + beginning: visit(node.beginning), + elements: visit_all(node.elements) + ) + end + + # Visit a WordsBeg node. + def visit_words_beg(node) + node.copy + end + + # Visit a XString node. + def visit_xstring(node) + node.copy(parts: visit_all(node.parts)) + end + + # Visit a XStringLiteral node. + def visit_xstring_literal(node) + node.copy(parts: visit_all(node.parts)) + end + + # Visit a YieldNode node. + def visit_yield(node) + node.copy(arguments: visit(node.arguments)) + end + + # Visit a ZSuper node. + def visit_zsuper(node) + node.copy + end + end + end +end diff --git a/lib/syntax_tree/visitor/with_environment.rb b/lib/syntax_tree/visitor/with_environment.rb index 043cbd4c..59033d50 100644 --- a/lib/syntax_tree/visitor/with_environment.rb +++ b/lib/syntax_tree/visitor/with_environment.rb @@ -56,14 +56,6 @@ def visit_def(node) with_new_environment { super } end - def visit_defs(node) - with_new_environment { super } - end - - def visit_def_endless(node) - with_new_environment { super } - end - # Visit for keeping track of local arguments, such as method and block # arguments def visit_params(node) diff --git a/syntax_tree.gemspec b/syntax_tree.gemspec index c82a8e98..19f4ee97 100644 --- a/syntax_tree.gemspec +++ b/syntax_tree.gemspec @@ -25,7 +25,7 @@ Gem::Specification.new do |spec| spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = %w[lib] - spec.add_dependency "prettier_print", ">= 1.0.2" + spec.add_dependency "prettier_print", ">= 1.1.0" spec.add_development_dependency "bundler" spec.add_development_dependency "minitest" diff --git a/test/cli_test.rb b/test/cli_test.rb index c00fb338..7c9e2652 100644 --- a/test/cli_test.rb +++ b/test/cli_test.rb @@ -41,6 +41,7 @@ def test_ast_ignore 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) end def test_check @@ -51,6 +52,7 @@ def test_check def test_check_unformatted result = run_cli("check", contents: "foo") assert_includes(result.stderr, "expected") + refute_equal(0, result.status) end def test_check_print_width @@ -59,6 +61,11 @@ 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") @@ -71,6 +78,7 @@ def test_debug_non_idempotent_format SyntaxTree.stub(:format, formatting) do result = run_cli("debug") assert_includes(result.stderr, "idempotently") + refute_equal(0, result.status) end end @@ -84,6 +92,12 @@ def test_expr assert_includes(result.stdio, "SyntaxTree::Ident") end + def test_expr_more_than_one + result = run_cli("expr", contents: "1; 2") + assert_includes(result.stderr, "single expression") + refute_equal(0, result.status) + end + def test_format result = run_cli("format") assert_equal("test\n", result.stdio) @@ -104,6 +118,17 @@ def test_search assert_equal(2, result.stdio.lines.length) end + def test_search_multi_line + result = run_cli("search", "Binary", contents: "1 +\n2") + assert_equal(1, result.stdio.lines.length) + end + + def test_search_invalid + result = run_cli("search", "FooBar") + assert_includes(result.stderr, "unable") + refute_equal(0, result.status) + end + def test_version result = run_cli("version") assert_includes(result.stdio, SyntaxTree::VERSION.to_s) @@ -120,6 +145,29 @@ def test_write def test_write_syntax_tree result = run_cli("write", contents: "<>") assert_includes(result.stderr, "syntax error") + refute_equal(0, result.status) + end + + def test_write_script + args = ["write", "-e", "1 + 2"] + stdout, stderr = capture_io { SyntaxTree::CLI.run(args) } + + assert_includes stdout, "script" + assert_empty stderr + end + + def test_write_stdin + previous = $stdin + $stdin = StringIO.new("1 + 2") + + begin + stdout, stderr = capture_io { SyntaxTree::CLI.run(["write"]) } + + assert_includes stdout, "stdin" + assert_empty stderr + ensure + $stdin = previous + end end def test_help @@ -128,8 +176,10 @@ def test_help end def test_help_default - *, stderr = capture_io { SyntaxTree::CLI.run(["foobar"]) } + status = 0 + *, stderr = capture_io { status = SyntaxTree::CLI.run(["foobar"]) } assert_includes(stderr, "stree help") + refute_equal(0, status) end def test_no_arguments @@ -148,7 +198,6 @@ def test_inline_script end def test_multiple_inline_scripts - skip if RUBY_ENGINE == "truffleruby" # Relies on a thread-safe StringIO stdio, = capture_io { SyntaxTree::CLI.run(%w[format -e 1+1 -e 2+2]) } assert_equal(["1 + 1", "2 + 2"], stdio.split("\n").sort) end @@ -216,6 +265,7 @@ def test_print_width_args_with_config_file_override result = run_cli("check", "--print-width=82", contents: contents) assert_includes(result.stderr, "expected") + refute_equal(0, result.status) end end @@ -252,7 +302,12 @@ def run_cli(command, *args, contents: :default) status = nil stdio, stderr = capture_io do - status = SyntaxTree::CLI.run([command, *args, tempfile.path]) + status = + begin + SyntaxTree::CLI.run([command, *args, tempfile.path]) + rescue SystemExit => error + error.status + end end Result.new(status: status, stdio: stdio, stderr: stderr) diff --git a/test/fixtures/def_endless.rb b/test/fixtures/def_endless.rb index 15ea518b..4595fba9 100644 --- a/test/fixtures/def_endless.rb +++ b/test/fixtures/def_endless.rb @@ -4,8 +4,6 @@ def foo = bar def foo(bar) = baz % def foo() = bar -- -def foo = bar % # >= 3.1.0 def foo = bar baz % # >= 3.1.0 @@ -14,8 +12,6 @@ def self.foo = bar def self.foo(bar) = baz % # >= 3.1.0 def self.foo() = bar -- -def self.foo = bar % # >= 3.1.0 def self.foo = bar baz % diff --git a/test/language_server_test.rb b/test/language_server_test.rb index 8e1ed9a7..2fe4e60a 100644 --- a/test/language_server_test.rb +++ b/test/language_server_test.rb @@ -159,6 +159,24 @@ def test_inlay_hint assert_equal(3, responses.dig(1, :result).size) end + def test_inlay_hint_invalid + responses = run_server([ + Initialize.new(1), + TextDocumentDidOpen.new("file:///path/to/file.rb", "<>"), + TextDocumentInlayHint.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_equal(0, responses.dig(1, :result).size) + end + def test_visualizing responses = run_server([ Initialize.new(1), diff --git a/test/mutation_test.rb b/test/mutation_test.rb new file mode 100644 index 00000000..ab9dd019 --- /dev/null +++ b/test/mutation_test.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require_relative "test_helper" + +module SyntaxTree + class MutationTest < Minitest::Test + def test_mutates_based_on_patterns + source = <<~RUBY + if a = b + c + end + RUBY + + expected = <<~RUBY + if (a = b) + c + end + RUBY + + program = SyntaxTree.parse(source).accept(build_mutation) + assert_equal(expected, SyntaxTree::Formatter.format(source, program)) + end + + private + + def build_mutation + SyntaxTree.mutation do |mutation| + mutation.mutate("IfNode[predicate: Assign | OpAssign]") do |node| + # Get the existing If's predicate node + predicate = node.predicate + + # Create a new predicate node that wraps the existing predicate node + # in parentheses + predicate = + SyntaxTree::Paren.new( + lparen: SyntaxTree::LParen.default, + contents: predicate, + location: predicate.location + ) + + # Return a copy of this node with the new predicate + node.copy(predicate: predicate) + end + end + end + end +end diff --git a/test/node_test.rb b/test/node_test.rb index ce26f9ea..15826be0 100644 --- a/test/node_test.rb +++ b/test/node_test.rb @@ -32,7 +32,7 @@ def test___end__ end def test_alias - assert_node(Alias, "alias left right") + assert_node(AliasNode, "alias left right") end def test_aref @@ -276,7 +276,7 @@ def test_brace_block source = "method { |variable| variable + 1 }" at = location(chars: 7..34) - assert_node(BraceBlock, source, at: at, &:block) + assert_node(BlockNode, source, at: at, &:block) end def test_break @@ -284,7 +284,7 @@ def test_break end def test_call - assert_node(Call, "receiver.message") + assert_node(CallNode, "receiver.message") end def test_case @@ -365,7 +365,7 @@ def test_cvar end def test_def - assert_node(Def, "def method(param) result end") + assert_node(DefNode, "def method(param) result end") end def test_def_paramless @@ -374,18 +374,18 @@ def method end SOURCE - assert_node(Def, source) + assert_node(DefNode, source) end guard_version("3.0.0") do def test_def_endless - assert_node(DefEndless, "def method = result") + assert_node(DefNode, "def method = result") end end guard_version("3.1.0") do def test_def_endless_command - assert_node(DefEndless, "def method = result argument") + assert_node(DefNode, "def method = result argument") end end @@ -394,7 +394,7 @@ def test_defined end def test_defs - assert_node(Defs, "def object.method(param) result end") + assert_node(DefNode, "def object.method(param) result end") end def test_defs_paramless @@ -403,22 +403,22 @@ def object.method end SOURCE - assert_node(Defs, source) + assert_node(DefNode, source) end def test_do_block source = "method do |variable| variable + 1 end" at = location(chars: 7..37) - assert_node(DoBlock, source, at: at, &:block) + assert_node(BlockNode, source, at: at, &:block) end def test_dot2 - assert_node(Dot2, "1..3") + assert_node(RangeNode, "1..3") end def test_dot3 - assert_node(Dot3, "1...3") + assert_node(RangeNode, "1...3") end def test_dyna_symbol @@ -487,7 +487,7 @@ def test_excessed_comma end def test_fcall - assert_node(FCall, "method(argument)") + assert_node(CallNode, "method(argument)") end def test_field @@ -575,7 +575,7 @@ def test_ident end def test_if - assert_node(If, "if value then else end") + assert_node(IfNode, "if value then else end") end def test_if_op @@ -583,7 +583,7 @@ def test_if_op end def test_if_mod - assert_node(IfMod, "expression if predicate") + assert_node(IfNode, "expression if predicate") end def test_imaginary @@ -647,7 +647,7 @@ def test_lbrace source = "method {}" at = location(chars: 7..8) - assert_node(LBrace, source, at: at) { |node| node.block.lbrace } + assert_node(LBrace, source, at: at) { |node| node.block.opening } end def test_lparen @@ -837,11 +837,11 @@ def test_retry end def test_return - assert_node(Return, "return value") + assert_node(ReturnNode, "return value") end def test_return0 - assert_node(Return0, "return") + assert_node(ReturnNode, "return") end def test_sclass @@ -923,23 +923,23 @@ def test_undef end def test_unless - assert_node(Unless, "unless value then else end") + assert_node(UnlessNode, "unless value then else end") end def test_unless_mod - assert_node(UnlessMod, "expression unless predicate") + assert_node(UnlessNode, "expression unless predicate") end def test_until - assert_node(Until, "until value do end") + assert_node(UntilNode, "until value do end") end def test_until_mod - assert_node(UntilMod, "expression until predicate") + assert_node(UntilNode, "expression until predicate") end def test_var_alias - assert_node(VarAlias, "alias $new $old") + assert_node(AliasNode, "alias $new $old") end def test_var_field @@ -981,11 +981,11 @@ def test_when end def test_while - assert_node(While, "while value do end") + assert_node(WhileNode, "while value do end") end def test_while_mod - assert_node(WhileMod, "expression while predicate") + assert_node(WhileNode, "expression while predicate") end def test_word @@ -1013,11 +1013,11 @@ def test_xstring_heredoc end def test_yield - assert_node(Yield, "yield value") + assert_node(YieldNode, "yield value") end def test_yield0 - assert_node(Yield0, "yield") + assert_node(YieldNode, "yield") end def test_zsuper diff --git a/test/plugin/single_quotes_test.rb b/test/plugin/single_quotes_test.rb index 719f33c1..6ce10448 100644 --- a/test/plugin/single_quotes_test.rb +++ b/test/plugin/single_quotes_test.rb @@ -4,8 +4,6 @@ module SyntaxTree class SingleQuotesTest < Minitest::Test - OPTIONS = Plugin.options("syntax_tree/plugin/single_quotes") - def test_empty_string_literal assert_format("''\n", "\"\"") end @@ -36,7 +34,8 @@ def test_label private def assert_format(expected, source = expected) - formatter = Formatter.new(source, [], **OPTIONS) + options = Formatter::Options.new(quote: "'") + formatter = Formatter.new(source, [], options: options) SyntaxTree.parse(source).format(formatter) formatter.flush diff --git a/test/plugin/trailing_comma_test.rb b/test/plugin/trailing_comma_test.rb index ba9ad846..7f6e49a8 100644 --- a/test/plugin/trailing_comma_test.rb +++ b/test/plugin/trailing_comma_test.rb @@ -4,8 +4,6 @@ module SyntaxTree class TrailingCommaTest < Minitest::Test - OPTIONS = Plugin.options("syntax_tree/plugin/trailing_comma") - def test_arg_paren_flat assert_format("foo(a)\n") end @@ -82,7 +80,8 @@ def test_hash_literal_break private def assert_format(expected, source = expected) - formatter = Formatter.new(source, [], **OPTIONS) + options = Formatter::Options.new(trailing_comma: true) + formatter = Formatter.new(source, [], options: options) SyntaxTree.parse(source).format(formatter) formatter.flush diff --git a/test/ractor_test.rb b/test/ractor_test.rb new file mode 100644 index 00000000..bcdb2a51 --- /dev/null +++ b/test/ractor_test.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +# 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") + +require_relative "test_helper" + +module SyntaxTree + class RactorTest < Minitest::Test + def test_formatting + 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 + end + end + + ractors.each { |ractor| assert_kind_of String, ractor.take } + end + + private + + def filepaths + Dir.glob(File.expand_path("../lib/syntax_tree/{node,parser}.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 + + begin + $VERBOSE = nil + yield + ensure + $VERBOSE = previous + end + end + end +end diff --git a/test/rake_test.rb b/test/rake_test.rb index bd315cc6..90662519 100644 --- a/test/rake_test.rb +++ b/test/rake_test.rb @@ -8,12 +8,28 @@ module Rake class CheckTaskTest < Minitest::Test Invocation = Struct.new(:args) + def test_task_command + assert_raises(NotImplementedError) { Task.new.command } + end + def test_check_task source_files = "{app,config,lib}/**/*.rb" - CheckTask.new { |t| t.source_files = source_files } + + 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(["check", source_files], invocation.args) + assert_equal(expected, invocation.args) end def test_write_task @@ -30,12 +46,11 @@ def invoke(task_name) invocation = nil stub = ->(args) { invocation = Invocation.new(args) } - begin + assert_raises SystemExit do SyntaxTree::CLI.stub(:run, stub) { ::Rake::Task[task_name].invoke } - flunk - rescue SystemExit - invocation end + + invocation end end end diff --git a/test/search_test.rb b/test/search_test.rb index 314142e3..9f7d89b8 100644 --- a/test/search_test.rb +++ b/test/search_test.rb @@ -4,6 +4,50 @@ module SyntaxTree class SearchTest < Minitest::Test + def test_search_invalid_syntax + assert_raises(Pattern::CompilationError) { search("", "<>") } + end + + def test_search_invalid_constant + assert_raises(Pattern::CompilationError) { search("", "Foo") } + end + + def test_search_invalid_nested_constant + assert_raises(Pattern::CompilationError) { search("", "Foo::Bar") } + end + + def test_search_regexp_with_interpolation + assert_raises(Pattern::CompilationError) { search("", "/\#{foo}/") } + end + + def test_search_string_with_interpolation + assert_raises(Pattern::CompilationError) { search("", '"#{foo}"') } + end + + def test_search_symbol_with_interpolation + assert_raises(Pattern::CompilationError) { search("", ":\"\#{foo}\"") } + end + + def test_search_invalid_node + assert_raises(Pattern::CompilationError) { search("", "Int[^foo]") } + end + + def test_search_self + assert_raises(Pattern::CompilationError) { search("", "self") } + end + + def test_search_array_pattern_no_constant + results = search("1 + 2", "[Int, Int]") + + assert_equal 1, results.length + end + + def test_search_array_pattern + results = search("1 + 2", "Binary[Int, Int]") + + assert_equal 1, results.length + end + def test_search_binary_or results = search("Foo + Bar + 1", "VarRef | Int") @@ -18,12 +62,24 @@ def test_search_const assert_equal %w[Bar Baz Foo], results.map { |node| node.value.value }.sort end + def test_search_object_const + results = search("1 + 2 + 3", "Int[value: String]") + + assert_equal 3, results.length + end + def test_search_syntax_tree_const results = search("Foo + Bar + Baz", "SyntaxTree::VarRef") assert_equal 3, results.length end + def test_search_hash_pattern_no_constant + results = search("Foo + Bar + Baz", "{ value: Const }") + + assert_equal 3, results.length + end + def test_search_hash_pattern_string results = search("Foo + Bar + Baz", "VarRef[value: Const[value: 'Foo']]") @@ -39,13 +95,25 @@ def test_search_hash_pattern_regexp end def test_search_string_empty - results = search("''", "StringLiteral[parts: []]") + results = search("", "''") - assert_equal 1, results.length + assert_empty results end def test_search_symbol_empty - results = search(":''", "DynaSymbol[parts: []]") + results = search("", ":''") + + assert_empty results + end + + def test_search_symbol_plain + results = search("1 + 2", "Binary[operator: :'+']") + + assert_equal 1, results.length + end + + def test_search_symbol + results = search("1 + 2", "Binary[operator: :+]") assert_equal 1, results.length end @@ -53,10 +121,7 @@ def test_search_symbol_empty private def search(source, query) - pattern = Pattern.new(query).compile - program = SyntaxTree.parse(source) - - Search.new(pattern).scan(program).to_a + SyntaxTree.search(source, query).to_a end end end diff --git a/test/test_helper.rb b/test/test_helper.rb index c46022ae..77627e26 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -2,10 +2,9 @@ require "simplecov" SimpleCov.start do - unless ENV["CI"] - add_filter("accept_methods_test.rb") - add_filter("idempotency_test.rb") - end + add_filter("idempotency_test.rb") unless ENV["CI"] + add_group("lib", "lib") + add_group("test", "test") end $LOAD_PATH.unshift(File.expand_path("../lib", __dir__)) @@ -61,6 +60,9 @@ def assert_syntax_tree(node) refute_includes(pretty, "#<") assert_includes(pretty, type) + # Assert that we can get back a new tree by using the mutation visitor. + assert_operator node, :===, node.accept(Visitor::MutationVisitor.new) + # Serialize the node to JSON, parse it back out, and assert that we have # found the expected type. json = node.to_json @@ -84,26 +86,6 @@ def assert_syntax_tree(node) end end -module SyntaxTree - module Plugin - # A couple of plugins modify the options hash on the formatter. They're - # modeled as files that should be required so that it's simple for the CLI - # and the library to use the same code path. In this case we're going to - # require the file for the plugin but ensure it doesn't make any lasting - # changes. - def self.options(path) - previous_options = SyntaxTree::Formatter::OPTIONS.dup - - begin - require path - SyntaxTree::Formatter::OPTIONS.dup - ensure - SyntaxTree::Formatter::OPTIONS.merge!(previous_options) - end - end - 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, @@ -151,9 +133,8 @@ def self.each_fixture # 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 + if comment&.start_with?(">=") + next if ruby_version < Gem::Version.new(comment.split[1]) end name = :"#{fixture}_#{index}" diff --git a/test/visitor_with_environment_test.rb b/test/visitor_with_environment_test.rb index b37bad16..cc4007fe 100644 --- a/test/visitor_with_environment_test.rb +++ b/test/visitor_with_environment_test.rb @@ -615,5 +615,45 @@ def test_double_nested_arguments assert_equal(1, argument.definitions[0].start_line) assert_equal(5, argument.usages[0].start_line) end + + class Resolver < Visitor + include WithEnvironment + + attr_reader :locals + + def initialize + @locals = [] + end + + def visit_assign(node) + level = 0 + environment = current_environment + level += 1 until (environment = environment.parent).nil? + + locals << [node.target.value.value, level] + super + end + end + + def test_class + source = <<~RUBY + module Level0 + level0 = 0 + + module Level1 + level1 = 1 + + class Level2 + level2 = 2 + end + end + end + RUBY + + visitor = Resolver.new + SyntaxTree.parse(source).accept(visitor) + + assert_equal [["level0", 0], ["level1", 1], ["level2", 2]], visitor.locals + end end end