diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 63d51a3c..7f5ac15c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -24,9 +24,28 @@ jobs: ruby-version: ${{ matrix.ruby }} - name: Test run: bundle exec rake test + + check: + name: Check + runs-on: ubuntu-latest + env: + CI: true + steps: + - uses: actions/checkout@master + - uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + ruby-version: '3.1' + - name: Check + run: | + bundle exec rake check + bundle exec rubocop + automerge: name: AutoMerge - needs: ci + needs: + - ci + - check runs-on: ubuntu-latest if: github.event_name == 'pull_request_target' && github.actor == 'dependabot[bot]' steps: diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 00000000..8c1bc99e --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,80 @@ +inherit_from: config/rubocop.yml + +AllCops: + DisplayCopNames: true + DisplayStyleGuide: true + NewCops: enable + SuggestExtensions: false + TargetRubyVersion: 2.7 + Exclude: + - '{bin,coverage,pkg,test/fixtures,vendor,tmp}/**/*' + - test.rb + +Layout/LineLength: + Max: 80 + +Lint/DuplicateBranch: + Enabled: false + +Lint/EmptyBlock: + Enabled: false + +Lint/InterpolationCheck: + Enabled: false + +Lint/MissingSuper: + Enabled: false + +Lint/UnusedMethodArgument: + AllowUnusedKeywordArguments: true + +Metrics: + Enabled: false + +Naming/MethodName: + Enabled: false + +Naming/MethodParameterName: + Enabled: false + +Naming/RescuedExceptionsVariableName: + PreferredName: error + +Style/ExplicitBlockArgument: + Enabled: false + +Style/FormatString: + EnforcedStyle: percent + +Style/GuardClause: + Enabled: false + +Style/IdenticalConditionalBranches: + Enabled: false + +Style/IfInsideElse: + Enabled: false + +Style/KeywordParametersOrder: + Enabled: false + +Style/MissingRespondToMissing: + Enabled: false + +Style/MutableConstant: + Enabled: false + +Style/NegatedIfElseCondition: + Enabled: false + +Style/NumericPredicate: + Enabled: false + +Style/ParallelAssignment: + Enabled: false + +Style/PerlBackrefs: + Enabled: false + +Style/SpecialGlobalVars: + Enabled: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a0eb40a..26e538c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,22 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a ## [Unreleased] +## [2.4.0] - 2022-05-07 + +### Added + +- [#65](https://github.com/ruby-syntax-tree/syntax_tree/pull/65) - Add a rubocop config at `config/rubocop.yml` that we can ship with the gem so folks can inherit from it to get their styling correct. +- [#65](https://github.com/ruby-syntax-tree/syntax_tree/pull/65) - Improve hash pattern formatting by a lot - multiple lines are now not so ugly. +- [#62](https://github.com/ruby-syntax-tree/syntax_tree/issues/62) - Add `options` as a method on `SyntaxTree::RegexpLiteral`, add it to pattern matching, and describe it using the `SyntaxTree::Visitor::FieldVisitor` class. +- [#69](https://github.com/ruby-syntax-tree/syntax_tree/pull/69) - The `construct_keys` option has been added to every `SyntaxTree::Node` descendant. This allows building a pattern match expression that can be used later. It is meant as a reflection API, not necessarily something that should be eval'd. +- [#69](https://github.com/ruby-syntax-tree/syntax_tree/pull/69) - You can now call `stree json` to get a JSON representation of your syntax tree. +- [#69](https://github.com/ruby-syntax-tree/syntax_tree/pull/69) - You can now call `stree match` to get a Ruby pattern matching expression to match against the given input. + +### Changed + +- [#69](https://github.com/ruby-syntax-tree/syntax_tree/pull/69) - Fixed a long-standing bug with pretty-print where if certain things were required in different orders you could end up with a bug in `PP` when calling pretty-print with a confusing error referring to inspect keys. +- [#69](https://github.com/ruby-syntax-tree/syntax_tree/pull/69) - `SyntaxTree.read` can now handle an empty file. + ## [2.3.1] - 2022-04-22 ### Changed @@ -193,7 +209,9 @@ 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/v2.3.0...HEAD +[unreleased]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v2.4.0...HEAD +[2.4.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v2.3.1...v2.4.0 +[2.3.1]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v2.3.0...v2.3.1 [2.3.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v2.2.0...v2.3.0 [2.2.0]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v2.1.1...v2.2.0 [2.1.1]: https://github.com/ruby-syntax-tree/syntax_tree/compare/v2.1.0...v2.1.1 diff --git a/Gemfile b/Gemfile index be173b20..73418542 100644 --- a/Gemfile +++ b/Gemfile @@ -3,3 +3,5 @@ source "https://rubygems.org" gemspec + +gem "rubocop" diff --git a/Gemfile.lock b/Gemfile.lock index 50166f2e..8357fd92 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,20 +1,40 @@ PATH remote: . specs: - syntax_tree (2.3.1) + syntax_tree (2.4.0) GEM remote: https://rubygems.org/ specs: + ast (2.4.2) docile (1.4.0) minitest (5.15.0) + parallel (1.22.1) + parser (3.1.2.0) + ast (~> 2.4.1) + rainbow (3.1.1) rake (13.0.6) + regexp_parser (2.3.1) + rexml (3.2.5) + rubocop (1.29.0) + parallel (~> 1.10) + parser (>= 3.1.0.0) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.17.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 1.4.0, < 3.0) + rubocop-ast (1.17.0) + parser (>= 3.1.1.0) + ruby-progressbar (1.11.0) simplecov (0.21.2) docile (~> 1.1) simplecov-html (~> 0.11) simplecov_json_formatter (~> 0.1) simplecov-html (0.12.3) simplecov_json_formatter (0.1.4) + unicode-display_width (2.1.0) PLATFORMS arm64-darwin-21 @@ -27,6 +47,7 @@ DEPENDENCIES bundler minitest rake + rubocop simplecov syntax_tree! diff --git a/README.md b/README.md index 384cc605..b0c916bd 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ It is built with only standard library dependencies. It additionally ships with - [ast](#ast) - [check](#check) - [format](#format) + - [json](#json) + - [match](#match) - [write](#write) - [Library](#library) - [SyntaxTree.read(filepath)](#syntaxtreereadfilepath) @@ -27,6 +29,7 @@ It is built with only standard library dependencies. It additionally ships with - [pretty_print(q)](#pretty_printq) - [to_json(*opts)](#to_jsonopts) - [format(q)](#formatq) + - [construct_keys](#construct_keys) - [Visitor](#visitor) - [visit_method](#visit_method) - [Language server](#language-server) @@ -34,6 +37,9 @@ It is built with only standard library dependencies. It additionally ships with - [textDocument/inlayHints](#textdocumentinlayhints) - [syntaxTree/visualizing](#syntaxtreevisualizing) - [Plugins](#plugins) +- [Integration](#integration) + - [RuboCop](#rubocop) + - [VSCode](#vscode) - [Contributing](#contributing) - [License](#license) @@ -122,6 +128,73 @@ For a file that contains `1 + 1`, you will receive: 1 + 1 ``` +### json + +This command will output a JSON representation of the syntax tree that is functionally equivalent to the input. This is mostly used in contexts where you need to access the tree from JavaScript or serialize it over a network. + +```sh +stree json path/to/file.rb +``` + +For a file that contains `1 + 1`, you will receive: + +```json +{ + "type": "program", + "location": [1, 0, 1, 6], + "statements": { + "type": "statements", + "location": [1, 0, 1, 6], + "body": [ + { + "type": "binary", + "location": [1, 0, 1, 5], + "left": { + "type": "int", + "location": [1, 0, 1, 1], + "value": "1", + "comments": [] + }, + "operator": "+", + "right": { + "type": "int", + "location": [1, 4, 1, 5], + "value": "1", + "comments": [] + }, + "comments": [] + } + ], + "comments": [] + }, + "comments": [] +} +``` + +### match + +This command will output a Ruby case-match expression that would match correctly against the input. + +```sh +stree match path/to/file.rb +``` + +For a file that contains `1 + 1`, you will receive: + +```ruby +SyntaxTree::Program[ + statements: SyntaxTree::Statements[ + body: [ + SyntaxTree::Binary[ + left: SyntaxTree::Int[value: "1"], + operator: :+, + right: SyntaxTree::Int[value: "1"] + ] + ] + ] +] +``` + ### write This command will format the listed files and write that formatted version back to the source files. Note that this overwrites the original content, to be sure to be using a version control system. @@ -223,6 +296,27 @@ formatter.output.join # => "1 + 1" ``` +### 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. + +```ruby +program = SyntaxTree.parse("1 + 1") +puts program.construct_keys + +# SyntaxTree::Program[ +# statements: SyntaxTree::Statements[ +# body: [ +# SyntaxTree::Binary[ +# left: SyntaxTree::Int[value: "1"], +# operator: :+, +# right: SyntaxTree::Int[value: "1"] +# ] +# ] +# ] +# ] +``` + ## Visitor If you want to operate over a set of nodes in the tree but don't want to walk the tree manually, the `Visitor` class makes it easy. `SyntaxTree::Visitor` is an implementation of the double dispatch visitor pattern. It works by the user defining visit methods that process nodes in the tree, which then call back to other visit methods to continue the descent. This is easier shown in code. @@ -328,6 +422,25 @@ Below are listed all of the "official" plugins hosted under the same GitHub orga * [SyntaxTree::JSON](https://github.com/ruby-syntax-tree/syntax_tree-json) for JSON. * [SyntaxTree::RBS](https://github.com/ruby-syntax-tree/syntax_tree-rbs) for the [RBS type language](https://github.com/ruby/rbs). +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. + +## Integration + +Syntax Tree's goal is to seemlessly integrate into your workflow. To this end, it provides a couple of additional tools beyond the CLI and the Ruby library. + +### RuboCop + +RuboCop and Syntax Tree serve different purposes, but there is overlap with some of RuboCop's functionality. Syntax Tree provides a RuboCop configuration file to disable rules that are redundant with Syntax Tree. To use this configuration file, add the following snippet to the top of your project's `.rubocop.yml`: + +```yaml +inherit_gem: + syntax_tree: config/rubocop.yml +``` + +### VSCode + +To integrate Syntax Tree into VSCode, you should use the official VSCode extension [ruby-syntax-tree/vscode-syntax-tree](https://github.com/ruby-syntax-tree/vscode-syntax-tree). + ## Contributing Bug reports and pull requests are welcome on GitHub at https://github.com/ruby-syntax-tree/syntax_tree. diff --git a/Rakefile b/Rakefile index 4caa98e2..4b3de39a 100644 --- a/Rakefile +++ b/Rakefile @@ -1,12 +1,34 @@ # frozen_string_literal: true -require 'bundler/gem_tasks' -require 'rake/testtask' +require "bundler/gem_tasks" +require "rake/testtask" Rake::TestTask.new(:test) do |t| - t.libs << 'test' - t.libs << 'lib' - t.test_files = FileList['test/**/*_test.rb'] + t.libs << "test" + t.libs << "lib" + t.test_files = FileList["test/**/*_test.rb"] end task default: :test + +FILEPATHS = %w[ + Gemfile + Rakefile + syntax_tree.gemspec + lib/**/*.rb + test/*.rb +].freeze + +task :syntax_tree do + $:.unshift File.expand_path("lib", __dir__) + require "syntax_tree" + require "syntax_tree/cli" +end + +task check: :syntax_tree do + exit SyntaxTree::CLI.run(["check"] + FILEPATHS) +end + +task format: :syntax_tree do + exit SyntaxTree::CLI.run(["write"] + FILEPATHS) +end diff --git a/config/rubocop.yml b/config/rubocop.yml new file mode 100644 index 00000000..eff5aede --- /dev/null +++ b/config/rubocop.yml @@ -0,0 +1,64 @@ +# Disabling all Layout/* rules, as they're unnecessary when the user is using +# Syntax Tree to handle all of the formatting. +Layout: + Enabled: false + +# Re-enable Layout/LineLength because certain cops that most projects use +# (e.g. Style/IfUnlessModifier) require Layout/LineLength to be enabled. +# By leaving it disabled, those rules will mis-fire. +# +# Users can always override these defaults in their own rubocop.yml files. +# https://github.com/prettier/plugin-ruby/issues/825 +Layout/LineLength: + Enabled: true + +Style/MultilineIfModifier: + Enabled: false + +# Syntax Tree will expand empty methods to put the end keyword on the subsequent +# line to reduce git diff noise. +Style/EmptyMethod: + EnforcedStyle: expanded + +# lambdas that are constructed with the lambda method call cannot be safely +# turned into lambda literals without removing a method call. +Style/Lambda: + Enabled: false + +# When method chains with multiple blocks are chained together, rubocop will let +# them pass if they're using braces but not if they're using do and end +# keywords. Because we will break individual blocks down to using keywords if +# they are multiline, this conflicts with rubocop. +Style/MultilineBlockChain: + Enabled: false + +# Syntax Tree by default uses double quotes, so changing the configuration here +# to match that. +Style/StringLiterals: + EnforcedStyle: double_quotes + +Style/StringLiteralsInInterpolation: + EnforcedStyle: double_quotes + +Style/QuotedSymbols: + EnforcedStyle: double_quotes + +# We let users have a little more freedom with symbol and words arrays. If the +# user only has an individual item like ["value"] then we don't bother +# converting it because it ends up being just noise. +Style/SymbolArray: + Enabled: false + +Style/WordArray: + Enabled: false + +# We don't support trailing commas in Syntax Tree by default, so just turning +# these off for now. +Style/TrailingCommaInArguments: + Enabled: false + +Style/TrailingCommaInArrayLiteral: + Enabled: false + +Style/TrailingCommaInHashLiteral: + Enabled: false diff --git a/lib/syntax_tree.rb b/lib/syntax_tree.rb index f1df71c5..c5e2d913 100644 --- a/lib/syntax_tree.rb +++ b/lib/syntax_tree.rb @@ -11,7 +11,9 @@ require_relative "syntax_tree/parser" require_relative "syntax_tree/version" require_relative "syntax_tree/visitor" +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/pretty_print_visitor" # If PrettyPrint::Align isn't defined, then we haven't gotten the updated @@ -28,6 +30,20 @@ end end +# When PP is running, it expects that everything that interacts with it is going +# to flow through PP.pp, since that's the main entry into the module from the +# perspective of its uses in core Ruby. In doing so, it calls guard_inspect_key +# at the top of the PP.pp method, which establishes some thread-local hashes to +# check for cycles in the pretty printed tree. This means that if you want to +# manually call pp on some object _before_ you have established these hashes, +# you're going to break everything. So this call ensures that those hashes have +# been set up before anything uses pp manually. +PP.new(+"", 0).guard_inspect_key {} + +# Syntax Tree is a suite of tools built on top of the internal CRuby parser. It +# provides the ability to generate a syntax tree from source, as well as the +# tools necessary to inspect and manipulate that syntax tree. It can be used to +# build formatters, linters, language servers, and more. module SyntaxTree # This holds references to objects that respond to both #parse and #format # so that we can use them in the CLI. @@ -61,8 +77,10 @@ def self.format(source) def self.read(filepath) encoding = File.open(filepath, "r") do |file| + break Encoding.default_external if file.eof? + header = file.readline - header += file.readline if header.start_with?("#!") + header += file.readline if !file.eof? && header.start_with?("#!") Ripper.new(header).tap(&:parse).encoding end diff --git a/lib/syntax_tree/cli.rb b/lib/syntax_tree/cli.rb index 1bf09cf7..64848ca6 100644 --- a/lib/syntax_tree/cli.rb +++ b/lib/syntax_tree/cli.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true module SyntaxTree + # Syntax Tree ships with the `stree` CLI, which can be used to inspect and + # manipulate Ruby code. This module is responsible for powering that CLI. module CLI # A utility wrapper around colored strings in the output. class Color @@ -46,7 +48,7 @@ def failure # An action of the CLI that prints out the AST for the given source. class AST < Action - def run(handler, filepath, source) + def run(handler, _filepath, source) pp handler.parse(source) end end @@ -83,9 +85,7 @@ def run(handler, filepath, source) warning = "[#{Color.yellow("warn")}] #{filepath}" formatted = handler.format(source) - if formatted != handler.format(formatted) - raise NonIdempotentFormatError - end + raise NonIdempotentFormatError if formatted != handler.format(formatted) rescue StandardError warn(warning) raise @@ -102,8 +102,8 @@ def failure # An action of the CLI that prints out the doc tree IR for the given source. class Doc < Action - def run(handler, filepath, source) - formatter = Formatter.new([]) + def run(handler, _filepath, source) + formatter = Formatter.new(source, []) handler.parse(source).format(formatter) pp formatter.groups.first end @@ -111,11 +111,28 @@ def run(handler, filepath, source) # An action of the CLI that formats the input source and prints it out. class Format < Action - def run(handler, filepath, source) + def run(handler, _filepath, source) puts handler.format(source) end end + # An action of the CLI that converts the source into its equivalent JSON + # representation. + class Json < Action + def run(handler, _filepath, source) + object = Visitor::JSONVisitor.new.visit(handler.parse(source)) + puts JSON.pretty_generate(object) + end + end + + # An action of the CLI that outputs a pattern-matching Ruby expression that + # would match the input given. + class Match < Action + def run(handler, _filepath, source) + puts handler.parse(source).construct_keys + end + end + # An action of the CLI that formats the input source and writes the # formatted output back to the file. class Write < Action @@ -154,6 +171,12 @@ def run(handler, filepath, source) #{Color.bold("stree format [OPTIONS] [FILE]")} Print out the formatted version of the given files + #{Color.bold("stree json [OPTIONS] [FILE]")} + Print out the JSON representation of the given files + + #{Color.bold("stree match [OPTIONS] [FILE]")} + Print out a pattern-matching Ruby expression that would match the given files + #{Color.bold("stree help")} Display this help message @@ -201,6 +224,10 @@ def run(argv) Debug.new when "doc" Doc.new + when "j", "json" + Json.new + when "m", "match" + Match.new when "f", "format" Format.new when "w", "write" @@ -212,7 +239,7 @@ def run(argv) # If we're not reading from stdin and the user didn't supply and # filepaths to be read, then we exit with the usage message. - if STDIN.tty? && arguments.empty? + if $stdin.tty? && arguments.empty? warn(HELP) return 1 end @@ -239,18 +266,11 @@ def run(argv) action.run(handler, filepath, source) rescue Parser::ParseError => error warn("Error: #{error.message}") - - if error.lineno - highlight_error(error, source) - else - warn(error.message) - warn(error.backtrace) - end - + highlight_error(error, source) errored = true rescue Check::UnformattedError, Debug::NonIdempotentFormatError errored = true - rescue => error + rescue StandardError => error warn(error.message) warn(error.backtrace) errored = true @@ -268,18 +288,20 @@ def run(argv) private def each_file(arguments) - if STDIN.tty? + if $stdin.tty? || arguments.any? arguments.each do |pattern| - Dir.glob(pattern).each do |filepath| - next unless File.file?(filepath) - - handler = HANDLERS[File.extname(filepath)] - source = handler.read(filepath) - yield handler, filepath, source - end + Dir + .glob(pattern) + .each do |filepath| + next unless File.file?(filepath) + + handler = HANDLERS[File.extname(filepath)] + source = handler.read(filepath) + yield handler, filepath, source + end end else - yield HANDLERS[".rb"], :stdin, STDIN.read + yield HANDLERS[".rb"], :stdin, $stdin.read end end @@ -310,7 +332,21 @@ def highlight_error(error, source) # Take a line of Ruby source and colorize the output. def colorize_line(line) require "irb" - IRB::Color.colorize_code(line, complete: false, ignore_error: true) + IRB::Color.colorize_code(line, **colorize_options) + end + + # These are the options we're going to pass into IRB::Color.colorize_code. + # Since we support multiple versions of IRB, we're going to need to do + # some reflection to make sure we always pass valid options. + def colorize_options + options = { complete: false } + + parameters = IRB::Color.method(:colorize_code).parameters + if parameters.any? { |(_type, name)| name == :ignore_error } + options[:ignore_error] = true + end + + options end end end diff --git a/lib/syntax_tree/formatter.rb b/lib/syntax_tree/formatter.rb index 7015a837..9959421a 100644 --- a/lib/syntax_tree/formatter.rb +++ b/lib/syntax_tree/formatter.rb @@ -41,11 +41,12 @@ def format(node, stackable: true) # If the node has a stree-ignore comment right before it, then we're # going to just print out the node as it was seen in the source. - if leading.last&.ignore? - doc = text(source[node.location.start_char...node.location.end_char]) - else - doc = node.format(self) - end + doc = + if leading.last&.ignore? + text(source[node.location.start_char...node.location.end_char]) + else + node.format(self) + end # Print all comments that were found after the node. trailing.each do |comment| diff --git a/lib/syntax_tree/language_server.rb b/lib/syntax_tree/language_server.rb index 177a39a1..1e305cca 100644 --- a/lib/syntax_tree/language_server.rb +++ b/lib/syntax_tree/language_server.rb @@ -7,10 +7,15 @@ require_relative "language_server/inlay_hints" module SyntaxTree + # Syntax Tree additionally ships with a language server conforming to the + # language server protocol. It can be invoked through the CLI by running: + # + # stree lsp + # class LanguageServer attr_reader :input, :output - def initialize(input: STDIN, output: STDOUT) + def initialize(input: $stdin, output: $stdout) @input = input.binmode @output = output.binmode end @@ -21,7 +26,7 @@ def run hash[uri] = File.binread(CGI.unescape(URI.parse(uri).path)) end - while headers = input.gets("\r\n\r\n") + while (headers = input.gets("\r\n\r\n")) source = input.read(headers[/Content-Length: (\d+)/i, 1].to_i) request = JSON.parse(source, symbolize_names: true) @@ -29,26 +34,46 @@ def run in { method: "initialize", id: } store.clear write(id: id, result: { capabilities: capabilities }) - in { method: "initialized" } + in method: "initialized" # ignored - in { method: "shutdown" } + in method: "shutdown" store.clear return - in { method: "textDocument/didChange", params: { textDocument: { uri: }, contentChanges: [{ text: }, *] } } + in { + method: "textDocument/didChange", + params: { textDocument: { uri: }, contentChanges: [{ text: }, *] } + } store[uri] = text - in { method: "textDocument/didOpen", params: { textDocument: { uri:, text: } } } + in { + method: "textDocument/didOpen", + params: { textDocument: { uri:, text: } } + } store[uri] = text - in { method: "textDocument/didClose", params: { textDocument: { uri: } } } + in { + method: "textDocument/didClose", params: { textDocument: { uri: } } + } store.delete(uri) - in { method: "textDocument/formatting", id:, params: { textDocument: { uri: } } } + in { + method: "textDocument/formatting", + id:, + params: { textDocument: { uri: } } + } write(id: id, result: [format(store[uri])]) - in { method: "textDocument/inlayHints", id:, params: { textDocument: { uri: } } } + in { + method: "textDocument/inlayHints", + id:, + params: { textDocument: { uri: } } + } write(id: id, result: inlay_hints(store[uri])) - in { method: "syntaxTree/visualizing", id:, params: { textDocument: { uri: } } } + in { + method: "syntaxTree/visualizing", + id:, + params: { textDocument: { uri: } } + } output = [] PP.pp(SyntaxTree.parse(store[uri]), output) write(id: id, result: output.join) - in { method: %r{\$/.+} } + in method: %r{\$/.+} # ignored else raise "Unhandled: #{request}" @@ -61,15 +86,24 @@ def run def capabilities { documentFormattingProvider: true, - textDocumentSync: { change: 1, openClose: true } + textDocumentSync: { + change: 1, + openClose: true + } } end def format(source) { range: { - start: { line: 0, character: 0 }, - end: { line: source.lines.size + 1, character: 0 } + start: { + line: 0, + character: 0 + }, + end: { + line: source.lines.size + 1, + character: 0 + } }, newText: SyntaxTree.format(source) } @@ -88,6 +122,8 @@ def inlay_hints(source) after: inlay_hints.after.map(&serialize) } rescue Parser::ParseError + # If there is a parse error, then we're not going to return any inlay + # hints for this source. end def write(value) diff --git a/lib/syntax_tree/language_server/inlay_hints.rb b/lib/syntax_tree/language_server/inlay_hints.rb index 0bed2a80..69fc5ce4 100644 --- a/lib/syntax_tree/language_server/inlay_hints.rb +++ b/lib/syntax_tree/language_server/inlay_hints.rb @@ -2,14 +2,79 @@ module SyntaxTree class LanguageServer - class InlayHints - attr_reader :before, :after + # This class provides inlay hints for the language server. It is loosely + # designed around the LSP spec, but existed before the spec was finalized so + # is a little different for now. + # + # For more information, see the spec here: + # https://github.com/microsoft/language-server-protocol/issues/956. + # + class InlayHints < Visitor + attr_reader :stack, :before, :after def initialize + @stack = [] @before = Hash.new { |hash, key| hash[key] = +"" } @after = Hash.new { |hash, key| hash[key] = +"" } end + def visit(node) + stack << node + result = super + stack.pop + result + end + + # Adds parentheses around assignments contained within the default values + # of parameters. For example, + # + # def foo(a = b = c) + # end + # + # becomes + # + # def foo(a = ā‚b = cā‚Ž) + # end + # + def visit_assign(node) + parentheses(node.location) if stack[-2].is_a?(Params) + end + + # Adds parentheses around binary expressions to make it clear which + # subexpression will be evaluated first. For example, + # + # a + b * c + # + # becomes + # + # a + ā‚b * cā‚Ž + # + def visit_binary(node) + case stack[-2] + in Assign | OpAssign + parentheses(node.location) + in Binary[operator: operator] if operator != node.operator + parentheses(node.location) + else + end + end + + # Adds parentheses around ternary operators contained within certain + # expressions where it could be confusing which subexpression will get + # evaluated first. For example, + # + # a ? b : c ? d : e + # + # becomes + # + # a ? b : ā‚c ? d : eā‚Ž + # + def visit_if_op(node) + if stack[-2] in Assign | Binary | IfOp | OpAssign + parentheses(node.location) + end + end + # Adds the implicitly rescued StandardError into a bare rescue clause. For # example, # @@ -23,54 +88,38 @@ def initialize # rescue StandardError # end # - def bare_rescue(location) - after[location.start_char + "rescue".length] << " StandardError" + def visit_rescue(node) + if node.exception.nil? + after[node.location.start_char + "rescue".length] << " StandardError" + end end - # Adds implicit parentheses around certain expressions to make it clear - # which subexpression will be evaluated first. For example, + # Adds parentheses around unary statements using the - operator that are + # contained within Binary nodes. For example, # - # a + b * c + # -a + b # # becomes # - # a + ā‚b * cā‚Ž + # ā‚-aā‚Ž + b # - def precedence_parentheses(location) - before[location.start_char] << "ā‚" - after[location.end_char] << "ā‚Ž" + def visit_unary(node) + if stack[-2].is_a?(Binary) && (node.operator == "-") + parentheses(node.location) + end end def self.find(program) - inlay_hints = new - queue = [[nil, program]] - - until queue.empty? - parent_node, child_node = queue.shift - - child_node.child_nodes.each do |grand_child_node| - queue << [child_node, grand_child_node] if grand_child_node - end + visitor = new + visitor.visit(program) + visitor + end - case [parent_node, child_node] - in _, Rescue[exception: nil, location:] - inlay_hints.bare_rescue(location) - in Assign | Binary | IfOp | OpAssign, IfOp[location:] - inlay_hints.precedence_parentheses(location) - in Assign | OpAssign, Binary[location:] - inlay_hints.precedence_parentheses(location) - in Binary[operator: parent_oper], Binary[operator: child_oper, location:] if parent_oper != child_oper - inlay_hints.precedence_parentheses(location) - in Binary, Unary[operator: "-", location:] - inlay_hints.precedence_parentheses(location) - in Params, Assign[location:] - inlay_hints.precedence_parentheses(location) - else - # do nothing - end - end + private - inlay_hints + def parentheses(location) + before[location.start_char] << "ā‚" + after[location.end_char] << "ā‚Ž" end end end diff --git a/lib/syntax_tree/node.rb b/lib/syntax_tree/node.rb index 0a4c0fdf..07bafb00 100644 --- a/lib/syntax_tree/node.rb +++ b/lib/syntax_tree/node.rb @@ -3,9 +3,21 @@ module SyntaxTree # Represents the location of a node in the tree from the source code. class Location - attr_reader :start_line, :start_char, :start_column, :end_line, :end_char, :end_column + attr_reader :start_line, + :start_char, + :start_column, + :end_line, + :end_char, + :end_column - def initialize(start_line:, start_char:, start_column:, end_line:, end_char:, end_column:) + def initialize( + start_line:, + start_char:, + start_column:, + end_line:, + end_char:, + end_column: + ) @start_line = start_line @start_char = start_char @start_column = start_column @@ -39,7 +51,7 @@ def deconstruct [start_line, start_char, start_column, end_line, end_char, end_column] end - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { start_line: start_line, start_char: start_char, @@ -62,7 +74,14 @@ def self.token(line:, char:, column:, size:) end def self.fixed(line:, char:, column:) - new(start_line: line, start_char: char, start_column: column, end_line: line, end_char: char, end_column: column) + new( + start_line: line, + start_char: char, + start_column: column, + end_line: line, + end_char: char, + end_column: column + ) end end @@ -102,6 +121,10 @@ def to_json(*opts) visitor = Visitor::JSONVisitor.new visitor.visit(self).to_json(*opts) end + + def construct_keys + PP.format(+"") { |q| Visitor::MatchVisitor.new(q).visit(self) } + end end # BEGINBlock represents the use of the +BEGIN+ keyword, which hooks into the @@ -140,7 +163,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { lbrace: lbrace, statements: statements, @@ -192,7 +215,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location, comments: comments } end @@ -243,7 +266,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { lbrace: lbrace, statements: statements, @@ -298,7 +321,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location, comments: comments } end @@ -323,6 +346,8 @@ def format(q) # symbols (note that this includes dynamic symbols like # :"left-#{middle}-right"). class Alias < 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 attr_reader :argument @@ -374,7 +399,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { left: left, right: right, location: location, comments: comments } end @@ -435,7 +460,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { collection: collection, index: index, @@ -496,7 +521,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { collection: collection, index: index, @@ -558,7 +583,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { arguments: arguments, location: location, comments: comments } end @@ -606,7 +631,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { parts: parts, location: location, comments: comments } end @@ -642,7 +667,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location, comments: comments } end @@ -679,7 +704,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location, comments: comments } end @@ -729,7 +754,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location, comments: comments } end @@ -745,6 +770,7 @@ def format(q) # [one, two, three] # class ArrayLiteral < Node + # Formats an array of multiple simple string literals into the %w syntax. class QWordsFormatter # [Args] the contents of the array attr_reader :contents @@ -761,7 +787,7 @@ def format(q) if part.is_a?(StringLiteral) q.format(part.parts.first) else - q.text(part.value[1..-1]) + q.text(part.value[1..]) end end end @@ -770,6 +796,7 @@ def format(q) end end + # Formats an array of multiple simple symbol literals into the %i syntax. class QSymbolsFormatter # [Args] the contents of the array attr_reader :contents @@ -791,6 +818,28 @@ def format(q) end end + # Formats an array that contains only a list of variable references. To make + # things simpler, if there are a bunch, we format them all using the "fill" + # algorithm as opposed to breaking them into a ton of lines. For example, + # + # [foo, bar, baz] + # + # instead of becoming: + # + # [ + # foo, + # bar, + # baz + # ] + # + # would instead become: + # + # [ + # foo, bar, + # baz + # ] + # + # provided the line length was hit between `bar` and `baz`. class VarRefsFormatter # [Args] the contents of the array attr_reader :contents @@ -816,6 +865,9 @@ def format(q) end end + # This is a special formatter used if the array literal contains no values + # but _does_ contain comments. In this case we do some special formatting to + # make sure the comments gets indented properly. class EmptyWithCommentsFormatter # [LBracket] the opening bracket attr_reader :lbracket @@ -865,7 +917,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { lbracket: lbracket, contents: contents, @@ -951,7 +1003,8 @@ def var_refs?(q) # If we have an empty array that contains only comments, then we're going # to do some special printing to ensure they get indented correctly. def empty_with_comments? - contents.nil? && lbracket.comments.any? && lbracket.comments.none?(&:inline?) + contents.nil? && lbracket.comments.any? && + lbracket.comments.none?(&:inline?) end end @@ -973,6 +1026,7 @@ def empty_with_comments? # and an optional array of positional matches that occur after the splat. # All of the in clauses above would create an AryPtn node. class AryPtn < Node + # Formats the optional splat of an array pattern. class RestFormatter # [VarField] the identifier that represents the remaining positionals attr_reader :value @@ -1035,7 +1089,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { constant: constant, requireds: requireds, @@ -1067,6 +1121,8 @@ def format(q) q.text("[") q.seplist(parts) { |part| q.format(part) } q.text("]") + elsif parts.empty? + q.text("[]") else q.group { q.seplist(parts) { |part| q.format(part) } } end @@ -1077,7 +1133,8 @@ def format(q) module AssignFormatting def self.skip_indent?(value) case value - in ArrayLiteral | HashLiteral | Heredoc | Lambda | QSymbols | QWords | Symbols | Words + in ArrayLiteral | HashLiteral | Heredoc | Lambda | QSymbols | QWords | + Symbols | Words true in Call[receiver:] skip_indent?(receiver) @@ -1123,7 +1180,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { target: target, value: value, location: location, comments: comments } end @@ -1185,12 +1242,12 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { key: key, value: value, location: location, comments: comments } end def format(q) - if value&.is_a?(HashLiteral) + if value.is_a?(HashLiteral) format_contents(q) else q.group { format_contents(q) } @@ -1243,7 +1300,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location, comments: comments } end @@ -1281,7 +1338,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location, comments: comments } end @@ -1316,7 +1373,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location, comments: comments } end @@ -1329,21 +1386,28 @@ def format(q) # hash or bare hash. It first determines if every key in the hash can use # labels. If it can, it uses labels. Otherwise it uses hash rockets. module HashKeyFormatter + # Formats the keys of a hash literal using labels. class Labels + LABEL = /^[@$_A-Za-z]([_A-Za-z0-9]*)?([!_=?A-Za-z0-9])?$/ + def format_key(q, key) case key - when Label + in Label q.format(key) - when SymbolLiteral + in SymbolLiteral q.format(key.value) q.text(":") - when DynaSymbol + in DynaSymbol[parts: [TStringContent[value: LABEL] => part]] + q.format(part) + q.text(":") + in DynaSymbol q.format(key) q.text(":") end end end + # Formats the keys of a hash literal using hash rockets. class Rockets def format_key(q, key) case key @@ -1417,7 +1481,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { assocs: assocs, location: location, comments: comments } end @@ -1459,7 +1523,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { bodystmt: bodystmt, location: location, comments: comments } end @@ -1507,7 +1571,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { statement: statement, location: location, comments: comments } end @@ -1567,7 +1631,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { left: left, operator: operator, @@ -1682,7 +1746,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { params: params, locals: locals, location: location, comments: comments } end @@ -1726,7 +1790,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { name: name, location: location, comments: comments } end @@ -1822,7 +1886,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { statements: statements, rescue_clause: rescue_clause, @@ -1868,6 +1932,7 @@ def format(q) # 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 @@ -1933,9 +1998,9 @@ def format(q) end q.group do - q.if_break { format_break(q, break_opening, break_closing) }.if_flat do - format_flat(q, flat_opening, flat_closing) - end + q + .if_break { format_break(q, break_opening, break_closing) } + .if_flat { format_flat(q, flat_opening, flat_closing) } end end @@ -2060,7 +2125,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { lbrace: lbrace, block_var: block_var, @@ -2099,7 +2164,11 @@ def format(q) # # break # - in [Paren[contents: { body: [ArrayLiteral[contents: { parts: [_, _, *] }] => array] }]] + in [Paren[ + contents: { + body: [ArrayLiteral[contents: { parts: [_, _, *] }] => array] + } + ]] # Here we have a single argument that is a set of parentheses wrapping # an array literal that has at least 2 elements. We're going to print # the contents of the array directly. This would be like if we had: @@ -2255,7 +2324,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { arguments: arguments, location: location, comments: comments } end @@ -2287,6 +2356,20 @@ def format(q) end end + # This is probably the most complicated formatter in this file. It's + # responsible for formatting chains of method calls, with or without arguments + # or blocks. In general, we want to go from something like + # + # foo.bar.baz + # + # to + # + # foo + # .bar + # .baz + # + # Of course there are a lot of caveats to that, including trailing operators + # when necessary, where comments are places, how blocks are aligned, etc. class CallChainFormatter # [Call | MethodAddBlock] the top of the call chain attr_reader :node @@ -2301,7 +2384,7 @@ def format(q) # First, walk down the chain until we get to the point where we're not # longer at a chainable node. - while true + loop do case children.last in Call[receiver: Call] children << children.last.receiver @@ -2321,7 +2404,7 @@ def format(q) # block. For more details, see # https://github.com/prettier/plugin-ruby/issues/863. parents = q.parents.take(4) - if parent = parents[2] + 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. @@ -2335,7 +2418,11 @@ def format(q) end if children.length >= threshold - q.group { q.if_break { format_chain(q, children) }.if_flat { node.format_contents(q) } } + q.group do + q + .if_break { format_chain(q, children) } + .if_flat { node.format_contents(q) } + end else node.format_contents(q) end @@ -2346,9 +2433,9 @@ def format_chain(q, children) # chain of calls without arguments except for the last one. This is common # enough in Ruby source code that it's worth the extra complexity here. empty_except_last = - children.drop(1).all? do |child| - child.is_a?(Call) && child.arguments.nil? - end + children + .drop(1) + .all? { |child| child.is_a?(Call) && 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 @@ -2368,9 +2455,12 @@ def format_chain(q, children) # and a trailing operator. skip_operator = false - while child = children.pop + while (child = children.pop) case child - in Call[receiver: Call[message: { value: "where" }], message: { value: "not" }] + in Call[ + receiver: Call[message: { value: "where" }], + message: { value: "not" } + ] # This is very specialized behavior wherein we group # .where.not calls together because it looks better. For more # information, see @@ -2432,16 +2522,25 @@ def self.chained?(node) # want to indent the first call. So we'll pop off the first children and # format it separately here. def attach_directly?(node) - [ArrayLiteral, HashLiteral, Heredoc, If, Unless, XStringLiteral] - .include?(node.receiver.class) + [ArrayLiteral, HashLiteral, Heredoc, If, Unless, XStringLiteral].include?( + node.receiver.class + ) end - def format_child(q, child, skip_comments: false, skip_operator: false, skip_attached: false) + def format_child( + q, + child, + skip_comments: false, + skip_operator: false, + skip_attached: false + ) # First, format the actual contents of the child. case child in Call q.group do - q.format(CallOperatorFormatter.new(child.operator)) unless skip_operator + unless skip_operator + q.format(CallOperatorFormatter.new(child.operator)) + end q.format(child.message) if child.message != :call child.format_arguments(q) unless skip_attached end @@ -2513,7 +2612,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { receiver: receiver, operator: operator, @@ -2528,8 +2627,13 @@ 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?(q.parent) && CallChainFormatter.chained?(receiver) - q.group { q.if_break { CallChainFormatter.new(self).format(q) }.if_flat { format_contents(q) } } + if !CallChainFormatter.chained?(q.parent) && + CallChainFormatter.chained?(receiver) + q.group do + q + .if_break { CallChainFormatter.new(self).format(q) } + .if_flat { format_contents(q) } + end else format_contents(q) end @@ -2618,7 +2722,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { keyword: keyword, value: value, @@ -2683,7 +2787,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, operator: operator, @@ -2772,7 +2876,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { constant: constant, superclass: superclass, @@ -2837,7 +2941,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location } end end @@ -2875,7 +2979,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { message: message, arguments: arguments, @@ -2957,7 +3061,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { receiver: receiver, operator: operator, @@ -3079,7 +3183,7 @@ def trailing? end def ignore? - value[1..-1].strip == "stree-ignore" + value[1..].strip == "stree-ignore" end def comments @@ -3096,7 +3200,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, inline: inline, location: location } end @@ -3142,7 +3246,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location, comments: comments } end @@ -3184,7 +3288,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { parent: parent, constant: constant, @@ -3231,7 +3335,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { parent: parent, constant: constant, @@ -3276,7 +3380,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { constant: constant, location: location, comments: comments } end @@ -3312,7 +3416,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location, comments: comments } end @@ -3356,7 +3460,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { name: name, params: params, @@ -3441,7 +3545,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { target: target, operator: operator, @@ -3509,7 +3613,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location, comments: comments } end @@ -3575,7 +3679,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { target: target, operator: operator, @@ -3650,7 +3754,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { keyword: keyword, block_var: block_var, @@ -3730,7 +3834,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { left: left, right: right, location: location, comments: comments } end @@ -3778,7 +3882,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { left: left, right: right, location: location, comments: comments } end @@ -3800,7 +3904,7 @@ module Quotes # quotes, then single quotes would deactivate it.) def self.locked?(node) node.parts.any? do |part| - part.is_a?(TStringContent) && part.value.match?(/#[@${]|[\\]/) + part.is_a?(TStringContent) && part.value.match?(/\\|#[@${]/) end end @@ -3865,7 +3969,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { parts: parts, quote: quote, location: location, comments: comments } end @@ -3906,9 +4010,14 @@ def quotes(q) matching = Quotes.matching(quote[2]) pattern = /[\n#{Regexp.escape(matching)}'"]/ - if parts.any? { |part| - part.is_a?(TStringContent) && part.value.match?(pattern) - } + # This check is to ensure we don't find a matching quote inside of the + # symbol that would be confusing. + matched = + parts.any? do |part| + part.is_a?(TStringContent) && part.value.match?(pattern) + end + + if matched [quote, matching] elsif Quotes.locked?(self) ["#{":" unless hash_key}'", "'"] @@ -3917,7 +4026,7 @@ def quotes(q) end elsif Quotes.locked?(self) if quote.start_with?(":") - [hash_key ? quote[1..-1] : quote, quote[1..-1]] + [hash_key ? quote[1..] : quote, quote[1..]] else [hash_key ? quote : ":#{quote}", quote] end @@ -3960,7 +4069,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { keyword: keyword, statements: statements, @@ -4026,7 +4135,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { predicate: predicate, statements: statements, @@ -4098,7 +4207,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location } end @@ -4133,7 +4242,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location } end end @@ -4163,7 +4272,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location } end end @@ -4195,7 +4304,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location } end end @@ -4234,7 +4343,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { keyword: keyword, statements: statements, @@ -4288,7 +4397,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location, comments: comments } end @@ -4331,7 +4440,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, arguments: arguments, @@ -4343,7 +4452,8 @@ def deconstruct_keys(keys) def format(q) q.format(value) - if arguments.is_a?(ArgParen) && arguments.arguments.nil? && !value.is_a?(Const) + 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 @@ -4390,7 +4500,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { parent: parent, operator: operator, @@ -4436,7 +4546,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location, comments: comments } end @@ -4488,7 +4598,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { constant: constant, left: left, @@ -4552,7 +4662,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { index: index, collection: collection, @@ -4609,7 +4719,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location, comments: comments } end @@ -4623,6 +4733,9 @@ def format(q) # { key => value } # class HashLiteral < Node + # This is a special formatter used if the hash literal contains no values + # but _does_ contain comments. In this case we do some special formatting to + # make sure the comments gets indented properly. class EmptyWithCommentsFormatter # [LBrace] the opening brace attr_reader :lbrace @@ -4672,7 +4785,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { lbrace: lbrace, assocs: assocs, location: location, comments: comments } end @@ -4741,7 +4854,14 @@ 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:, + comments: [] + ) @beginning = beginning @ending = ending @dedent = dedent @@ -4760,7 +4880,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { beginning: beginning, location: location, @@ -4775,8 +4895,12 @@ def format(q) # prettyprint module. It's when you want to force a newline, but don't # want to force the break parent. breakable = -> do - q.target << - PrettyPrint::Breakable.new(" ", 1, indent: false, force: true) + q.target << PrettyPrint::Breakable.new( + " ", + 1, + indent: false, + force: true + ) end q.group do @@ -4832,7 +4956,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location, comments: comments } end @@ -4849,6 +4973,7 @@ def format(q) # end # class HshPtn < Node + # Formats a key-value pair in a hash pattern. The value is optional. class KeywordFormatter # [Label] the keyword being used attr_reader :key @@ -4875,6 +5000,7 @@ def format(q) end end + # Formats the optional double-splat from the pattern. class KeywordRestFormatter # [VarField] the parameter that matches the remaining keywords attr_reader :keyword_rest @@ -4924,7 +5050,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { constant: constant, keywords: keywords, @@ -4947,27 +5073,52 @@ def format(q) q.text(" then") if !constant && keyword_rest && keyword_rest.value.nil? end + # If there is a constant, we're going to format to have the constant name + # first and then use brackets. if constant - q.format(constant) - q.group(0, "[", "]", &contents) + q.group do + q.format(constant) + q.text("[") + q.indent do + q.breakable("") + contents.call + end + q.breakable("") + q.text("]") + end return end + # If there's nothing at all, then we're going to use empty braces. if parts.empty? q.text("{}") - elsif PATTERNS.include?(q.parent.class) - q.text("{ ") - contents.call - q.text(" }") - else + return + end + + # If there's only one pair, then we'll just print the contents provided + # we're not inside another pattern. + if !PATTERNS.include?(q.parent.class) && parts.size == 1 contents.call + return + end + + # Otherwise, we're going to always use braces to make it clear it's a hash + # pattern. + q.group do + q.text("{") + q.indent do + q.breakable + contents.call + end + q.breakable + q.text("}") end end end # The list of nodes that represent patterns inside of pattern matching so that # when a pattern is being printed it knows if it's nested. - PATTERNS = [AryPtn, Binary, FndPtn, HshPtn, RAssign] + PATTERNS = [AryPtn, Binary, FndPtn, HshPtn, RAssign].freeze # Ident represents an identifier anywhere in code. It can represent a very # large number of things, depending on where it is in the syntax tree. @@ -4997,7 +5148,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location, comments: comments } end @@ -5014,7 +5165,7 @@ module ContainsAssignment def self.call(parent) queue = [parent] - while node = queue.shift + while (node = queue.shift) return true if [Assign, MAssign, OpAssign].include?(node.class) queue += node.child_nodes end @@ -5041,9 +5192,12 @@ def call(q, node) else # Otherwise, we're going to check the conditional for certain cases. case node - in { predicate: Assign | Command | CommandCall | MAssign | OpAssign } + in predicate: Assign | Command | CommandCall | MAssign | OpAssign false - in { statements: { body: [truthy] }, consequent: Else[statements: { body: [falsy] }] } + in { + statements: { body: [truthy] }, + consequent: Else[statements: { body: [falsy] }] + } ternaryable?(truthy) && ternaryable?(falsy) else false @@ -5054,8 +5208,8 @@ def call(q, node) private # Certain expressions cannot be reduced to a ternary without adding - # parentheses around them. In this case we say they cannot be ternaried and - # default instead to breaking them into multiple lines. + # parentheses around them. In this case we say they cannot be ternaried + # and default instead to breaking them into multiple lines. def ternaryable?(statement) # This is a list of nodes that should not be allowed to be a part of a # ternary clause. @@ -5113,13 +5267,15 @@ def format(q) 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) + 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 @@ -5148,51 +5304,53 @@ def format_break(q, force:) def format_ternary(q) q.group do - q.if_break do - q.text("#{keyword} ") - q.nest(keyword.length + 1) { q.format(node.predicate) } - - q.indent do - q.breakable - q.format(node.statements) - end + q + .if_break do + q.text("#{keyword} ") + q.nest(keyword.length + 1) { q.format(node.predicate) } - q.breakable - q.group do - q.format(node.consequent.keyword) q.indent do - # This is a very special case of breakable where we want to force - # it into the output but we _don't_ want to explicitly break the - # parent. If a break-parent shows up in the tree, then it's going - # to force it all the way up to the tree, which is going to negate - # the ternary. Maybe this should be an option in prettyprint? As - # in force: :no_break_parent or something. - q.target << PrettyPrint::Breakable.new(" ", 1, force: true) - q.format(node.consequent.statements) + q.breakable + q.format(node.statements) end - end - q.breakable - q.text("end") - end.if_flat do - Parentheses.flat(q) do - q.format(node.predicate) - q.text(" ? ") + q.breakable + q.group do + q.format(node.consequent.keyword) + q.indent do + # This is a very special case of breakable where we want to + # force it into the output but we _don't_ want to explicitly + # break the parent. If a break-parent shows up in the tree, then + # it's going to force it all the way up to the tree, which is + # going to negate the ternary. Maybe this should be an option in + # prettyprint? As in force: :no_break_parent or something. + q.target << PrettyPrint::Breakable.new(" ", 1, force: true) + q.format(node.consequent.statements) + end + end + + q.breakable + q.text("end") + end + .if_flat do + Parentheses.flat(q) do + q.format(node.predicate) + q.text(" ? ") - statements = [node.statements, node.consequent.statements] - statements.reverse! if keyword == "unless" + statements = [node.statements, node.consequent.statements] + statements.reverse! if keyword == "unless" - q.format(statements[0]) - q.text(" : ") - q.format(statements[1]) + q.format(statements[0]) + q.text(" : ") + q.format(statements[1]) + end end - end end end def contains_conditional? case node - in { statements: { body: [If | IfMod | IfOp | Unless | UnlessMod] } } + in statements: { body: [If | IfMod | IfOp | Unless | UnlessMod] } true else false @@ -5242,7 +5400,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { predicate: predicate, statements: statements, @@ -5292,7 +5450,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { predicate: predicate, truthy: truthy, @@ -5310,7 +5468,8 @@ def format(q) Yield0, ZSuper ] - if q.parent.is_a?(Paren) || force_flat.include?(truthy.class) || force_flat.include?(falsy.class) + if q.parent.is_a?(Paren) || force_flat.include?(truthy.class) || + force_flat.include?(falsy.class) q.group { format_flat(q) } return end @@ -5430,7 +5589,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { statement: statement, predicate: predicate, @@ -5471,7 +5630,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location, comments: comments } end @@ -5518,7 +5677,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { pattern: pattern, statements: statements, @@ -5577,7 +5736,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location, comments: comments } end @@ -5587,7 +5746,7 @@ def format(q) # the values, then we're going to insert them every 3 characters # starting from the right. index = (value.length + 2) % 3 - q.text(" #{value}"[index..-1].scan(/.../).join("_").strip) + q.text(" #{value}"[index..].scan(/.../).join("_").strip) else q.text(value) end @@ -5621,7 +5780,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location, comments: comments } end @@ -5666,7 +5825,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location, comments: comments } end @@ -5703,7 +5862,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { name: name, location: location, comments: comments } end @@ -5749,7 +5908,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location, comments: comments } end @@ -5784,7 +5943,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location } end end @@ -5820,7 +5979,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { params: params, statements: statements, @@ -5842,25 +6001,27 @@ def format(q) end q.text(" ") - q.if_break do - force_parens = - q.parents.any? do |node| - node.is_a?(Command) || node.is_a?(CommandCall) + q + .if_break do + force_parens = + q.parents.any? do |node| + node.is_a?(Command) || node.is_a?(CommandCall) + end + + q.text(force_parens ? "{" : "do") + q.indent do + q.breakable + q.format(statements) end - q.text(force_parens ? "{" : "do") - q.indent do q.breakable + q.text(force_parens ? "}" : "end") + end + .if_flat do + q.text("{ ") q.format(statements) + q.text(" }") end - - q.breakable - q.text(force_parens ? "}" : "end") - end.if_flat do - q.text("{ ") - q.format(statements) - q.text(" }") - end end end end @@ -5889,7 +6050,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location, comments: comments } end @@ -5922,7 +6083,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location, comments: comments } end @@ -5955,7 +6116,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location, comments: comments } end @@ -6005,7 +6166,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { target: target, value: value, location: location, comments: comments } end @@ -6052,7 +6213,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { call: call, block: block, location: location, comments: comments } end @@ -6060,8 +6221,13 @@ 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?(q.parent) && CallChainFormatter.chained?(call) - q.group { q.if_break { CallChainFormatter.new(self).format(q) }.if_flat { format_contents(q) } } + if !CallChainFormatter.chained?(q.parent) && + CallChainFormatter.chained?(call) + q.group do + q + .if_break { CallChainFormatter.new(self).format(q) } + .if_flat { format_contents(q) } + end else format_contents(q) end @@ -6108,7 +6274,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { parts: parts, location: location, comma: comma, comments: comments } end @@ -6152,7 +6318,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { contents: contents, location: location, comments: comments } end @@ -6208,7 +6374,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { constant: constant, bodystmt: bodystmt, @@ -6275,7 +6441,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { parts: parts, location: location, comments: comments } end @@ -6324,7 +6490,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { arguments: arguments, location: location, comments: comments } end @@ -6361,7 +6527,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location, comments: comments } end @@ -6407,7 +6573,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { target: target, operator: operator, @@ -6475,7 +6641,16 @@ def skip_indent? # This approach maintains the nice conciseness of the inline version, while # keeping the correct semantic meaning. module Parentheses - NODES = [Args, Assign, Assoc, Binary, Call, Defined, MAssign, OpAssign] + NODES = [ + Args, + Assign, + Assoc, + Binary, + Call, + Defined, + MAssign, + OpAssign + ].freeze def self.flat(q) return yield unless NODES.include?(q.parent.class) @@ -6507,6 +6682,8 @@ def self.break(q) # def method(param) end # class Params < Node + # Formats the optional position of the parameters. This includes the label, + # as well as the default value. class OptionalFormatter # [Ident] the name of the parameter attr_reader :name @@ -6530,6 +6707,8 @@ def format(q) end end + # Formats the keyword position of the parameters. This includes the label, + # as well as an optional default value. class KeywordFormatter # [Ident] the name of the parameter attr_reader :name @@ -6556,6 +6735,8 @@ def format(q) end end + # Formats the keyword_rest position of the parameters. This can be the **nil + # syntax, the ... syntax, or the ** syntax. class KeywordRestFormatter # [:nil | ArgsForward | KwRestParam] the value of the parameter attr_reader :value @@ -6569,11 +6750,7 @@ def comments end def format(q) - if value == :nil - q.text("**nil") - else - q.format(value) - end + value == :nil ? q.text("**nil") : q.format(value) end end @@ -6654,7 +6831,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { location: location, requireds: requireds, @@ -6675,18 +6852,17 @@ def format(q) ] parts << rest if rest && !rest.is_a?(ExcessedComma) - parts += - [ - *posts, - *keywords.map { |(name, value)| KeywordFormatter.new(name, value) } - ] + parts += [ + *posts, + *keywords.map { |(name, value)| KeywordFormatter.new(name, value) } + ] parts << KeywordRestFormatter.new(keyword_rest) if keyword_rest parts << block if block contents = -> do q.seplist(parts) { |part| q.format(part) } - q.format(rest) if rest && rest.is_a?(ExcessedComma) + q.format(rest) if rest.is_a?(ExcessedComma) end if ![Def, Defs, DefEndless].include?(q.parent.class) || parts.empty? @@ -6736,7 +6912,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { lparen: lparen, contents: contents, @@ -6787,7 +6963,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location, comments: comments } end @@ -6820,7 +6996,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { statements: statements, location: location, comments: comments } end @@ -6865,7 +7041,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { beginning: beginning, elements: elements, @@ -6920,18 +7096,9 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location } end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("qsymbols_beg") - - q.breakable - q.pp(value) - end - end end # QWords represents a string literal array without interpolation. @@ -6965,7 +7132,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { beginning: beginning, elements: elements, @@ -7020,7 +7187,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location } end end @@ -7052,7 +7219,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location, comments: comments } end @@ -7081,7 +7248,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location } end end @@ -7106,7 +7273,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location } end end @@ -7138,7 +7305,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location, comments: comments } end @@ -7177,7 +7344,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { beginning: beginning, parts: parts, location: location } end end @@ -7210,7 +7377,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location } end end @@ -7244,7 +7411,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location } end end @@ -7285,10 +7452,11 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { beginning: beginning, ending: ending, + options: options, parts: parts, location: location, comments: comments @@ -7296,7 +7464,7 @@ def deconstruct_keys(keys) end def format(q) - braces = ambiguous?(q) || include?(%r{\/}) + braces = ambiguous?(q) || include?(%r{/}) if braces && include?(/[{}]/) q.group do @@ -7323,18 +7491,22 @@ def format(q) end q.text("}") - q.text(ending[1..-1]) + q.text(options) end else q.group do q.text("/") q.format_each(parts) q.text("/") - q.text(ending[1..-1]) + q.text(options) end end end + def options + ending[1..] + end + private def include?(pattern) @@ -7390,7 +7562,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { exceptions: exceptions, variable: variable, @@ -7465,7 +7637,10 @@ def bind_end(end_char, end_column) if consequent consequent.bind_end(end_char, end_column) - statements.bind_end(consequent.location.start_char, consequent.location.start_column) + statements.bind_end( + consequent.location.start_char, + consequent.location.start_column + ) else statements.bind_end(end_char, end_column) end @@ -7481,7 +7656,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { keyword: keyword, exception: exception, @@ -7548,7 +7723,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { statement: statement, value: value, @@ -7602,7 +7777,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { name: name, location: location, comments: comments } end @@ -7639,7 +7814,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location, comments: comments } end @@ -7675,7 +7850,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { arguments: arguments, location: location, comments: comments } end @@ -7711,7 +7886,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location, comments: comments } end @@ -7740,7 +7915,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location } end end @@ -7779,7 +7954,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { target: target, bodystmt: bodystmt, @@ -7881,7 +8056,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { parser: parser, body: body, location: location, comments: comments } end @@ -7951,12 +8126,19 @@ def attach_comments(start_char, end_char) comment = parser_comments[comment_index] location = comment.location - if !comment.inline? && (start_char <= location.start_char) && (end_char >= location.end_char) && !comment.ignore? - while (node = body[body_index]) && (node.is_a?(VoidStmt) || node.location.start_char < location.start_char) + if !comment.inline? && (start_char <= location.start_char) && + (end_char >= location.end_char) && !comment.ignore? + while (node = body[body_index]) && + ( + node.is_a?(VoidStmt) || + node.location.start_char < location.start_char + ) body_index += 1 end - if body_index != 0 && body[body_index - 1].location.start_char < location.start_char && body[body_index - 1].location.end_char > location.start_char + if body_index != 0 && + body[body_index - 1].location.start_char < location.start_char && + body[body_index - 1].location.end_char > location.start_char # The previous node entirely encapsules the comment, so we don't # want to attach it here since it will get attached normally. This # is mostly in the case of hash and array literals. @@ -7996,7 +8178,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { parts: parts, location: location } end end @@ -8034,14 +8216,14 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { left: left, right: right, location: location, comments: comments } end def format(q) q.group do q.format(left) - q.text(' \\') + q.text(" \\") q.indent do q.breakable(force: true) q.format(right) @@ -8079,7 +8261,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { variable: variable, location: location, comments: comments } end @@ -8119,7 +8301,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { statements: statements, location: location, comments: comments } end @@ -8177,7 +8359,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { parts: parts, quote: quote, location: location, comments: comments } end @@ -8240,7 +8422,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { arguments: arguments, location: location, comments: comments } end @@ -8293,7 +8475,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location } end end @@ -8323,7 +8505,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location } end end @@ -8357,7 +8539,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location, comments: comments } end @@ -8398,7 +8580,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { beginning: beginning, elements: elements, @@ -8454,7 +8636,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location } end end @@ -8483,7 +8665,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location } end end @@ -8513,7 +8695,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location } end end @@ -8547,7 +8729,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { constant: constant, location: location, comments: comments } end @@ -8585,7 +8767,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { constant: constant, location: location, comments: comments } end @@ -8624,7 +8806,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location } end end @@ -8664,7 +8846,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location, comments: comments } end @@ -8702,7 +8884,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location } end end @@ -8738,7 +8920,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { statement: statement, parentheses: parentheses, @@ -8749,7 +8931,9 @@ def deconstruct_keys(keys) def format(q) parent = q.parents.take(2)[1] - ternary = (parent.is_a?(If) || parent.is_a?(Unless)) && Ternaryable.call(q, parent) + ternary = + (parent.is_a?(If) || parent.is_a?(Unless)) && + Ternaryable.call(q, parent) q.text("not") @@ -8803,7 +8987,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { operator: operator, statement: statement, @@ -8823,6 +9007,9 @@ def format(q) # undef method # class Undef < Node + # Undef accepts a variable number of arguments that can be either DynaSymbol + # or SymbolLiteral objects. For SymbolLiteral objects we descend directly + # into the value in order to have it come out as bare words. class UndefArgumentFormatter # [DynaSymbol | SymbolLiteral] the symbol to undefine attr_reader :node @@ -8866,7 +9053,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { symbols: symbols, location: location, comments: comments } end @@ -8925,7 +9112,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { predicate: predicate, statements: statements, @@ -8971,7 +9158,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { statement: statement, predicate: predicate, @@ -9010,13 +9197,15 @@ def format(q) 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) + 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 end @@ -9066,7 +9255,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { predicate: predicate, statements: statements, @@ -9122,7 +9311,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { statement: statement, predicate: predicate, @@ -9188,7 +9377,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { left: left, right: right, location: location, comments: comments } end @@ -9231,7 +9420,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location, comments: comments } end @@ -9275,7 +9464,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location, comments: comments } end @@ -9316,7 +9505,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location, comments: comments } end @@ -9356,7 +9545,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location, comments: comments } end @@ -9391,7 +9580,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { location: location, comments: comments } end @@ -9442,7 +9631,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { arguments: arguments, statements: statements, @@ -9523,7 +9712,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { predicate: predicate, statements: statements, @@ -9579,7 +9768,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { statement: statement, predicate: predicate, @@ -9648,7 +9837,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { parts: parts, location: location, comments: comments } end @@ -9688,7 +9877,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { beginning: beginning, elements: elements, @@ -9744,7 +9933,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location } end end @@ -9773,7 +9962,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { parts: parts, location: location } end end @@ -9806,7 +9995,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { parts: parts, location: location, comments: comments } end @@ -9844,7 +10033,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { arguments: arguments, location: location, comments: comments } end @@ -9894,7 +10083,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location, comments: comments } end @@ -9930,7 +10119,7 @@ def child_nodes alias deconstruct child_nodes - def deconstruct_keys(keys) + def deconstruct_keys(_keys) { value: value, location: location, comments: comments } end diff --git a/lib/syntax_tree/parser.rb b/lib/syntax_tree/parser.rb index f167aa83..75d3c322 100644 --- a/lib/syntax_tree/parser.rb +++ b/lib/syntax_tree/parser.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true module SyntaxTree + # Parser is a subclass of the Ripper library that subscribes to the stream of + # tokens and nodes coming from the parser and builds up a syntax tree. class Parser < Ripper # A special parser error so that we can get nice syntax displays on the # error message when prettier prints out the results. @@ -40,9 +42,11 @@ def initialize(start, line) @start = start @indices = [] - line.each_char.with_index(start) do |char, index| - char.bytesize.times { @indices << index } - end + line + .each_char + .with_index(start) do |char, index| + char.bytesize.times { @indices << index } + end end # Technically it's possible for the column index to be a negative value if @@ -130,10 +134,10 @@ def initialize(source, *) last_index = 0 @source.lines.each do |line| - if line.size == line.bytesize - @line_counts << SingleByteString.new(last_index) + @line_counts << if line.size == line.bytesize + SingleByteString.new(last_index) else - @line_counts << MultiByteString.new(last_index, line) + MultiByteString.new(last_index, line) end last_index += line.size @@ -239,7 +243,7 @@ def find_colon2_before(const) # By finding the next non-space character, we can make sure that the bounds # of the statement list are correct. def find_next_statement_start(position) - remaining = source[position..-1] + remaining = source[position..] if remaining.sub(/\A +/, "")[0] == "#" return position + remaining.index("\n") @@ -264,7 +268,7 @@ def on_BEGIN(statements) start_char, start_char - line_counts[lbrace.location.start_line - 1].start, rbrace.location.start_char, - rbrace.location.start_column, + rbrace.location.start_column ) keyword = find_token(Kw, "BEGIN") @@ -281,7 +285,13 @@ def on_BEGIN(statements) def on_CHAR(value) CHAR.new( value: value, - location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size) + location: + Location.token( + line: lineno, + char: char_pos, + column: current_column, + size: value.size + ) ) end @@ -313,8 +323,14 @@ def on_END(statements) def on___end__(value) @__end__ = EndContent.new( - value: source[(char_pos + value.length)..-1], - location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size) + value: source[(char_pos + value.length)..], + location: + Location.token( + line: lineno, + char: char_pos, + column: current_column, + size: value.size + ) ) end @@ -423,7 +439,8 @@ def on_args_add_block(arguments, block) # If there are any arguments and the operator we found from the list is # not after them, then we're going to return the arguments as-is because # we're looking at an & that occurs before the arguments are done. - if arguments.parts.any? && operator.location.start_char < arguments.location.end_char + if arguments.parts.any? && + operator.location.start_char < arguments.location.end_char return arguments end @@ -478,7 +495,11 @@ def on_args_forward # :call-seq: # on_args_new: () -> Args def on_args_new - Args.new(parts: [], location: Location.fixed(line: lineno, column: current_column, char: char_pos)) + Args.new( + parts: [], + location: + Location.fixed(line: lineno, column: current_column, char: char_pos) + ) end # :call-seq: @@ -529,7 +550,7 @@ def on_aryptn(constant, requireds, rest, posts) # If there's the optional then keyword, then we'll delete that and use it # as the end bounds of the location. - if token = find_token(Kw, "then", consume: false) + if (token = find_token(Kw, "then", consume: false)) tokens.delete(token) location = location.to(token.location) end @@ -538,10 +559,10 @@ def on_aryptn(constant, requireds, rest, posts) # here because it currently doesn't have anything to use for its precise # location. If we hit a comma, then we've gone too far. if rest.is_a?(VarField) && rest.value.nil? - tokens.rindex do |token| - case token + tokens.rindex do |rtoken| + case rtoken in Op[value: "*"] - rest = VarField.new(value: nil, location: token.location) + rest = VarField.new(value: nil, location: rtoken.location) break in Comma break @@ -561,7 +582,13 @@ def on_aryptn(constant, requireds, rest, posts) # :call-seq: # on_assign: ( - # (ARefField | ConstPathField | Field | TopConstField | VarField) target, + # ( + # ARefField | + # ConstPathField | + # Field | + # TopConstField | + # VarField + # ) target, # untyped value # ) -> Assign def on_assign(target, value) @@ -586,7 +613,10 @@ def on_assoc_new(key, value) def on_assoc_splat(value) operator = find_token(Op, "**") - AssocSplat.new(value: value, location: operator.location.to(value.location)) + AssocSplat.new( + value: value, + location: operator.location.to(value.location) + ) end # def on_assoclist_from_args(assocs) @@ -598,7 +628,13 @@ def on_assoc_splat(value) def on_backref(value) Backref.new( value: value, - location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size) + location: + Location.token( + line: lineno, + char: char_pos, + column: current_column, + size: value.size + ) ) end @@ -608,7 +644,13 @@ def on_backtick(value) node = Backtick.new( value: value, - location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size) + location: + Location.token( + line: lineno, + char: char_pos, + column: current_column, + size: value.size + ) ) tokens << node @@ -616,7 +658,9 @@ def on_backtick(value) end # :call-seq: - # on_bare_assoc_hash: (Array[AssocNew | AssocSplat] assocs) -> BareAssocHash + # on_bare_assoc_hash: ( + # Array[AssocNew | AssocSplat] assocs + # ) -> BareAssocHash def on_bare_assoc_hash(assocs) BareAssocHash.new( assocs: assocs, @@ -641,7 +685,7 @@ def on_begin(bodystmt) keyword = find_token(Kw, "begin") end_location = if bodystmt.rescue_clause || bodystmt.ensure_clause || - bodystmt.else_clause + bodystmt.else_clause bodystmt.location else find_token(Kw, "end").location @@ -660,7 +704,11 @@ def on_begin(bodystmt) end # :call-seq: - # on_binary: (untyped left, (Op | Symbol) operator, untyped right) -> Binary + # on_binary: ( + # untyped left, + # (Op | Symbol) operator, + # untyped right + # ) -> Binary def on_binary(left, operator, right) if operator.is_a?(Symbol) # Here, we're going to search backward for the token that's between the @@ -737,7 +785,8 @@ def on_bodystmt(statements, rescue_clause, else_clause, ensure_clause) else_keyword: else_clause && find_token(Kw, "else"), else_clause: else_clause, ensure_clause: ensure_clause, - location: Location.fixed(line: lineno, char: char_pos, column: current_column) + location: + Location.fixed(line: lineno, char: char_pos, column: current_column) ) end @@ -764,7 +813,10 @@ def on_brace_block(block_var, statements) start_line: lbrace.location.start_line, start_char: lbrace.location.start_char, start_column: lbrace.location.start_column, - end_line: [rbrace.location.end_line, statements.location.end_line].max, + end_line: [ + rbrace.location.end_line, + statements.location.end_line + ].max, end_char: rbrace.location.end_char, end_column: rbrace.location.end_column ) @@ -816,7 +868,7 @@ def on_call(receiver, operator, message) # :call-seq: # on_case: (untyped value, untyped consequent) -> Case | RAssign def on_case(value, consequent) - if keyword = find_token(Kw, "case", consume: false) + if (keyword = find_token(Kw, "case", consume: false)) tokens.delete(keyword) Case.new( @@ -870,7 +922,13 @@ def on_comma(value) node = Comma.new( value: value, - location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size) + location: + Location.token( + line: lineno, + char: char_pos, + column: current_column, + size: value.size + ) ) tokens << node @@ -915,7 +973,12 @@ def on_comment(value) value: value.chomp, inline: value.strip != lines[line - 1].strip, location: - Location.token(line: line, char: char_pos, column: current_column, size: value.size - 1) + Location.token( + line: line, + char: char_pos, + column: current_column, + size: value.size - 1 + ) ) @comments << comment @@ -927,7 +990,13 @@ def on_comment(value) def on_const(value) Const.new( value: value, - location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size) + location: + Location.token( + line: lineno, + char: char_pos, + column: current_column, + size: value.size + ) ) end @@ -962,7 +1031,13 @@ def on_const_ref(constant) def on_cvar(value) CVar.new( value: value, - location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size) + location: + Location.token( + line: lineno, + char: char_pos, + column: current_column, + size: value.size + ) ) end @@ -1047,7 +1122,10 @@ def on_defined(value) ending = find_token(RParen) end - Defined.new(value: value, location: beginning.location.to(ending.location)) + Defined.new( + value: value, + location: beginning.location.to(ending.location) + ) end # :call-seq: @@ -1268,7 +1346,8 @@ def on_embdoc_beg(value) @embdoc = EmbDoc.new( value: value, - location: Location.fixed(line: lineno, column: current_column, char: char_pos) + location: + Location.fixed(line: lineno, column: current_column, char: char_pos) ) end @@ -1302,7 +1381,13 @@ def on_embexpr_beg(value) node = EmbExprBeg.new( value: value, - location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size) + location: + Location.token( + line: lineno, + char: char_pos, + column: current_column, + size: value.size + ) ) tokens << node @@ -1315,7 +1400,13 @@ def on_embexpr_end(value) node = EmbExprEnd.new( value: value, - location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size) + location: + Location.token( + line: lineno, + char: char_pos, + column: current_column, + size: value.size + ) ) tokens << node @@ -1328,7 +1419,13 @@ def on_embvar(value) node = EmbVar.new( value: value, - location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size) + location: + Location.token( + line: lineno, + char: char_pos, + column: current_column, + size: value.size + ) ) tokens << node @@ -1395,7 +1492,13 @@ def on_field(parent, operator, name) def on_float(value) FloatLiteral.new( value: value, - location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size) + location: + Location.token( + line: lineno, + char: char_pos, + column: current_column, + size: value.size + ) ) end @@ -1413,8 +1516,7 @@ def on_fndptn(constant, left, values, right) # the location of the node. opening = find_token(LBracket, consume: false) || - find_token(LParen, consume: false) || - left + find_token(LParen, consume: false) || left # The closing is based on the opening, which is either the matched # punctuation or the right splat. @@ -1453,8 +1555,9 @@ def on_for(index, collection, statements) # Consume the do keyword if it exists so that it doesn't get confused for # some other block keyword = find_token(Kw, "do", consume: false) - if keyword && keyword.location.start_char > collection.location.end_char && - keyword.location.end_char < ending.location.start_char + if keyword && + keyword.location.start_char > collection.location.end_char && + keyword.location.end_char < ending.location.start_char tokens.delete(keyword) end @@ -1483,7 +1586,13 @@ def on_for(index, collection, statements) def on_gvar(value) GVar.new( value: value, - location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size) + location: + Location.token( + line: lineno, + char: char_pos, + column: current_column, + size: value.size + ) ) end @@ -1504,7 +1613,12 @@ def on_hash(assocs) # on_heredoc_beg: (String value) -> HeredocBeg def on_heredoc_beg(value) location = - Location.token(line: lineno, char: char_pos, column: current_column, size: value.size + 1) + Location.token( + line: lineno, + char: char_pos, + column: current_column, + size: value.size + 1 + ) # Here we're going to artificially create an extra node type so that if # there are comments after the declaration of a heredoc, they get printed. @@ -1545,7 +1659,7 @@ def on_heredoc_end(value) start_column: heredoc.location.start_column, end_line: lineno, end_char: char_pos, - end_column: current_column, + end_column: current_column ) ) end @@ -1563,24 +1677,32 @@ def on_hshptn(constant, keywords, keyword_rest) keyword_rest = VarField.new(value: nil, location: token.location) end + parts = [constant, *keywords&.flatten(1), keyword_rest].compact + + # If there's no constant, there may be braces, so we're going to look for + # those to get our bounds. + unless constant + lbrace = find_token(LBrace, consume: false) + rbrace = find_token(RBrace, consume: false) + + if lbrace && rbrace + parts = [lbrace, *parts, rbrace] + tokens.delete(lbrace) + tokens.delete(rbrace) + end + end + # Delete the optional then keyword - if token = find_token(Kw, "then", consume: false) + if (token = find_token(Kw, "then", consume: false)) + parts << token tokens.delete(token) end - parts = [constant, *keywords&.flatten(1), keyword_rest].compact - location = - if parts.any? - parts[0].location.to(parts[-1].location) - else - find_token(LBrace).location.to(find_token(RBrace).location) - end - HshPtn.new( constant: constant, keywords: keywords || [], keyword_rest: keyword_rest, - location: location + location: parts[0].location.to(parts[-1].location) ) end @@ -1589,7 +1711,13 @@ def on_hshptn(constant, keywords, keyword_rest) def on_ident(value) Ident.new( value: value, - location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size) + location: + Location.token( + line: lineno, + char: char_pos, + column: current_column, + size: value.size + ) ) end @@ -1654,7 +1782,13 @@ def on_if_mod(predicate, statement) def on_imaginary(value) Imaginary.new( value: value, - location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size) + location: + Location.token( + line: lineno, + char: char_pos, + column: current_column, + size: value.size + ) ) end @@ -1673,7 +1807,7 @@ def on_in(pattern, statements, consequent) ending = consequent || find_token(Kw, "end") statements_start = pattern - if token = find_token(Kw, "then", consume: false) + if (token = find_token(Kw, "then", consume: false)) tokens.delete(token) statements_start = token end @@ -1681,7 +1815,8 @@ def on_in(pattern, statements, consequent) start_char = find_next_statement_start(statements_start.location.end_char) statements.bind( start_char, - start_char - line_counts[statements_start.location.start_line - 1].start, + start_char - + line_counts[statements_start.location.start_line - 1].start, ending.location.start_char, ending.location.start_column ) @@ -1699,7 +1834,13 @@ def on_in(pattern, statements, consequent) def on_int(value) Int.new( value: value, - location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size) + location: + Location.token( + line: lineno, + char: char_pos, + column: current_column, + size: value.size + ) ) end @@ -1708,7 +1849,13 @@ def on_int(value) def on_ivar(value) IVar.new( value: value, - location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size) + location: + Location.token( + line: lineno, + char: char_pos, + column: current_column, + size: value.size + ) ) end @@ -1718,7 +1865,13 @@ def on_kw(value) node = Kw.new( value: value, - location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size) + location: + Location.token( + line: lineno, + char: char_pos, + column: current_column, + size: value.size + ) ) tokens << node @@ -1739,7 +1892,13 @@ def on_kwrest_param(name) def on_label(value) Label.new( value: value, - location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size) + location: + Location.token( + line: lineno, + char: char_pos, + column: current_column, + size: value.size + ) ) end @@ -1749,7 +1908,13 @@ def on_label_end(value) node = LabelEnd.new( value: value, - location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size) + location: + Location.token( + line: lineno, + char: char_pos, + column: current_column, + size: value.size + ) ) tokens << node @@ -1763,11 +1928,13 @@ def on_label_end(value) # ) -> Lambda def on_lambda(params, statements) beginning = find_token(TLambda) - - if tokens.any? { |token| + braces = + tokens.any? do |token| token.is_a?(TLamBeg) && token.location.start_char > beginning.location.start_char - } + end + + if braces opening = find_token(TLamBeg) closing = find_token(RBrace) else @@ -1795,7 +1962,13 @@ def on_lbrace(value) node = LBrace.new( value: value, - location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size) + location: + Location.token( + line: lineno, + char: char_pos, + column: current_column, + size: value.size + ) ) tokens << node @@ -1808,7 +1981,13 @@ def on_lbracket(value) node = LBracket.new( value: value, - location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size) + location: + Location.token( + line: lineno, + char: char_pos, + column: current_column, + size: value.size + ) ) tokens << node @@ -1821,7 +2000,13 @@ def on_lparen(value) node = LParen.new( value: value, - location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size) + location: + Location.token( + line: lineno, + char: char_pos, + column: current_column, + size: value.size + ) ) tokens << node @@ -1920,7 +2105,11 @@ def on_mlhs_add_star(mlhs, part) # :call-seq: # on_mlhs_new: () -> MLHS def on_mlhs_new - MLHS.new(parts: [], location: Location.fixed(line: lineno, char: char_pos, column: current_column)) + MLHS.new( + parts: [], + location: + Location.fixed(line: lineno, char: char_pos, column: current_column) + ) end # :call-seq: @@ -1965,18 +2154,18 @@ def on_module(constant, bodystmt) # :call-seq: # on_mrhs_new: () -> MRHS def on_mrhs_new - MRHS.new(parts: [], location: Location.fixed(line: lineno, char: char_pos, column: current_column)) + MRHS.new( + parts: [], + location: + Location.fixed(line: lineno, char: char_pos, column: current_column) + ) end # :call-seq: # on_mrhs_add: (MRHS mrhs, untyped part) -> MRHS def on_mrhs_add(mrhs, part) location = - if mrhs.parts.empty? - mrhs.location - else - mrhs.location.to(part.location) - end + (mrhs.parts.empty? ? mrhs.location : mrhs.location.to(part.location)) MRHS.new(parts: mrhs.parts << part, location: location) end @@ -2034,7 +2223,13 @@ def on_op(value) node = Op.new( value: value, - location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size) + location: + Location.token( + line: lineno, + char: char_pos, + column: current_column, + size: value.size + ) ) tokens << node @@ -2043,7 +2238,13 @@ def on_op(value) # :call-seq: # on_opassign: ( - # (ARefField | ConstPathField | Field | TopConstField | VarField) target, + # ( + # ARefField | + # ConstPathField | + # Field | + # TopConstField | + # VarField + # ) target, # Op operator, # untyped value # ) -> OpAssign @@ -2118,14 +2319,15 @@ def on_paren(contents) lparen = find_token(LParen) rparen = find_token(RParen) - if contents && contents.is_a?(Params) + if contents.is_a?(Params) location = contents.location start_char = find_next_statement_start(lparen.location.end_char) location = Location.new( start_line: location.start_line, start_char: start_char, - start_column: start_char - line_counts[lparen.location.start_line - 1].start, + start_column: + start_char - line_counts[lparen.location.start_line - 1].start, end_line: location.end_line, end_char: rparen.location.start_char, end_column: rparen.location.start_column @@ -2166,7 +2368,13 @@ def on_parse_error(error, *) def on_period(value) Period.new( value: value, - location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size) + location: + Location.token( + line: lineno, + char: char_pos, + column: current_column, + size: value.size + ) ) end @@ -2298,7 +2506,13 @@ def on_qsymbols_beg(value) node = QSymbolsBeg.new( value: value, - location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size) + location: + Location.token( + line: lineno, + char: char_pos, + column: current_column, + size: value.size + ) ) tokens << node @@ -2333,7 +2547,13 @@ def on_qwords_beg(value) node = QWordsBeg.new( value: value, - location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size) + location: + Location.token( + line: lineno, + char: char_pos, + column: current_column, + size: value.size + ) ) tokens << node @@ -2345,7 +2565,11 @@ def on_qwords_beg(value) def on_qwords_new beginning = find_token(QWordsBeg) - QWords.new(beginning: beginning, elements: [], location: beginning.location) + QWords.new( + beginning: beginning, + elements: [], + location: beginning.location + ) end # :call-seq: @@ -2353,7 +2577,13 @@ def on_qwords_new def on_rational(value) RationalLiteral.new( value: value, - location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size) + location: + Location.token( + line: lineno, + char: char_pos, + column: current_column, + size: value.size + ) ) end @@ -2363,7 +2593,13 @@ def on_rbrace(value) node = RBrace.new( value: value, - location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size) + location: + Location.token( + line: lineno, + char: char_pos, + column: current_column, + size: value.size + ) ) tokens << node @@ -2376,7 +2612,13 @@ def on_rbracket(value) node = RBracket.new( value: value, - location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size) + location: + Location.token( + line: lineno, + char: char_pos, + column: current_column, + size: value.size + ) ) tokens << node @@ -2410,7 +2652,13 @@ def on_regexp_beg(value) node = RegexpBeg.new( value: value, - location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size) + location: + Location.token( + line: lineno, + char: char_pos, + column: current_column, + size: value.size + ) ) tokens << node @@ -2422,7 +2670,13 @@ def on_regexp_beg(value) def on_regexp_end(value) RegexpEnd.new( value: value, - location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size) + location: + Location.token( + line: lineno, + char: char_pos, + column: current_column, + size: value.size + ) ) end @@ -2563,7 +2817,13 @@ def on_rparen(value) node = RParen.new( value: value, - location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size) + location: + Location.token( + line: lineno, + char: char_pos, + column: current_column, + size: value.size + ) ) tokens << node @@ -2611,7 +2871,11 @@ def on_stmts_add(statements, statement) statements.location.to(statement.location) end - Statements.new(self, body: statements.body << statement, location: location) + Statements.new( + self, + body: statements.body << statement, + location: location + ) end # :call-seq: @@ -2620,7 +2884,8 @@ def on_stmts_new Statements.new( self, body: [], - location: Location.fixed(line: lineno, char: char_pos, column: current_column) + location: + Location.fixed(line: lineno, char: char_pos, column: current_column) ) end @@ -2654,7 +2919,8 @@ def on_string_concat(left, right) def on_string_content StringContent.new( parts: [], - location: Location.fixed(line: lineno, char: char_pos, column: current_column) + location: + Location.fixed(line: lineno, char: char_pos, column: current_column) ) end @@ -2703,7 +2969,7 @@ def on_string_embexpr(statements) def on_string_literal(string) heredoc = @heredocs[-1] - if heredoc && heredoc.ending + if heredoc&.ending heredoc = @heredocs.pop Heredoc.new( @@ -2756,7 +3022,13 @@ def on_symbeg(value) node = SymBeg.new( value: value, - location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size) + location: + Location.token( + line: lineno, + char: char_pos, + column: current_column, + size: value.size + ) ) tokens << node @@ -2810,7 +3082,13 @@ def on_symbols_beg(value) node = SymbolsBeg.new( value: value, - location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size) + location: + Location.token( + line: lineno, + char: char_pos, + column: current_column, + size: value.size + ) ) tokens << node @@ -2835,7 +3113,13 @@ def on_tlambda(value) node = TLambda.new( value: value, - location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size) + location: + Location.token( + line: lineno, + char: char_pos, + column: current_column, + size: value.size + ) ) tokens << node @@ -2848,7 +3132,13 @@ def on_tlambeg(value) node = TLamBeg.new( value: value, - location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size) + location: + Location.token( + line: lineno, + char: char_pos, + column: current_column, + size: value.size + ) ) tokens << node @@ -2883,7 +3173,13 @@ def on_tstring_beg(value) node = TStringBeg.new( value: value, - location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size) + location: + Location.token( + line: lineno, + char: char_pos, + column: current_column, + size: value.size + ) ) tokens << node @@ -2895,7 +3191,13 @@ def on_tstring_beg(value) def on_tstring_content(value) TStringContent.new( value: value, - location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size) + location: + Location.token( + line: lineno, + char: char_pos, + column: current_column, + size: value.size + ) ) end @@ -2905,7 +3207,13 @@ def on_tstring_end(value) node = TStringEnd.new( value: value, - location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size) + location: + Location.token( + line: lineno, + char: char_pos, + column: current_column, + size: value.size + ) ) tokens << node @@ -3014,7 +3322,7 @@ def on_until(predicate, statements) # some other block keyword = find_token(Kw, "do", consume: false) if keyword && keyword.location.start_char > predicate.location.end_char && - keyword.location.end_char < ending.location.start_char + keyword.location.end_char < ending.location.start_char tokens.delete(keyword) end @@ -3081,7 +3389,10 @@ def on_var_ref(value) if pin && pin.location.start_char == value.location.start_char - 1 tokens.delete(pin) - PinnedVarRef.new(value: value, location: pin.location.to(value.location)) + PinnedVarRef.new( + value: value, + location: pin.location.to(value.location) + ) else VarRef.new(value: value, location: value.location) end @@ -3096,7 +3407,10 @@ def on_vcall(ident) # :call-seq: # on_void_stmt: () -> VoidStmt def on_void_stmt - VoidStmt.new(location: Location.fixed(line: lineno, char: char_pos, column: current_column)) + VoidStmt.new( + location: + Location.fixed(line: lineno, char: char_pos, column: current_column) + ) end # :call-seq: @@ -3110,7 +3424,7 @@ def on_when(arguments, statements, consequent) ending = consequent || find_token(Kw, "end") statements_start = arguments - if token = find_token(Kw, "then", consume: false) + if (token = find_token(Kw, "then", consume: false)) tokens.delete(token) statements_start = token end @@ -3119,7 +3433,8 @@ def on_when(arguments, statements, consequent) statements.bind( start_char, - start_char - line_counts[statements_start.location.start_line - 1].start, + start_char - + line_counts[statements_start.location.start_line - 1].start, ending.location.start_char, ending.location.start_column ) @@ -3142,7 +3457,7 @@ def on_while(predicate, statements) # some other block keyword = find_token(Kw, "do", consume: false) if keyword && keyword.location.start_char > predicate.location.end_char && - keyword.location.end_char < ending.location.start_char + keyword.location.end_char < ending.location.start_char tokens.delete(keyword) end @@ -3188,7 +3503,11 @@ def on_word_add(word, part) # :call-seq: # on_word_new: () -> Word def on_word_new - Word.new(parts: [], location: Location.fixed(line: lineno, char: char_pos, column: current_column)) + Word.new( + parts: [], + location: + Location.fixed(line: lineno, char: char_pos, column: current_column) + ) end # :call-seq: @@ -3207,7 +3526,13 @@ def on_words_beg(value) node = WordsBeg.new( value: value, - location: Location.token(line: lineno, char: char_pos, column: current_column, size: value.size) + location: + Location.token( + line: lineno, + char: char_pos, + column: current_column, + size: value.size + ) ) tokens << node @@ -3219,7 +3544,11 @@ def on_words_beg(value) def on_words_new beginning = find_token(WordsBeg) - Words.new(beginning: beginning, elements: [], location: beginning.location) + Words.new( + beginning: beginning, + elements: [], + location: beginning.location + ) end # def on_words_sep(value) diff --git a/lib/syntax_tree/prettyprint.rb b/lib/syntax_tree/prettyprint.rb index 0950ddfa..7fe64a56 100644 --- a/lib/syntax_tree/prettyprint.rb +++ b/lib/syntax_tree/prettyprint.rb @@ -375,7 +375,7 @@ class SingleLine # This argument is a noop. # * +newline+ - Argument position expected to be here for compatibility. # This argument is a noop. - def initialize(output, maxwidth = nil, newline = nil) + def initialize(output, _maxwidth = nil, _newline = nil) @output = Buffer.for(output) @target = @output @line_suffixes = Buffer::ArrayBuffer.new @@ -397,7 +397,7 @@ def flush # They are all noop arguments. def breakable( separator = " ", - width = separator.length, + _width = separator.length, indent: nil, force: nil ) @@ -410,7 +410,7 @@ def break_parent # Appends +separator+ to the output buffer. +width+ is a noop here for # compatibility. - def fill_breakable(separator = " ", width = separator.length) + def fill_breakable(separator = " ", _width = separator.length) target << separator end @@ -419,9 +419,9 @@ def trim target.trim! end - # ---------------------------------------------------------------------------- + # -------------------------------------------------------------------------- # Container node builders - # ---------------------------------------------------------------------------- + # -------------------------------------------------------------------------- # Opens a block for grouping objects to be pretty printed. # @@ -432,11 +432,11 @@ def trim # * +open_width+ - noop argument. Present for compatibility. # * +close_width+ - noop argument. Present for compatibility. def group( - indent = nil, + _indent = nil, open_object = "", close_object = "", - open_width = nil, - close_width = nil + _open_width = nil, + _close_width = nil ) target << open_object yield @@ -478,14 +478,14 @@ def line_suffix # Takes +indent+ arg, but does nothing with it. # # Yields to a block. - def nest(indent) + def nest(_indent) yield end # Add +object+ to the text to be output. # # +width+ argument is here for compatibility. It is a noop argument. - def text(object = "", width = nil) + def text(object = "", _width = nil) target << object end end @@ -546,17 +546,17 @@ def indent(part = IndentPart) last_spaces = 0 end - next_queue.each do |part| - case part + next_queue.each do |next_part| + case next_part when IndentPart flush_spaces.call add_spaces.call(2) when StringAlignPart flush_spaces.call - next_value += part.n - next_length += part.n.length + next_value += next_part.n + next_length += next_part.n.length when NumberAlignPart - last_spaces += part.n + last_spaces += next_part.n end end @@ -623,9 +623,9 @@ def self.format( # def self.singleline_format( output = "".dup, - maxwidth = nil, - newline = nil, - genspace = nil + _maxwidth = nil, + _newline = nil, + _genspace = nil ) q = SingleLine.new(output) yield q @@ -778,16 +778,19 @@ def flush position -= buffer.trim! when Group if mode == MODE_FLAT && !should_remeasure - commands << - [indent, doc.break? ? MODE_BREAK : MODE_FLAT, doc.contents] + commands << [ + indent, + doc.break? ? MODE_BREAK : MODE_FLAT, + doc.contents + ] else should_remeasure = false next_cmd = [indent, MODE_FLAT, doc.contents] - - if !doc.break? && fits?(next_cmd, commands, maxwidth - position) - commands << next_cmd + commands << if !doc.break? && + fits?(next_cmd, commands, maxwidth - position) + next_cmd else - commands << [indent, MODE_BREAK, doc.contents] + [indent, MODE_BREAK, doc.contents] end end when IfBreak @@ -1060,7 +1063,7 @@ def nest(indent) def text(object = "", width = object.length) doc = target.last - unless Text === doc + unless doc.is_a?(Text) doc = Text.new target << doc end diff --git a/lib/syntax_tree/version.rb b/lib/syntax_tree/version.rb index 7b857068..894ff1b7 100644 --- a/lib/syntax_tree/version.rb +++ b/lib/syntax_tree/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module SyntaxTree - VERSION = "2.3.1" + VERSION = "2.4.0" end diff --git a/lib/syntax_tree/visitor.rb b/lib/syntax_tree/visitor.rb index 63227d03..57794ddb 100644 --- a/lib/syntax_tree/visitor.rb +++ b/lib/syntax_tree/visitor.rb @@ -1,6 +1,9 @@ # frozen_string_literal: true module SyntaxTree + # Visitor is a parent class that provides the ability to walk down the tree + # and handle a subset of nodes. By defining your own subclass, you can + # explicitly handle a node type by defining a visit_* method. class Visitor # This is raised when you use the Visitor.visit_method method and it fails. # It is correctable to through DidYouMean. @@ -24,7 +27,9 @@ def initialize(error) def corrections @corrections ||= - DidYouMean::SpellChecker.new(dictionary: Visitor.visit_methods).correct(visit_method) + DidYouMean::SpellChecker.new( + dictionary: Visitor.visit_methods + ).correct(visit_method) end DidYouMean.correct_error(VisitMethodError, self) diff --git a/lib/syntax_tree/visitor/field_visitor.rb b/lib/syntax_tree/visitor/field_visitor.rb new file mode 100644 index 00000000..631084e8 --- /dev/null +++ b/lib/syntax_tree/visitor/field_visitor.rb @@ -0,0 +1,1115 @@ +# frozen_string_literal: true + +module SyntaxTree + class Visitor + # This is the parent class of a lot of built-in visitors for Syntax Tree. It + # reflects visiting each of the fields on every node in turn. It itself does + # not do anything with these fields, it leaves that behavior up to the + # subclass to implement. + # + # In order to properly use this class, you will need to subclass it and + # implement #comments, #field, #list, #node, #pairs, and #text. Those are + # documented here. + # + # == comments(node) + # + # This accepts the node that is being visited and does something depending + # on the comments attached to the node. + # + # == field(name, value) + # + # This accepts the name of the field being visited as a string (like + # "value") and the actual value of that field. The value can be a subclass + # of Node or any other type that can be held within the tree. + # + # == list(name, values) + # + # This accepts the name of the field being visited as well as a list of + # values. This is used, for example, when visiting something like the body + # of a Statements node. + # + # == node(name, node) + # + # This is the parent serialization method for each node. It is called with + # the node itself, as well as the type of the node as a string. The type + # is an internally used value that usually resembles the name of the + # ripper event that generated the node. The method should yield to the + # given block which then calls through to visit each of the fields on the + # node. + # + # == text(name, value) + # + # This accepts the name of the field being visited as well as a string + # value representing the value of the field. + # + # == pairs(name, values) + # + # This accepts the name of the field being visited as well as a list of + # pairs that represent the value of the field. It is used only in a couple + # of circumstances, like when visiting the list of optional parameters + # defined on a method. + # + class FieldVisitor < Visitor + attr_reader :q + + def visit_aref(node) + node(node, "aref") do + field("collection", node.collection) + field("index", node.index) + comments(node) + end + end + + def visit_aref_field(node) + node(node, "aref_field") do + field("collection", node.collection) + field("index", node.index) + comments(node) + end + end + + def visit_alias(node) + node(node, "alias") do + field("left", node.left) + field("right", node.right) + comments(node) + end + end + + def visit_arg_block(node) + node(node, "arg_block") do + field("value", node.value) if node.value + comments(node) + end + end + + def visit_arg_paren(node) + node(node, "arg_paren") do + field("arguments", node.arguments) + comments(node) + end + end + + def visit_arg_star(node) + node(node, "arg_star") do + field("value", node.value) + comments(node) + end + end + + def visit_args(node) + node(node, "args") do + list("parts", node.parts) + comments(node) + end + end + + def visit_args_forward(node) + visit_token(node, "args_forward") + end + + def visit_array(node) + node(node, "array") do + field("contents", node.contents) + comments(node) + end + end + + def visit_aryptn(node) + node(node, "aryptn") do + field("constant", node.constant) if node.constant + list("requireds", node.requireds) if node.requireds.any? + field("rest", node.rest) if node.rest + list("posts", node.posts) if node.posts.any? + comments(node) + end + end + + def visit_assign(node) + node(node, "assign") do + field("target", node.target) + field("value", node.value) + comments(node) + end + end + + def visit_assoc(node) + node(node, "assoc") do + field("key", node.key) + field("value", node.value) if node.value + comments(node) + end + end + + def visit_assoc_splat(node) + node(node, "assoc_splat") do + field("value", node.value) + comments(node) + end + end + + def visit_backref(node) + visit_token(node, "backref") + end + + def visit_backtick(node) + visit_token(node, "backtick") + end + + def visit_bare_assoc_hash(node) + node(node, "bare_assoc_hash") do + list("assocs", node.assocs) + comments(node) + end + end + + def visit_BEGIN(node) + node(node, "BEGIN") do + field("statements", node.statements) + comments(node) + end + end + + def visit_begin(node) + node(node, "begin") do + field("bodystmt", node.bodystmt) + comments(node) + end + end + + def visit_binary(node) + node(node, "binary") do + field("left", node.left) + text("operator", node.operator) + field("right", node.right) + comments(node) + end + end + + def visit_blockarg(node) + node(node, "blockarg") do + field("name", node.name) if node.name + comments(node) + end + end + + def visit_block_var(node) + node(node, "block_var") do + field("params", node.params) + list("locals", node.locals) if node.locals.any? + comments(node) + end + end + + def visit_bodystmt(node) + node(node, "bodystmt") do + field("statements", node.statements) + field("rescue_clause", node.rescue_clause) if node.rescue_clause + field("else_clause", node.else_clause) if node.else_clause + field("ensure_clause", node.ensure_clause) if node.ensure_clause + comments(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) + comments(node) + end + end + + def visit_call(node) + node(node, "call") do + field("receiver", node.receiver) + field("operator", node.operator) + field("message", node.message) + field("arguments", node.arguments) if node.arguments + comments(node) + end + end + + def visit_case(node) + node(node, "case") do + field("keyword", node.keyword) + field("value", node.value) if node.value + field("consequent", node.consequent) + comments(node) + end + end + + def visit_CHAR(node) + visit_token(node, "CHAR") + end + + def visit_class(node) + node(node, "class") do + field("constant", node.constant) + field("superclass", node.superclass) if node.superclass + field("bodystmt", node.bodystmt) + comments(node) + end + end + + def visit_comma(node) + node(node, "comma") { field("value", node.value) } + end + + def visit_command(node) + node(node, "command") do + field("message", node.message) + field("arguments", node.arguments) + comments(node) + end + end + + def visit_command_call(node) + node(node, "command_call") do + field("receiver", node.receiver) + field("operator", node.operator) + field("message", node.message) + field("arguments", node.arguments) if node.arguments + comments(node) + end + end + + def visit_comment(node) + node(node, "comment") { field("value", node.value) } + end + + def visit_const(node) + visit_token(node, "const") + end + + def visit_const_path_field(node) + node(node, "const_path_field") do + field("parent", node.parent) + field("constant", node.constant) + comments(node) + end + end + + def visit_const_path_ref(node) + node(node, "const_path_ref") do + field("parent", node.parent) + field("constant", node.constant) + comments(node) + end + end + + def visit_const_ref(node) + node(node, "const_ref") do + field("constant", node.constant) + comments(node) + end + end + + def visit_cvar(node) + visit_token(node, "cvar") + end + + def visit_def(node) + node(node, "def") do + field("name", node.name) + field("params", node.params) + field("bodystmt", node.bodystmt) + comments(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) + comments(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) + comments(node) + end + end + + def visit_END(node) + node(node, "END") do + field("statements", node.statements) + comments(node) + end + end + + def visit_else(node) + node(node, "else") do + field("statements", node.statements) + comments(node) + end + end + + def visit_elsif(node) + node(node, "elsif") do + field("predicate", node.predicate) + field("statements", node.statements) + field("consequent", node.consequent) if node.consequent + comments(node) + end + end + + def visit_embdoc(node) + node(node, "embdoc") { field("value", node.value) } + end + + def visit_embexpr_beg(node) + node(node, "embexpr_beg") { field("value", node.value) } + end + + def visit_embexpr_end(node) + node(node, "embexpr_end") { field("value", node.value) } + end + + def visit_embvar(node) + node(node, "embvar") { field("value", node.value) } + end + + def visit_ensure(node) + node(node, "ensure") do + field("statements", node.statements) + comments(node) + end + end + + 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) + field("operator", node.operator) + field("name", node.name) + comments(node) + end + end + + def visit_float(node) + visit_token(node, "float") + end + + def visit_fndptn(node) + node(node, "fndptn") do + field("constant", node.constant) if node.constant + field("left", node.left) + list("values", node.values) + field("right", node.right) + comments(node) + end + end + + def visit_for(node) + node(node, "for") do + field("index", node.index) + field("collection", node.collection) + field("statements", node.statements) + comments(node) + end + end + + def visit_gvar(node) + visit_token(node, "gvar") + end + + def visit_hash(node) + node(node, "hash") do + list("assocs", node.assocs) if node.assocs.any? + comments(node) + end + end + + def visit_heredoc(node) + node(node, "heredoc") do + list("parts", node.parts) + comments(node) + end + end + + def visit_heredoc_beg(node) + visit_token(node, "heredoc_beg") + end + + def visit_hshptn(node) + node(node, "hshptn") do + field("constant", node.constant) if node.constant + pairs("keywords", node.keywords) if node.keywords.any? + field("keyword_rest", node.keyword_rest) if node.keyword_rest + comments(node) + end + end + + def visit_ident(node) + visit_token(node, "ident") + end + + def visit_if(node) + node(node, "if") do + field("predicate", node.predicate) + field("statements", node.statements) + field("consequent", node.consequent) if node.consequent + comments(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) + field("truthy", node.truthy) + field("falsy", node.falsy) + comments(node) + end + end + + def visit_imaginary(node) + visit_token(node, "imaginary") + end + + def visit_in(node) + node(node, "in") do + field("pattern", node.pattern) + field("statements", node.statements) + field("consequent", node.consequent) if node.consequent + comments(node) + end + end + + def visit_int(node) + visit_token(node, "int") + end + + def visit_ivar(node) + visit_token(node, "ivar") + end + + def visit_kw(node) + visit_token(node, "kw") + end + + def visit_kwrest_param(node) + node(node, "kwrest_param") do + field("name", node.name) + comments(node) + end + end + + def visit_label(node) + visit_token(node, "label") + end + + def visit_label_end(node) + node(node, "label_end") { field("value", node.value) } + end + + def visit_lambda(node) + node(node, "lambda") do + field("params", node.params) + field("statements", node.statements) + comments(node) + end + end + + def visit_lbrace(node) + visit_token(node, "lbrace") + end + + def visit_lbracket(node) + visit_token(node, "lbracket") + end + + def visit_lparen(node) + visit_token(node, "lparen") + end + + def visit_massign(node) + node(node, "massign") do + field("target", node.target) + field("value", node.value) + comments(node) + end + end + + def visit_method_add_block(node) + node(node, "method_add_block") do + field("call", node.call) + field("block", node.block) + comments(node) + end + end + + def visit_mlhs(node) + node(node, "mlhs") do + list("parts", node.parts) + comments(node) + end + end + + def visit_mlhs_paren(node) + node(node, "mlhs_paren") do + field("contents", node.contents) + comments(node) + end + end + + def visit_module(node) + node(node, "module") do + field("constant", node.constant) + field("bodystmt", node.bodystmt) + comments(node) + end + end + + def visit_mrhs(node) + node(node, "mrhs") do + list("parts", node.parts) + comments(node) + end + end + + def visit_next(node) + node(node, "next") do + field("arguments", node.arguments) + comments(node) + end + end + + def visit_not(node) + node(node, "not") do + field("statement", node.statement) + comments(node) + end + end + + def visit_op(node) + visit_token(node, "op") + end + + def visit_opassign(node) + node(node, "opassign") do + field("target", node.target) + field("operator", node.operator) + field("value", node.value) + comments(node) + end + end + + def visit_params(node) + node(node, "params") do + list("requireds", node.requireds) if node.requireds.any? + pairs("optionals", node.optionals) if node.optionals.any? + field("rest", node.rest) if node.rest + list("posts", node.posts) if node.posts.any? + pairs("keywords", node.keywords) if node.keywords.any? + field("keyword_rest", node.keyword_rest) if node.keyword_rest + field("block", node.block) if node.block + comments(node) + end + end + + def visit_paren(node) + node(node, "paren") do + field("contents", node.contents) + comments(node) + end + end + + def visit_period(node) + visit_token(node, "period") + end + + def visit_pinned_begin(node) + node(node, "pinned_begin") do + field("statement", node.statement) + comments(node) + end + end + + def visit_pinned_var_ref(node) + node(node, "pinned_var_ref") do + field("value", node.value) + comments(node) + end + end + + def visit_program(node) + node(node, "program") do + field("statements", node.statements) + comments(node) + end + end + + def visit_qsymbols(node) + node(node, "qsymbols") do + list("elements", node.elements) + comments(node) + end + end + + def visit_qsymbols_beg(node) + node(node, "qsymbols_beg") { field("value", node.value) } + end + + def visit_qwords(node) + node(node, "qwords") do + list("elements", node.elements) + comments(node) + end + end + + def visit_qwords_beg(node) + node(node, "qwords_beg") { field("value", node.value) } + end + + def visit_rassign(node) + node(node, "rassign") do + field("value", node.value) + field("operator", node.operator) + field("pattern", node.pattern) + comments(node) + end + end + + def visit_rational(node) + visit_token(node, "rational") + end + + def visit_rbrace(node) + node(node, "rbrace") { field("value", node.value) } + end + + def visit_rbracket(node) + node(node, "rbracket") { field("value", node.value) } + end + + def visit_redo(node) + visit_token(node, "redo") + end + + def visit_regexp_beg(node) + node(node, "regexp_beg") { field("value", node.value) } + end + + def visit_regexp_content(node) + node(node, "regexp_content") { list("parts", node.parts) } + end + + def visit_regexp_end(node) + node(node, "regexp_end") { field("value", node.value) } + end + + def visit_regexp_literal(node) + node(node, "regexp_literal") do + list("parts", node.parts) + field("options", node.options) + comments(node) + end + end + + def visit_rescue(node) + node(node, "rescue") do + field("exception", node.exception) if node.exception + field("statements", node.statements) + field("consequent", node.consequent) if node.consequent + comments(node) + end + end + + def visit_rescue_ex(node) + node(node, "rescue_ex") do + field("exceptions", node.exceptions) + field("variable", node.variable) + comments(node) + end + end + + def visit_rescue_mod(node) + node(node, "rescue_mod") do + field("statement", node.statement) + field("value", node.value) + comments(node) + end + end + + def visit_rest_param(node) + node(node, "rest_param") do + field("name", node.name) + comments(node) + end + end + + def visit_retry(node) + visit_token(node, "retry") + end + + def visit_return(node) + node(node, "return") do + field("arguments", node.arguments) + comments(node) + end + end + + def visit_return0(node) + visit_token(node, "return0") + end + + def visit_rparen(node) + node(node, "rparen") { field("value", node.value) } + end + + def visit_sclass(node) + node(node, "sclass") do + field("target", node.target) + field("bodystmt", node.bodystmt) + comments(node) + end + end + + def visit_statements(node) + node(node, "statements") do + list("body", node.body) + comments(node) + end + end + + def visit_string_concat(node) + node(node, "string_concat") do + field("left", node.left) + field("right", node.right) + comments(node) + end + end + + def visit_string_content(node) + node(node, "string_content") { list("parts", node.parts) } + end + + def visit_string_dvar(node) + node(node, "string_dvar") do + field("variable", node.variable) + comments(node) + end + end + + def visit_string_embexpr(node) + node(node, "string_embexpr") do + field("statements", node.statements) + comments(node) + end + end + + def visit_string_literal(node) + node(node, "string_literal") do + list("parts", node.parts) + comments(node) + end + end + + def visit_super(node) + node(node, "super") do + field("arguments", node.arguments) + comments(node) + end + end + + def visit_symbeg(node) + node(node, "symbeg") { field("value", node.value) } + end + + def visit_symbol_content(node) + node(node, "symbol_content") { field("value", node.value) } + end + + def visit_symbol_literal(node) + node(node, "symbol_literal") do + field("value", node.value) + comments(node) + end + end + + def visit_symbols(node) + node(node, "symbols") do + list("elements", node.elements) + comments(node) + end + end + + def visit_symbols_beg(node) + node(node, "symbols_beg") { field("value", node.value) } + end + + def visit_tlambda(node) + node(node, "tlambda") { field("value", node.value) } + end + + def visit_tlambeg(node) + node(node, "tlambeg") { field("value", node.value) } + end + + def visit_top_const_field(node) + node(node, "top_const_field") do + field("constant", node.constant) + comments(node) + end + end + + def visit_top_const_ref(node) + node(node, "top_const_ref") do + field("constant", node.constant) + comments(node) + end + end + + def visit_tstring_beg(node) + node(node, "tstring_beg") { field("value", node.value) } + end + + def visit_tstring_content(node) + visit_token(node, "tstring_content") + end + + def visit_tstring_end(node) + node(node, "tstring_end") { field("value", node.value) } + end + + def visit_unary(node) + node(node, "unary") do + field("operator", node.operator) + field("statement", node.statement) + comments(node) + end + end + + def visit_undef(node) + node(node, "undef") do + list("symbols", node.symbols) + comments(node) + end + end + + def visit_unless(node) + node(node, "unless") do + field("predicate", node.predicate) + field("statements", node.statements) + field("consequent", node.consequent) if node.consequent + comments(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) + field("statements", node.statements) + comments(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) + comments(node) + end + end + + def visit_var_ref(node) + node(node, "var_ref") do + field("value", node.value) + comments(node) + end + end + + def visit_vcall(node) + node(node, "vcall") do + field("value", node.value) + comments(node) + end + end + + def visit_void_stmt(node) + node(node, "void_stmt") { comments(node) } + end + + def visit_when(node) + node(node, "when") do + field("arguments", node.arguments) + field("statements", node.statements) + field("consequent", node.consequent) if node.consequent + comments(node) + end + end + + def visit_while(node) + node(node, "while") do + field("predicate", node.predicate) + field("statements", node.statements) + comments(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) + comments(node) + end + end + + def visit_words(node) + node(node, "words") do + list("elements", node.elements) + comments(node) + end + end + + def visit_words_beg(node) + node(node, "words_beg") { field("value", node.value) } + end + + def visit_xstring(node) + node(node, "xstring") { list("parts", node.parts) } + end + + def visit_xstring_literal(node) + node(node, "xstring_literal") do + list("parts", node.parts) + comments(node) + end + end + + def visit_yield(node) + node(node, "yield") do + field("arguments", node.arguments) + comments(node) + end + end + + def visit_yield0(node) + visit_token(node, "yield0") + end + + def visit_zsuper(node) + visit_token(node, "zsuper") + end + + def visit___end__(node) + visit_token(node, "__end__") + end + + private + + def visit_token(node, type) + node(node, type) do + field("value", node.value) + comments(node) + end + end + end + end +end diff --git a/lib/syntax_tree/visitor/json_visitor.rb b/lib/syntax_tree/visitor/json_visitor.rb index 9f0c8f94..b516980c 100644 --- a/lib/syntax_tree/visitor/json_visitor.rb +++ b/lib/syntax_tree/visitor/json_visitor.rb @@ -1,1316 +1,45 @@ - # frozen_string_literal: true +# frozen_string_literal: true module SyntaxTree class Visitor - class JSONVisitor < Visitor - def visit_aref(node) - { - type: :aref, - collection: visit(node.collection), - index: visit(node.index), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_aref_field(node) - { - type: :aref_field, - collection: visit(node.collection), - index: visit(node.index), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_alias(node) - { - type: :alias, - left: visit(node.left), - right: visit(node.right), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_arg_block(node) - { - type: :arg_block, - value: visit(node.value), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_arg_paren(node) - { - type: :arg_paren, - args: visit(node.arguments), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_arg_star(node) - { - type: :arg_star, - value: visit(node.value), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_args(node) - { - type: :args, - parts: visit_all(node.parts), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_args_forward(node) - visit_token(:args_forward, node) - end - - def visit_array(node) - { - type: :array, - cnts: visit(node.contents), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_aryptn(node) - { - type: :aryptn, - constant: visit(node.constant), - reqs: visit_all(node.requireds), - rest: visit(node.rest), - posts: visit_all(node.posts), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_assign(node) - { - type: :assign, - target: visit(node.target), - value: visit(node.value), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_assoc(node) - { - type: :assoc, - key: visit(node.key), - value: visit(node.value), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_assoc_splat(node) - { - type: :assoc_splat, - value: visit(node.value), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_backref(node) - visit_token(:backref, node) - end - - def visit_backtick(node) - visit_token(:backtick, node) - end - - def visit_bare_assoc_hash(node) - { - type: :bare_assoc_hash, - assocs: visit_all(node.assocs), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_BEGIN(node) - { - type: :BEGIN, - lbrace: visit(node.lbrace), - stmts: visit(node.statements), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_begin(node) - { - type: :begin, - bodystmt: visit(node.bodystmt), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_binary(node) - { - type: :binary, - left: visit(node.left), - op: node.operator, - right: visit(node.right), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_blockarg(node) - { - type: :blockarg, - name: visit(node.name), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_block_var(node) - { - type: :block_var, - params: visit(node.params), - locals: visit_all(node.locals), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_bodystmt(node) - { - type: :bodystmt, - stmts: visit(node.statements), - rsc: visit(node.rescue_clause), - els: visit(node.else_clause), - ens: visit(node.ensure_clause), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_brace_block(node) - { - type: :brace_block, - lbrace: visit(node.lbrace), - block_var: visit(node.block_var), - stmts: visit(node.statements), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_break(node) - { - type: :break, - args: visit(node.arguments), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_call(node) - { - type: :call, - receiver: visit(node.receiver), - op: visit_call_operator(node.operator), - message: node.message == :call ? :call : visit(node.message), - args: visit(node.arguments), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_case(node) - { - type: :case, - value: visit(node.value), - cons: visit(node.consequent), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_CHAR(node) - visit_token(:CHAR, node) - end - - def visit_class(node) - { - type: :class, - constant: visit(node.constant), - superclass: visit(node.superclass), - bodystmt: visit(node.bodystmt), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_comma(node) - visit_token(:comma, node) - end - - def visit_command(node) - { - type: :command, - message: visit(node.message), - args: visit(node.arguments), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_command_call(node) - { - type: :command_call, - receiver: visit(node.receiver), - op: visit_call_operator(node.operator), - message: visit(node.message), - args: visit(node.arguments), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_comment(node) - { - type: :comment, - value: node.value, - inline: node.inline, - loc: visit_location(node.location) - } - end - - def visit_const(node) - visit_token(:const, node) - end - - def visit_const_path_field(node) - { - type: :const_path_field, - parent: visit(node.parent), - constant: visit(node.constant), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_const_path_ref(node) - { - type: :const_path_ref, - parent: visit(node.parent), - constant: visit(node.constant), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_const_ref(node) - { - type: :const_ref, - constant: visit(node.constant), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_cvar(node) - visit_token(:cvar, node) - end - - def visit_def(node) - { - type: :def, - name: visit(node.name), - params: visit(node.params), - bodystmt: visit(node.bodystmt), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_def_endless(node) - { - type: :def_endless, - name: visit(node.name), - paren: visit(node.paren), - stmt: visit(node.statement), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_defined(node) - visit_token(:defined, node) - end - - def visit_defs(node) - { - type: :defs, - target: visit(node.target), - op: visit(node.operator), - name: visit(node.name), - params: visit(node.params), - bodystmt: visit(node.bodystmt), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_do_block(node) - { - type: :do_block, - keyword: visit(node.keyword), - block_var: visit(node.block_var), - bodystmt: visit(node.bodystmt), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_dot2(node) - { - type: :dot2, - left: visit(node.left), - right: visit(node.right), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_dot3(node) - { - type: :dot3, - left: visit(node.left), - right: visit(node.right), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_dyna_symbol(node) - { - type: :dyna_symbol, - parts: visit_all(node.parts), - quote: node.quote, - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_END(node) - { - type: :END, - lbrace: visit(node.lbrace), - stmts: visit(node.statements), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_else(node) - { - type: :else, - stmts: visit(node.statements), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_elsif(node) - { - type: :elsif, - pred: visit(node.predicate), - stmts: visit(node.statements), - cons: visit(node.consequent), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_embdoc(node) - { - type: :embdoc, - value: node.value, - loc: visit_location(node.location) - } - end - - def visit_embexpr_beg(node) - { - type: :embexpr_beg, - value: node.value, - loc: visit_location(node.location) - } - end - - def visit_embexpr_end(node) - { - type: :embexpr_end, - value: node.value, - loc: visit_location(node.location) - } - end - - def visit_embvar(node) - { - type: :embvar, - value: node.value, - loc: visit_location(node.location) - } - end - - def visit_ensure(node) - { - type: :ensure, - keyword: visit(node.keyword), - stmts: visit(node.statements), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_excessed_comma(node) - visit_token(:excessed_comma, node) - end - - def visit_fcall(node) - { - type: :fcall, - value: visit(node.value), - args: visit(node.arguments), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_field(node) - { - type: :field, - parent: visit(node.parent), - op: visit_call_operator(node.operator), - name: visit(node.name), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_float(node) - visit_token(:float, node) - end - - def visit_fndptn(node) - { - type: :fndptn, - constant: visit(node.constant), - left: visit(node.left), - values: visit_all(node.values), - right: visit(node.right), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_for(node) - { - type: :for, - index: visit(node.index), - collection: visit(node.collection), - stmts: visit(node.statements), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end + # This visitor transforms the AST into a hash that contains only primitives + # that can be easily serialized into JSON. + class JSONVisitor < FieldVisitor + attr_reader :target - def visit_gvar(node) - visit_token(:gvar, node) + def initialize + @target = nil end - def visit_hash(node) - { - type: :hash, - assocs: visit_all(node.assocs), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_heredoc(node) - { - type: :heredoc, - beging: visit(node.beginning), - ending: node.ending, - parts: visit_all(node.parts), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_heredoc_beg(node) - visit_token(:heredoc_beg, node) - end - - def visit_hshptn(node) - { - type: :hshptn, - constant: visit(node.constant), - keywords: node.keywords.map { |(name, value)| [visit(name), visit(value)] }, - kwrest: visit(node.keyword_rest), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_ident(node) - visit_token(:ident, node) - end - - def visit_if(node) - { - type: :if, - pred: visit(node.predicate), - stmts: visit(node.statements), - cons: visit(node.consequent), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_if_mod(node) - { - type: :if_mod, - stmt: visit(node.statement), - pred: visit(node.predicate), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_if_op(node) - { - type: :ifop, - pred: visit(node.predicate), - tthy: visit(node.truthy), - flsy: visit(node.falsy), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_imaginary(node) - visit_token(:imaginary, node) - end - - def visit_in(node) - { - type: :in, - pattern: visit(node.pattern), - stmts: visit(node.statements), - cons: visit(node.consequent), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_int(node) - visit_token(:int, node) - end - - def visit_ivar(node) - visit_token(:ivar, node) - end - - def visit_kw(node) - visit_token(:kw, node) - end - - def visit_kwrest_param(node) - { - type: :kwrest_param, - name: visit(node.name), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_label(node) - visit_token(:label, node) - end - - def visit_label_end(node) - visit_token(:label_end, node) - end - - def visit_lambda(node) - { - type: :lambda, - params: visit(node.params), - stmts: visit(node.statements), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_lbrace(node) - visit_token(:lbrace, node) - end - - def visit_lbracket(node) - visit_token(:lbracket, node) - end - - def visit_lparen(node) - visit_token(:lparen, node) - end - - def visit_massign(node) - { - type: :massign, - target: visit(node.target), - value: visit(node.value), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_method_add_block(node) - { - type: :method_add_block, - call: visit(node.call), - block: visit(node.block), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_mlhs(node) - { - type: :mlhs, - parts: visit_all(node.parts), - comma: node.comma, - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_mlhs_paren(node) - { - type: :mlhs_paren, - cnts: visit(node.contents), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_module(node) - { - type: :module, - constant: visit(node.constant), - bodystmt: visit(node.bodystmt), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_mrhs(node) - { - type: :mrhs, - parts: visit_all(node.parts), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_next(node) - { - type: :next, - args: visit(node.arguments), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_not(node) - { - type: :not, - value: visit(node.statement), - paren: node.parentheses, - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_op(node) - visit_token(:op, node) - end - - def visit_opassign(node) - { - type: :opassign, - target: visit(node.target), - op: visit(node.operator), - value: visit(node.value), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_params(node) - { - type: :params, - reqs: visit_all(node.requireds), - opts: node.optionals.map { |(name, value)| [visit(name), visit(value)] }, - rest: visit(node.rest), - posts: visit_all(node.posts), - keywords: node.keywords.map { |(name, value)| [visit(name), visit(value || nil)] }, - kwrest: node.keyword_rest == :nil ? "nil" : visit(node.keyword_rest), - block: visit(node.block), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_paren(node) - { - type: :paren, - lparen: visit(node.lparen), - cnts: visit(node.contents), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_period(node) - visit_token(:period, node) - end - - def visit_pinned_begin(node) - { - type: :pinned_begin, - stmt: visit(node.statement), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_pinned_var_ref(node) - { - type: :pinned_var_ref, - value: visit(node.value), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_program(node) - { - type: :program, - stmts: visit(node.statements), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_qsymbols(node) - { - type: :qsymbols, - elems: visit_all(node.elements), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_qsymbols_beg(node) - visit_token(:qsymbols_beg, node) - end - - def visit_qwords(node) - { - type: :qwords, - elems: visit_all(node.elements), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_qwords_beg(node) - visit_token(:qwords_beg, node) - end - - def visit_rassign(node) - { - type: :rassign, - value: visit(node.value), - op: visit(node.operator), - pattern: visit(node.pattern), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_rational(node) - visit_token(:rational, node) - end - - def visit_rbrace(node) - visit_token(:rbrace, node) - end - - def visit_rbracket(node) - visit_token(:rbracket, node) - end - - def visit_redo(node) - visit_token(:redo, node) - end - - def visit_regexp_beg(node) - visit_token(:regexp_beg, node) - end - - def visit_regexp_content(node) - { - type: :regexp_content, - beging: node.beginning, - parts: visit_all(node.parts), - loc: visit_location(node.location) - } - end - - def visit_regexp_end(node) - visit_token(:regexp_end, node) - end - - def visit_regexp_literal(node) - { - type: :regexp_literal, - beging: node.beginning, - ending: node.ending, - parts: visit_all(node.parts), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_rescue(node) - { - type: :rescue, - extn: visit(node.exception), - stmts: visit(node.statements), - cons: visit(node.consequent), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_rescue_ex(node) - { - type: :rescue_ex, - extns: visit(node.exceptions), - var: visit(node.variable), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_rescue_mod(node) - { - type: :rescue_mod, - stmt: visit(node.statement), - value: visit(node.value), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_rest_param(node) - { - type: :rest_param, - name: visit(node.name), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_retry(node) - visit_token(:retry, node) - end - - def visit_return(node) - { - type: :return, - args: visit(node.arguments), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_return0(node) - visit_token(:return0, node) - end - - def visit_rparen(node) - visit_token(:rparen, node) - end - - def visit_sclass(node) - { - type: :sclass, - target: visit(node.target), - bodystmt: visit(node.bodystmt), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_statements(node) - { - type: :statements, - body: visit_all(node.body), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_string_concat(node) - { - type: :string_concat, - left: visit(node.left), - right: visit(node.right), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_string_content(node) - { - type: :string_content, - parts: visit_all(node.parts), - loc: visit_location(node.location) - } - end - - def visit_string_dvar(node) - { - type: :string_dvar, - var: visit(node.variable), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_string_embexpr(node) - { - type: :string_embexpr, - stmts: visit(node.statements), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_string_literal(node) - { - type: :string_literal, - parts: visit_all(node.parts), - quote: node.quote, - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_super(node) - { - type: :super, - args: visit(node.arguments), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_symbeg(node) - visit_token(:symbeg, node) - end - - def visit_symbol_content(node) - { - type: :symbol_content, - value: visit(node.value), - loc: visit_location(node.location) - } - end - - def visit_symbol_literal(node) - { - type: :symbol_literal, - value: visit(node.value), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_symbols(node) - { - type: :symbols, - elems: visit_all(node.elements), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_symbols_beg(node) - visit_token(:symbols_beg, node) - end - - def visit_tlambda(node) - visit_token(:tlambda, node) - end - - def visit_tlambeg(node) - visit_token(:tlambeg, node) - end - - def visit_top_const_field(node) - { - type: :top_const_field, - constant: visit(node.constant), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_top_const_ref(node) - { - type: :top_const_ref, - constant: visit(node.constant), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_tstring_beg(node) - visit_token(:tstring_beg, node) - end - - def visit_tstring_content(node) - visit_token(:tstring_content, node) - end - - def visit_tstring_end(node) - visit_token(:tstring_end, node) - end - - def visit_unary(node) - { - type: :unary, - op: node.operator, - value: visit(node.statement), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_undef(node) - { - type: :undef, - syms: visit_all(node.symbols), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_unless(node) - { - type: :unless, - pred: visit(node.predicate), - stmts: visit(node.statements), - cons: visit(node.consequent), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_unless_mod(node) - { - type: :unless_mod, - stmt: visit(node.statement), - pred: visit(node.predicate), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_until(node) - { - type: :until, - pred: visit(node.predicate), - stmts: visit(node.statements), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_until_mod(node) - { - type: :until_mod, - stmt: visit(node.statement), - pred: visit(node.predicate), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_var_alias(node) - { - type: :var_alias, - left: visit(node.left), - right: visit(node.right), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_var_field(node) - { - type: :var_field, - value: visit(node.value), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_var_ref(node) - { - type: :var_ref, - value: visit(node.value), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_vcall(node) - { - type: :vcall, - value: visit(node.value), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_void_stmt(node) - { - type: :void_stmt, - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_when(node) - { - type: :when, - args: visit(node.arguments), - stmts: visit(node.statements), - cons: visit(node.consequent), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_while(node) - { - type: :while, - pred: visit(node.predicate), - stmts: visit(node.statements), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_while_mod(node) - { - type: :while_mod, - stmt: visit(node.statement), - pred: visit(node.predicate), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_word(node) - { - type: :word, - parts: visit_all(node.parts), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_words(node) - { - type: :words, - elems: visit_all(node.elements), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end - - def visit_words_beg(node) - visit_token(:words_beg, node) - end + private - def visit_xstring(node) - { - type: :xstring, - parts: visit_all(node.parts), - loc: visit_location(node.location) - } + def comments(node) + target[:comments] = visit_all(node.comments) end - def visit_xstring_literal(node) - { - type: :xstring_literal, - parts: visit_all(node.parts), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } + def field(name, value) + target[name] = value.is_a?(Node) ? visit(value) : value end - def visit_yield(node) - { - type: :yield, - args: visit(node.arguments), - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } + def list(name, values) + target[name] = visit_all(values) end - def visit_yield0(node) - visit_token(:yield0, node) + def node(node, type) + previous = @target + @target = { type: type, location: visit_location(node.location) } + yield + @target + ensure + @target = previous end - def visit_zsuper(node) - visit_token(:zsuper, node) + def pairs(name, values) + target[name] = values.map { |(key, value)| [visit(key), visit(value)] } end - def visit___end__(node) - visit_token(:__end__, node) - end - - private - - def visit_call_operator(operator) - operator == :"::" ? :"::" : visit(operator) + def text(name, value) + target[name] = value end def visit_location(location) @@ -1321,15 +50,6 @@ def visit_location(location) location.end_char ] end - - def visit_token(type, node) - { - type: type, - value: node.value, - loc: visit_location(node.location), - cmts: visit_all(node.comments) - } - end end end end diff --git a/lib/syntax_tree/visitor/match_visitor.rb b/lib/syntax_tree/visitor/match_visitor.rb new file mode 100644 index 00000000..205f2b90 --- /dev/null +++ b/lib/syntax_tree/visitor/match_visitor.rb @@ -0,0 +1,122 @@ +# frozen_string_literal: true + +module SyntaxTree + class Visitor + # This visitor transforms the AST into a Ruby pattern matching expression + # that would match correctly against the AST. + class MatchVisitor < FieldVisitor + attr_reader :q + + def initialize(q) + @q = q + end + + def visit(node) + case node + when Node + super + when String + # pp will split up a string on newlines and concat them together using + # a "+" operator. This breaks the pattern matching expression. So + # instead we're going to check here for strings and manually put the + # entire value into the output buffer. + q.text(node.inspect) + else + q.pp(node) + end + end + + private + + def comments(node) + return if node.comments.empty? + + q.nest(0) do + q.text("comments: [") + q.indent do + q.breakable("") + q.seplist(node.comments) { |comment| visit(comment) } + end + q.breakable("") + q.text("]") + end + end + + def field(name, value) + q.nest(0) do + q.text(name) + q.text(": ") + visit(value) + end + end + + def list(name, values) + q.group do + q.text(name) + q.text(": [") + q.indent do + q.breakable("") + q.seplist(values) { |value| visit(value) } + end + q.breakable("") + q.text("]") + end + end + + def node(node, _type) + items = [] + q.with_target(items) { yield } + + if items.empty? + q.text(node.class.name) + return + end + + q.group do + q.text(node.class.name) + q.text("[") + q.indent do + q.breakable("") + q.seplist(items) { |item| q.target << item } + end + q.breakable("") + q.text("]") + end + end + + def pairs(name, values) + q.group do + q.text(name) + q.text(": [") + q.indent do + q.breakable("") + q.seplist(values) do |(key, value)| + q.group do + q.text("[") + q.indent do + q.breakable("") + visit(key) + q.text(",") + q.breakable + visit(value || nil) + end + q.breakable("") + q.text("]") + end + end + end + q.breakable("") + q.text("]") + end + end + + def text(name, value) + q.nest(0) do + q.text(name) + q.text(": ") + q.pp(value) + end + end + end + end +end diff --git a/lib/syntax_tree/visitor/pretty_print_visitor.rb b/lib/syntax_tree/visitor/pretty_print_visitor.rb index 40420a15..674e3aac 100644 --- a/lib/syntax_tree/visitor/pretty_print_visitor.rb +++ b/lib/syntax_tree/visitor/pretty_print_visitor.rb @@ -1,1167 +1,37 @@ - # frozen_string_literal: true +# frozen_string_literal: true module SyntaxTree class Visitor - class PrettyPrintVisitor < Visitor + # This visitor pretty-prints the AST into an equivalent s-expression. + class PrettyPrintVisitor < FieldVisitor attr_reader :q def initialize(q) @q = q end - def visit_aref(node) - node("aref") do - field("collection", node.collection) - field("index", node.index) - comments(node) - end - end - - def visit_aref_field(node) - node("aref_field") do - field("collection", node.collection) - field("index", node.index) - comments(node) - end - end - - def visit_alias(node) - node("alias") do - field("left", node.left) - field("right", node.right) - comments(node) - end - end - - def visit_arg_block(node) - node("arg_block") do - field("value", node.value) if node.value - comments(node) - end - end - - def visit_arg_paren(node) - node("arg_paren") do - field("arguments", node.arguments) - comments(node) - end - end - - def visit_arg_star(node) - node("arg_star") do - field("value", node.value) - comments(node) - end - end - - def visit_args(node) - node("args") do - list("parts", node.parts) - comments(node) - end - end - - def visit_args_forward(node) - visit_token("args_forward", node) - end - - def visit_array(node) - node("array") do - field("contents", node.contents) - comments(node) - end - end - - def visit_aryptn(node) - node("aryptn") do - field("constant", node.constant) if node.constant - list("requireds", node.requireds) if node.requireds.any? - field("rest", node.rest) if node.rest - list("posts", node.posts) if node.posts.any? - comments(node) - end - end - - def visit_assign(node) - node("assign") do - field("target", node.target) - field("value", node.value) - comments(node) - end - end - - def visit_assoc(node) - node("assoc") do - field("key", node.key) - field("value", node.value) if node.value - comments(node) - end - end - - def visit_assoc_splat(node) - node("assoc_splat") do - field("value", node.value) - comments(node) - end - end - - def visit_backref(node) - visit_token("backref", node) - end - - def visit_backtick(node) - visit_token("backtick", node) - end - - def visit_bare_assoc_hash(node) - node("bare_assoc_hash") do - list("assocs", node.assocs) - comments(node) - end - end - - def visit_BEGIN(node) - node("BEGIN") do - field("statements", node.statements) - comments(node) - end - end - - def visit_begin(node) - node("begin") do - field("bodystmt", node.bodystmt) - comments(node) - end - end - - def visit_binary(node) - node("binary") do - field("left", node.left) - text("operator", node.operator) - field("right", node.right) - comments(node) - end - end - - def visit_blockarg(node) - node("blockarg") do - field("name", node.name) if node.name - comments(node) - end - end - - def visit_block_var(node) - node("block_var") do - field("params", node.params) - list("locals", node.locals) if node.locals.any? - comments(node) - end - end - - def visit_bodystmt(node) - node("bodystmt") do - field("statements", node.statements) - field("rescue_clause", node.rescue_clause) if node.rescue_clause - field("else_clause", node.else_clause) if node.else_clause - field("ensure_clause", node.ensure_clause) if node.ensure_clause - comments(node) - end - end - - def visit_brace_block(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("break") do - field("arguments", node.arguments) - comments(node) - end - end - - def visit_call(node) - node("call") do - field("receiver", node.receiver) - field("operator", node.operator) - field("message", node.message) - field("arguments", node.arguments) if node.arguments - comments(node) - end - end - - def visit_case(node) - node("case") do - field("keyword", node.keyword) - field("value", node.value) if node.value - field("consequent", node.consequent) - comments(node) - end - end - - def visit_CHAR(node) - visit_token("CHAR", node) - end - - def visit_class(node) - node("class") do - field("constant", node.constant) - field("superclass", node.superclass) if node.superclass - field("bodystmt", node.bodystmt) - comments(node) - end - end - - def visit_comma(node) - node("comma") do - field("value", node) - end - end - - def visit_command(node) - node("command") do - field("message", node.message) - field("arguments", node.arguments) - comments(node) - end - end - - def visit_command_call(node) - node("command_call") do - field("receiver", node.receiver) - field("operator", node.operator) - field("message", node.message) - field("arguments", node.arguments) if node.arguments - comments(node) - end - end - - def visit_comment(node) - node("comment") do - field("value", node.value) - end - end - - def visit_const(node) - visit_token("const", node) - end - - def visit_const_path_field(node) - node("const_path_field") do - field("parent", node.parent) - field("constant", node.constant) - comments(node) - end - end - - def visit_const_path_ref(node) - node("const_path_ref") do - field("parent", node.parent) - field("constant", node.constant) - comments(node) - end - end - - def visit_const_ref(node) - node("const_ref") do - field("constant", node.constant) - comments(node) - end - end - - def visit_cvar(node) - visit_token("cvar", node) - end - - def visit_def(node) - node("def") do - field("name", node.name) - field("params", node.params) - field("bodystmt", node.bodystmt) - comments(node) - end - end - - def visit_def_endless(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("defined") do - field("value", node.value) - comments(node) - end - end - - def visit_defs(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("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("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("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("dyna_symbol") do - list("parts", node.parts) - comments(node) - end - end - - def visit_END(node) - node("END") do - field("statements", node.statements) - comments(node) - end - end - - def visit_else(node) - node("else") do - field("statements", node.statements) - comments(node) - end - end - - def visit_elsif(node) - node("elsif") do - field("predicate", node.predicate) - field("statements", node.statements) - field("consequent", node.consequent) if node.consequent - comments(node) - end - end - - def visit_embdoc(node) - node("embdoc") do - field("value", node.value) - end - end - - def visit_embexpr_beg(node) - node("embexpr_beg") do - field("value", node.value) - end - end - - def visit_embexpr_end(node) - node("embexpr_end") do - field("value", node.value) - end - end - - def visit_embvar(node) - node("embvar") do - field("value", node.value) - end - end - - def visit_ensure(node) - node("ensure") do - field("statements", node.statements) - comments(node) - end - end - - def visit_excessed_comma(node) - visit_token("excessed_comma", node) - end - - def visit_fcall(node) - node("fcall") do - field("value", node.value) - field("arguments", node.arguments) if node.arguments - comments(node) - end - end - - def visit_field(node) - node("field") do - field("parent", node.parent) - field("operator", node.operator) - field("name", node.name) - comments(node) - end - end - - def visit_float(node) - visit_token("float", node) - end - - def visit_fndptn(node) - node("fndptn") do - field("constant", node.constant) if node.constant - field("left", node.left) - list("values", node.values) - field("right", node.right) - comments(node) - end - end - - def visit_for(node) - node("for") do - field("index", node.index) - field("collection", node.collection) - field("statements", node.statements) - comments(node) - end - end - - def visit_gvar(node) - visit_token("gvar", node) - end - - def visit_hash(node) - node("hash") do - list("assocs", node.assocs) if node.assocs.any? - comments(node) - end - end - - def visit_heredoc(node) - node("heredoc") do - list("parts", node.parts) - comments(node) - end - end - - def visit_heredoc_beg(node) - visit_token("heredoc_beg", node) - end - - def visit_hshptn(node) - node("hshptn") do - field("constant", node.constant) if node.constant - - if node.keywords.any? - q.breakable - q.group(2, "(", ")") do - q.seplist(node.keywords) do |(key, value)| - q.group(2, "(", ")") do - key.pretty_print(q) - - if value - q.breakable - value.pretty_print(q) - end - end - end - end - end - - field("keyword_rest", node.keyword_rest) if node.keyword_rest - comments(node) - end - end - - def visit_ident(node) - visit_token("ident", node) - end - - def visit_if(node) - node("if") do - field("predicate", node.predicate) - field("statements", node.statements) - field("consequent", node.consequent) if node.consequent - comments(node) - end - end - - def visit_if_mod(node) - node("if_mod") do - field("statement", node.statement) - field("predicate", node.predicate) - comments(node) - end - end - - def visit_if_op(node) - node("ifop") do - field("predicate", node.predicate) - field("truthy", node.truthy) - field("falsy", node.falsy) - comments(node) - end - end - - def visit_imaginary(node) - visit_token("imaginary", node) - end - - def visit_in(node) - node("in") do - field("pattern", node.pattern) - field("statements", node.statements) - field("consequent", node.consequent) if node.consequent - comments(node) - end - end - - def visit_int(node) - visit_token("int", node) - end - - def visit_ivar(node) - visit_token("ivar", node) - end - - def visit_kw(node) - visit_token("kw", node) - end - - def visit_kwrest_param(node) - node("kwrest_param") do - field("name", node.name) - comments(node) - end - end - - def visit_label(node) - node("label") do - q.breakable - q.text(":") - q.text(node.value[0...-1]) - comments(node) - end - end - - def visit_label_end(node) - node("label_end") do - field("value", node.value) - end - end - - def visit_lambda(node) - node("lambda") do - field("params", node.params) - field("statements", node.statements) - comments(node) - end - end - - def visit_lbrace(node) - visit_token("lbrace", node) - end - - def visit_lbracket(node) - visit_token("lbracket", node) - end - - def visit_lparen(node) - visit_token("lparen", node) - end - - def visit_massign(node) - node("massign") do - field("target", node.target) - field("value", node.value) - comments(node) - end - end - - def visit_method_add_block(node) - node("method_add_block") do - field("call", node.call) - field("block", node.block) - comments(node) - end - end - - def visit_mlhs(node) - node("mlhs") do - list("parts", node.parts) - comments(node) - end - end - - def visit_mlhs_paren(node) - node("mlhs_paren") do - field("contents", node.contents) - comments(node) - end - end - - def visit_module(node) - node("module") do - field("constant", node.constant) - field("bodystmt", node.bodystmt) - comments(node) - end - end - - def visit_mrhs(node) - node("mrhs") do - list("parts", node.parts) - comments(node) - end - end - - def visit_next(node) - node("next") do - field("arguments", node.arguments) - comments(node) - end - end - - def visit_not(node) - node("not") do - field("statement", node.statement) - comments(node) - end - end - - def visit_op(node) - visit_token("op", node) - end - - def visit_opassign(node) - node("opassign") do - field("target", node.target) - field("operator", node.operator) - field("value", node.value) - comments(node) - end - end - - def visit_params(node) - node("params") do - list("requireds", node.requireds) if node.requireds.any? - - if node.optionals.any? - q.breakable - q.group(2, "(", ")") do - q.seplist(node.optionals) do |(name, default)| - name.pretty_print(q) - q.text("=") - q.group(2) do - q.breakable("") - default.pretty_print(q) - end - end - end - end - - field("rest", node.rest) if node.rest - list("posts", node.posts) if node.posts.any? - - if node.keywords.any? - q.breakable - q.group(2, "(", ")") do - q.seplist(node.keywords) do |(name, default)| - name.pretty_print(q) - - if default - q.text("=") - q.group(2) do - q.breakable("") - default.pretty_print(q) - end - end - end - end - end - - field("keyword_rest", node.keyword_rest) if node.keyword_rest - field("block", node.block) if node.block - comments(node) - end - end - - def visit_paren(node) - node("paren") do - field("contents", node.contents) - comments(node) - end - end - - def visit_period(node) - visit_token("period", node) - end - - def visit_pinned_begin(node) - node("pinned_begin") do - field("statement", node.statement) - comments(node) - end - end - - def visit_pinned_var_ref(node) - node("pinned_var_ref") do - field("value", node.value) - comments(node) - end - end - - def visit_program(node) - node("program") do - field("statements", node.statements) - comments(node) - end - end - - def visit_qsymbols(node) - node("qsymbols") do - list("elements", node.elements) - comments(node) - end - end - - def visit_qsymbols_beg(node) - node("qsymbols_beg") do - field("value", node.value) - end - end - - def visit_qwords(node) - node("qwords") do - list("elements", node.elements) - comments(node) - end - end - - def visit_qwords_beg(node) - node("qwords_beg") do - field("value", node.value) - end - end - - def visit_rassign(node) - node("rassign") do - field("value", node.value) - field("operator", node.operator) - field("pattern", node.pattern) - comments(node) - end - end - - def visit_rational(node) - visit_token("rational", node) - end - - def visit_rbrace(node) - node("rbrace") do - field("value", node.value) - end - end - - def visit_rbracket(node) - node("rbracket") do - field("value", node.value) - end - end - - def visit_redo(node) - visit_token("redo", node) - end - - def visit_regexp_beg(node) - node("regexp_beg") do - field("value", node.value) - end - end - - def visit_regexp_content(node) - node("regexp_content") do - list("parts", node.parts) - end - end - - def visit_regexp_end(node) - node("regexp_end") do - field("value", node.value) - end - end - - def visit_regexp_literal(node) - node("regexp_literal") do - list("parts", node.parts) - comments(node) - end - end - - def visit_rescue(node) - node("rescue") do - field("exception", node.exception) if node.exception - field("statements", node.statements) - field("consequent", node.consequent) if node.consequent - comments(node) - end - end - - def visit_rescue_ex(node) - node("rescue_ex") do - field("exceptions", node.exceptions) - field("variable", node.variable) - comments(node) - end - end - - def visit_rescue_mod(node) - node("rescue_mod") do - field("statement", node.statement) - field("value", node.value) - comments(node) - end - end - - def visit_rest_param(node) - node("rest_param") do - field("name", node.name) - comments(node) - end - end - - def visit_retry(node) - visit_token("retry", node) - end - - def visit_return(node) - node("return") do - field("arguments", node.arguments) - comments(node) - end - end - - def visit_return0(node) - visit_token("return0", node) - end - - def visit_rparen(node) - node("rparen") do - field("value", node.value) - end - end - - def visit_sclass(node) - node("sclass") do - field("target", node.target) - field("bodystmt", node.bodystmt) - comments(node) - end - end - - def visit_statements(node) - node("statements") do - list("body", node.body) - comments(node) - end - end - - def visit_string_concat(node) - node("string_concat") do - field("left", node.left) - field("right", node.right) - comments(node) - end - end - - def visit_string_content(node) - node("string_content") do - list("parts", node.parts) - end - end - - def visit_string_dvar(node) - node("string_dvar") do - field("variable", node.variable) - comments(node) - end - end - - def visit_string_embexpr(node) - node("string_embexpr") do - field("statements", node.statements) - comments(node) - end - end - - def visit_string_literal(node) - node("string_literal") do - list("parts", node.parts) - comments(node) - end - end - - def visit_super(node) - node("super") do - field("arguments", node.arguments) - comments(node) - end - end - - def visit_symbeg(node) - node("symbeg") do - field("value", node.value) - end - end - - def visit_symbol_content(node) - node("symbol_content") do - field("value", node.value) - end - end - - def visit_symbol_literal(node) - node("symbol_literal") do - field("value", node.value) - comments(node) - end - end - - def visit_symbols(node) - node("symbols") do - list("elements", node.elements) - comments(node) - end - end - - def visit_symbols_beg(node) - node("symbols_beg") do - field("value", node.value) - end - end - - def visit_tlambda(node) - node("tlambda") do - field("value", node.value) - end - end - - def visit_tlambeg(node) - node("tlambeg") do - field("value", node.value) - end - end - - def visit_top_const_field(node) - node("top_const_field") do - field("constant", node.constant) - comments(node) - end - end - - def visit_top_const_ref(node) - node("top_const_ref") do - field("constant", node.constant) - comments(node) - end - end - - def visit_tstring_beg(node) - node("tstring_beg") do - field("value", node.value) - end - end - - def visit_tstring_content(node) - visit_token("tstring_content", node) - end - - def visit_tstring_end(node) - node("tstring_end") do - field("value", node.value) - end - end - - def visit_unary(node) - node("unary") do - field("operator", node.operator) - field("statement", node.statement) - comments(node) - end - end - - def visit_undef(node) - node("undef") do - list("symbols", node.symbols) - comments(node) - end - end - - def visit_unless(node) - node("unless") do - field("predicate", node.predicate) - field("statements", node.statements) - field("consequent", node.consequent) if node.consequent - comments(node) - end - end - - def visit_unless_mod(node) - node("unless_mod") do - field("statement", node.statement) - field("predicate", node.predicate) - comments(node) - end - end - - def visit_until(node) - node("until") do - field("predicate", node.predicate) - field("statements", node.statements) - comments(node) - end - end - - def visit_until_mod(node) - node("until_mod") do - field("statement", node.statement) - field("predicate", node.predicate) - comments(node) - end - end - - def visit_var_alias(node) - node("var_alias") do + # This is here because we need to make sure the operator is cast to a + # string before we print it out. + def visit_binary(node) + node(node, "binary") do field("left", node.left) + text("operator", node.operator.to_s) field("right", node.right) comments(node) end end - def visit_var_field(node) - node("var_field") do - field("value", node.value) - comments(node) - end - end - - def visit_var_ref(node) - node("var_ref") do - field("value", node.value) - comments(node) - end - end - - def visit_vcall(node) - node("vcall") do - field("value", node.value) - comments(node) - end - end - - def visit_void_stmt(node) - node("void_stmt") do - comments(node) - end - end - - def visit_when(node) - node("when") do - field("arguments", node.arguments) - field("statements", node.statements) - field("consequent", node.consequent) if node.consequent - comments(node) - end - end - - def visit_while(node) - node("while") do - field("predicate", node.predicate) - field("statements", node.statements) - comments(node) - end - end - - def visit_while_mod(node) - node("while_mod") do - field("statement", node.statement) - field("predicate", node.predicate) - comments(node) - end - end - - def visit_word(node) - node("word") do - list("parts", node.parts) - comments(node) - end - end - - def visit_words(node) - node("words") do - list("elements", node.elements) - comments(node) - end - end - - def visit_words_beg(node) - node("words_beg") do - field("value", node.value) - end - end - - def visit_xstring(node) - node("xstring") do - list("parts", node.parts) - end - end - - def visit_xstring_literal(node) - node("xstring_literal") do - list("parts", node.parts) - comments(node) - end - end - - def visit_yield(node) - node("yield") do - field("arguments", node.arguments) + # This is here to make it a little nicer to look at labels since they + # typically have their : at the end of the value. + def visit_label(node) + node(node, "label") do + q.breakable + q.text(":") + q.text(node.value[0...-1]) comments(node) end end - def visit_yield0(node) - visit_token("yield0", node) - end - - def visit_zsuper(node) - visit_token("zsuper", node) - end - - def visit___end__(node) - visit_token("__end__", node) - end - private def comments(node) @@ -1169,45 +39,47 @@ def comments(node) q.breakable q.group(2, "(", ")") do - q.seplist(node.comments) { |comment| comment.pretty_print(q) } + q.seplist(node.comments) { |comment| q.pp(comment) } end end def field(_name, value) q.breakable - - # I don't entirely know why this is necessary, but in Ruby 2.7 there is - # an issue with calling q.pp on strings that somehow involves inspect - # keys. I'm purposefully avoiding the inspect key stuff here because I - # know the tree does not contain any cycles. - value.is_a?(String) ? q.text(value.inspect) : value.pretty_print(q) + q.pp(value) end def list(_name, values) q.breakable - q.group(2, "(", ")") do - q.seplist(values) { |value| value.pretty_print(q) } - end + q.group(2, "(", ")") { q.seplist(values) { |value| q.pp(value) } } end - def node(type) + def node(_node, type) q.group(2, "(", ")") do q.text(type) yield end end + def pairs(_name, values) + q.group(2, "(", ")") do + q.seplist(values) do |(key, value)| + q.pp(key) + + if value + q.text("=") + q.group(2) do + q.breakable("") + q.pp(value) + end + end + end + end + end + def text(_name, value) q.breakable q.text(value) end - - def visit_token(type, node) - node(type) do - field("value", node.value) - comments(node) - end - end end end end diff --git a/syntax_tree.gemspec b/syntax_tree.gemspec index d17296f6..06a7ed78 100644 --- a/syntax_tree.gemspec +++ b/syntax_tree.gemspec @@ -1,30 +1,32 @@ # frozen_string_literal: true -require_relative 'lib/syntax_tree/version' +require_relative "lib/syntax_tree/version" Gem::Specification.new do |spec| - spec.name = 'syntax_tree' - spec.version = SyntaxTree::VERSION - spec.authors = ['Kevin Newton'] - spec.email = ['kddnewton@gmail.com'] + spec.name = "syntax_tree" + spec.version = SyntaxTree::VERSION + spec.authors = ["Kevin Newton"] + spec.email = ["kddnewton@gmail.com"] - spec.summary = 'A parser based on ripper' - spec.homepage = 'https://github.com/kddnewton/syntax_tree' - spec.license = 'MIT' - spec.metadata = { 'rubygems_mfa_required' => 'true' } + spec.summary = "A parser based on ripper" + spec.homepage = "https://github.com/kddnewton/syntax_tree" + spec.license = "MIT" + spec.metadata = { "rubygems_mfa_required" => "true" } - spec.files = Dir.chdir(__dir__) do - `git ls-files -z`.split("\x0").reject do |f| - f.match(%r{^(test|spec|features)/}) + spec.files = + Dir.chdir(__dir__) do + `git ls-files -z`.split("\x0") + .reject { |f| f.match(%r{^(test|spec|features)/}) } end - end - spec.bindir = 'exe' - spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } + spec.required_ruby_version = ">= 2.7.3" + + spec.bindir = "exe" + spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = %w[lib] - spec.add_development_dependency 'bundler' - spec.add_development_dependency 'minitest' - spec.add_development_dependency 'rake' - spec.add_development_dependency 'simplecov' + spec.add_development_dependency "bundler" + spec.add_development_dependency "minitest" + spec.add_development_dependency "rake" + spec.add_development_dependency "simplecov" end diff --git a/test/cli_test.rb b/test/cli_test.rb new file mode 100644 index 00000000..ade1485c --- /dev/null +++ b/test/cli_test.rb @@ -0,0 +1,167 @@ +# frozen_string_literal: true + +require_relative "test_helper" + +module SyntaxTree + class CLITest < Minitest::Test + class TestHandler + def parse(source) + source * 2 + end + + def read(filepath) + File.read(filepath) + end + end + + def test_handler + SyntaxTree.register_handler(".test", TestHandler.new) + + file = Tempfile.new(%w[test- .test]) + file.puts("test") + + result = run_cli("ast", file: file) + assert_equal("\"test\\n\" + \"test\\n\"\n", result.stdio) + ensure + SyntaxTree::HANDLERS.delete(".test") + end + + def test_ast + result = run_cli("ast") + assert_includes(result.stdio, "ident \"test\"") + end + + def test_ast_syntax_error + file = Tempfile.new(%w[test- .rb]) + file.puts("foo\n<>\nbar\n") + + result = run_cli("ast", file: file) + assert_includes(result.stderr, "syntax error") + end + + def test_check + result = run_cli("check") + assert_includes(result.stdio, "match") + end + + def test_check_unformatted + file = Tempfile.new(%w[test- .rb]) + file.write("foo") + + result = run_cli("check", file: file) + assert_includes(result.stderr, "expected") + end + + def test_debug + result = run_cli("debug") + assert_includes(result.stdio, "idempotently") + end + + def test_debug_non_idempotent_format + formats = 0 + formatting = ->(*) { (formats += 1).to_s } + + SyntaxTree.stub(:format, formatting) do + result = run_cli("debug") + assert_includes(result.stderr, "idempotently") + end + end + + def test_doc + result = run_cli("doc") + assert_includes(result.stdio, "test") + end + + def test_format + result = run_cli("format") + assert_equal("test\n", result.stdio) + end + + def test_json + result = run_cli("json") + assert_includes(result.stdio, "\"type\": \"program\"") + end + + def test_match + result = run_cli("match") + assert_includes(result.stdio, "SyntaxTree::Program") + end + + def test_version + result = run_cli("version") + assert_includes(result.stdio, SyntaxTree::VERSION.to_s) + end + + def test_write + file = Tempfile.new(%w[test- .test]) + filepath = file.path + + result = run_cli("write", file: file) + assert_includes(result.stdio, filepath) + end + + def test_write_syntax_tree + file = Tempfile.new(%w[test- .rb]) + file.write("<>") + + result = run_cli("write", file: file) + assert_includes(result.stderr, "syntax error") + end + + def test_help + stdio, = capture_io { SyntaxTree::CLI.run(["help"]) } + assert_includes(stdio, "stree help") + end + + def test_help_default + *, stderr = capture_io { SyntaxTree::CLI.run(["foobar"]) } + assert_includes(stderr, "stree help") + end + + def test_no_arguments + $stdin.stub(:tty?, true) do + *, stderr = capture_io { SyntaxTree::CLI.run(["check"]) } + assert_includes(stderr, "stree help") + end + end + + def test_no_arguments_no_tty + stdin = $stdin + $stdin = StringIO.new("1+1") + + stdio, = capture_io { SyntaxTree::CLI.run(["format"]) } + assert_equal("1 + 1\n", stdio) + ensure + $stdin = stdin + end + + def test_generic_error + SyntaxTree.stub(:format, ->(*) { raise }) do + result = run_cli("format") + refute_equal(0, result.status) + end + end + + private + + Result = Struct.new(:status, :stdio, :stderr, keyword_init: true) + + def run_cli(command, file: nil) + if file.nil? + file = Tempfile.new(%w[test- .rb]) + file.puts("test") + end + + file.rewind + + status = nil + stdio, stderr = + capture_io { status = SyntaxTree::CLI.run([command, file.path]) } + + Result.new(status: status, stdio: stdio, stderr: stderr) + ensure + file.close + file.unlink + end + end +end diff --git a/test/encoded.rb b/test/encoded.rb new file mode 100644 index 00000000..a67aebf3 --- /dev/null +++ b/test/encoded.rb @@ -0,0 +1,2 @@ +# encoding: Shift_JIS +# frozen_string_literal: true diff --git a/test/fixtures/aref_field.rb b/test/fixtures/aref_field.rb index 93f338bd..4c4da4de 100644 --- a/test/fixtures/aref_field.rb +++ b/test/fixtures/aref_field.rb @@ -8,3 +8,5 @@ ] = baz % foo[bar] # comment +% +foo[bar] += baz diff --git a/test/fixtures/array_literal.rb b/test/fixtures/array_literal.rb index 8402b7db..df807728 100644 --- a/test/fixtures/array_literal.rb +++ b/test/fixtures/array_literal.rb @@ -19,12 +19,30 @@ - [foo, bar, baz] % +fooooooooooooooooo = 1 +[fooooooooooooooooo, fooooooooooooooooo, fooooooooooooooooo, fooooooooooooooooo, fooooooooooooooooo, fooooooooooooooooo, fooooooooooooooooo, fooooooooooooooooo, fooooooooooooooooo, fooooooooooooooooo] +- +fooooooooooooooooo = 1 +[ + fooooooooooooooooo, fooooooooooooooooo, fooooooooooooooooo, + fooooooooooooooooo, fooooooooooooooooo, fooooooooooooooooo, + fooooooooooooooooo, fooooooooooooooooo, fooooooooooooooooo, fooooooooooooooooo +] +% +[ + # comment +] +% ["foo"] % ["foo", "bar"] - %w[foo bar] % +["f", ?b] +- +%w[f b] +% [ "foo", "bar" # comment diff --git a/test/fixtures/aryptn.rb b/test/fixtures/aryptn.rb index 19f1ab13..c5562305 100644 --- a/test/fixtures/aryptn.rb +++ b/test/fixtures/aryptn.rb @@ -1,5 +1,9 @@ % case foo +in [] +end +% +case foo in _, _ end % diff --git a/test/fixtures/assign.rb b/test/fixtures/assign.rb index eb0ceefd..b402b721 100644 --- a/test/fixtures/assign.rb +++ b/test/fixtures/assign.rb @@ -10,6 +10,10 @@ bar HERE % +foo = %s[ + bar +] +% foo = barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr - foo = diff --git a/test/fixtures/assoc.rb b/test/fixtures/assoc.rb index 43bb2b08..cd3e5ed1 100644 --- a/test/fixtures/assoc.rb +++ b/test/fixtures/assoc.rb @@ -40,3 +40,9 @@ } % # >= 3.1.0 { foo: } +% +{ "foo": "bar" } +- +{ foo: "bar" } +% +{ "foo #{bar}": "baz" } diff --git a/test/fixtures/bare_assoc_hash.rb b/test/fixtures/bare_assoc_hash.rb index d9114eec..d25d0bf4 100644 --- a/test/fixtures/bare_assoc_hash.rb +++ b/test/fixtures/bare_assoc_hash.rb @@ -7,7 +7,7 @@ % foo(:"bar" => bar) - -foo("bar": bar) +foo(bar: bar) % foo(bar => bar, baz: baz) - diff --git a/test/fixtures/call.rb b/test/fixtures/call.rb index 874d290c..f3333276 100644 --- a/test/fixtures/call.rb +++ b/test/fixtures/call.rb @@ -12,3 +12,12 @@ foo::(1) - foo.(1) +% +foo.bar.baz.qux +% +fooooooooooooooooo.barrrrrrrrrrrrrrrrrrr {}.bazzzzzzzzzzzzzzzzzzzzzzzzzz.quxxxxxxxxx +- +fooooooooooooooooo + .barrrrrrrrrrrrrrrrrrr {} + .bazzzzzzzzzzzzzzzzzzzzzzzzzz + .quxxxxxxxxx diff --git a/test/fixtures/command.rb b/test/fixtures/command.rb index 7f061acd..84bd5b86 100644 --- a/test/fixtures/command.rb +++ b/test/fixtures/command.rb @@ -23,3 +23,8 @@ % meta3 meta2 meta1 def self.foo end +% +foo bar {} +% +foo bar do +end diff --git a/test/fixtures/command_call.rb b/test/fixtures/command_call.rb index 955c4bfc..5060ffa4 100644 --- a/test/fixtures/command_call.rb +++ b/test/fixtures/command_call.rb @@ -23,3 +23,8 @@ expect(foo).to_not receive( fooooooooooooooooooooooooooooooooooooooooooooooooooooooooo ) +% +foo.bar baz {} +% +foo.bar baz do +end diff --git a/test/fixtures/def_endless.rb b/test/fixtures/def_endless.rb index 5e14dbc7..dbac88bb 100644 --- a/test/fixtures/def_endless.rb +++ b/test/fixtures/def_endless.rb @@ -8,3 +8,13 @@ def foo() = bar def foo = bar % # >= 3.1.0 def foo = bar baz +% # >= 3.1.0 +def self.foo = bar +% # >= 3.1.0 +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/fixtures/dyna_symbol.rb b/test/fixtures/dyna_symbol.rb index 63a277b0..7ac74a31 100644 --- a/test/fixtures/dyna_symbol.rb +++ b/test/fixtures/dyna_symbol.rb @@ -15,7 +15,7 @@ % { %s[foo] => bar } - -{ "foo": bar } +{ foo: bar } % %s[ foo diff --git a/test/fixtures/hash.rb b/test/fixtures/hash.rb index 757f7bca..9c43a4fe 100644 --- a/test/fixtures/hash.rb +++ b/test/fixtures/hash.rb @@ -1,4 +1,6 @@ % +{} +% { bar: bar } % { :bar => bar } @@ -7,7 +9,7 @@ % { :"bar" => bar } - -{ "bar": bar } +{ bar: bar } % { bar => bar, baz: baz } - @@ -23,3 +25,7 @@ bar: barrrrrrrrrrrrrrrrrrrrrrrrrrrrrrr, baz: bazzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz } +% +{ + # comment +} diff --git a/test/fixtures/hshptn.rb b/test/fixtures/hshptn.rb index b2474ea9..2935f9c1 100644 --- a/test/fixtures/hshptn.rb +++ b/test/fixtures/hshptn.rb @@ -14,18 +14,28 @@ case foo in bar:, baz: end +- +case foo +in { bar:, baz: } +end % case foo in bar: bar, baz: baz end +- +case foo +in { bar: bar, baz: baz } +end % case foo in **bar end % case foo -in foo:, # comment1 - bar: # comment2 +in { + foo:, # comment1 + bar: # comment2 + } baz end % diff --git a/test/fixtures/if.rb b/test/fixtures/if.rb index ed5e5a30..cabea4c3 100644 --- a/test/fixtures/if.rb +++ b/test/fixtures/if.rb @@ -32,3 +32,6 @@ if foo a ? b : c end +% +if foo {} +end diff --git a/test/fixtures/regexp_literal.rb b/test/fixtures/regexp_literal.rb index 0569426d..76da96f4 100644 --- a/test/fixtures/regexp_literal.rb +++ b/test/fixtures/regexp_literal.rb @@ -53,3 +53,7 @@ /foo\/bar/ - %r{foo/bar} +% +/foo\/bar\/#{baz}/ +- +%r{foo/bar/#{baz}} diff --git a/test/fixtures/undef.rb b/test/fixtures/undef.rb index de42d5c3..73986b97 100644 --- a/test/fixtures/undef.rb +++ b/test/fixtures/undef.rb @@ -21,3 +21,5 @@ bar # comment - undef foo, bar # comment +% +undef :"foo", :"bar" diff --git a/test/fixtures/when.rb b/test/fixtures/when.rb index 1fb102da..c98d249e 100644 --- a/test/fixtures/when.rb +++ b/test/fixtures/when.rb @@ -54,3 +54,7 @@ case when foo... then end +% +case +when foo # comment +end diff --git a/test/formatting_test.rb b/test/formatting_test.rb index 5f51d471..eff7ef71 100644 --- a/test/formatting_test.rb +++ b/test/formatting_test.rb @@ -12,7 +12,20 @@ class FormattingTest < Minitest::Test def test_format_class_level source = "1+1" - assert_equal("1 + 1\n", SyntaxTree::Formatter.format(source, SyntaxTree.parse(source))) + + assert_equal( + "1 + 1\n", + Formatter.format(source, SyntaxTree.parse(source)) + ) + end + + def test_stree_ignore + source = <<~SOURCE + # stree-ignore + 1+1 + SOURCE + + assert_equal(source, SyntaxTree.format(source)) end end end diff --git a/test/idempotency_test.rb b/test/idempotency_test.rb index bb12bdbb..1f560db2 100644 --- a/test/idempotency_test.rb +++ b/test/idempotency_test.rb @@ -10,7 +10,11 @@ class IdempotencyTest < Minitest::Test source = SyntaxTree.read(filepath) formatted = SyntaxTree.format(source) - assert_equal(formatted, SyntaxTree.format(formatted), "expected #{filepath} to be formatted idempotently") + assert_equal( + formatted, + SyntaxTree.format(formatted), + "expected #{filepath} to be formatted idempotently" + ) end end end diff --git a/test/interface_test.rb b/test/interface_test.rb new file mode 100644 index 00000000..49a74e92 --- /dev/null +++ b/test/interface_test.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +require_relative "test_helper" + +module SyntaxTree + class InterfaceTest < Minitest::Test + ObjectSpace.each_object(Node.singleton_class) do |klass| + next if klass == Node + + define_method(:"test_instantiate_#{klass.name}") do + assert_syntax_tree(instantiate(klass)) + end + end + + Fixtures.each_fixture do |fixture| + define_method(:"test_#{fixture.name}") do + assert_syntax_tree(SyntaxTree.parse(fixture.source)) + end + end + + private + + # This method is supposed to instantiate a new instance of the given class. + # The class is always a descendant from SyntaxTree::Node, so we can make + # certain assumptions about the way the initialize method is set up. If it + # needs to be special-cased, it's done so at the end of this method. + def instantiate(klass) + params = {} + + # Set up all of the keyword parameters for the class. + klass + .instance_method(:initialize) + .parameters + .each { |(type, name)| params[name] = nil if type.start_with?("key") } + + # Set up any default values that have to be arrays. + %i[ + assocs + comments + elements + keywords + locals + optionals + parts + posts + requireds + symbols + values + ].each { |key| params[key] = [] if params.key?(key) } + + # Set up a default location for the node. + params[:location] = Location.fixed(line: 0, char: 0, column: 0) + + case klass.name + when "SyntaxTree::Binary" + klass.new(**params, operator: :+) + when "SyntaxTree::Label" + klass.new(**params, value: "label:") + when "SyntaxTree::RegexpLiteral" + klass.new(**params, ending: "/") + when "SyntaxTree::Statements" + klass.new(nil, **params, body: []) + else + klass.new(**params) + end + end + end +end diff --git a/test/json_visitor_test.rb b/test/json_visitor_test.rb deleted file mode 100644 index 917aca71..00000000 --- a/test/json_visitor_test.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -require_relative "test_helper" - -module SyntaxTree - class JSONVisitorTest < Minitest::Test - Fixtures.each_fixture do |fixture| - define_method(:"test_json_#{fixture.name}") do - refute_includes(SyntaxTree.format(fixture.source).to_json, "#<") - end - end - end -end diff --git a/test/node_test.rb b/test/node_test.rb index e412d648..6bde39bc 100644 --- a/test/node_test.rb +++ b/test/node_test.rb @@ -9,15 +9,15 @@ def self.guard_version(version) end def test_BEGIN - assert_node(BEGINBlock, "BEGIN", "BEGIN {}") + assert_node(BEGINBlock, "BEGIN {}") end def test_CHAR - assert_node(CHAR, "CHAR", "?a") + assert_node(CHAR, "?a") end def test_END - assert_node(ENDBlock, "END", "END {}") + assert_node(ENDBlock, "END {}") end def test___end__ @@ -28,29 +28,29 @@ def test___end__ SOURCE at = location(lines: 2..2, chars: 6..14) - assert_node(EndContent, "__end__", source, at: at) + assert_node(EndContent, source, at: at) end def test_alias - assert_node(Alias, "alias", "alias left right") + assert_node(Alias, "alias left right") end def test_aref - assert_node(ARef, "aref", "collection[index]") + assert_node(ARef, "collection[index]") end def test_aref_field source = "collection[index] = value" at = location(chars: 0..17) - assert_node(ARefField, "aref_field", source, at: at, &:target) + assert_node(ARefField, source, at: at, &:target) end def test_arg_paren source = "method(argument)" at = location(chars: 6..16) - assert_node(ArgParen, "arg_paren", source, at: at, &:arguments) + assert_node(ArgParen, source, at: at, &:arguments) end def test_arg_paren_heredoc @@ -61,23 +61,21 @@ def test_arg_paren_heredoc SOURCE at = location(lines: 1..3, chars: 6..28) - assert_node(ArgParen, "arg_paren", source, at: at, &:arguments) + assert_node(ArgParen, source, at: at, &:arguments) end def test_args source = "method(first, second, third)" at = location(chars: 7..27) - assert_node(Args, "args", source, at: at) do |node| - node.arguments.arguments - end + assert_node(Args, source, at: at) { |node| node.arguments.arguments } end def test_arg_block source = "method(argument, &block)" at = location(chars: 17..23) - assert_node(ArgBlock, "arg_block", source, at: at) do |node| + assert_node(ArgBlock, source, at: at) do |node| node.arguments.arguments.parts[1] end end @@ -91,7 +89,7 @@ def method(&) SOURCE at = location(lines: 2..2, chars: 29..30) - assert_node(ArgBlock, "arg_block", source, at: at) do |node| + assert_node(ArgBlock, source, at: at) do |node| node.bodystmt.statements.body.first.arguments.arguments.parts[0] end end @@ -101,7 +99,7 @@ def test_arg_star source = "method(prefix, *arguments, suffix)" at = location(chars: 15..25) - assert_node(ArgStar, "arg_star", source, at: at) do |node| + assert_node(ArgStar, source, at: at) do |node| node.arguments.arguments.parts[1] end end @@ -114,13 +112,13 @@ def get(...) SOURCE at = location(lines: 2..2, chars: 29..32) - assert_node(ArgsForward, "args_forward", source, at: at) do |node| + assert_node(ArgsForward, source, at: at) do |node| node.bodystmt.statements.body.first.arguments.arguments.parts.last end end def test_array - assert_node(ArrayLiteral, "array", "[1]") + assert_node(ArrayLiteral, "[1]") end def test_aryptn @@ -132,20 +130,18 @@ def test_aryptn SOURCE at = location(lines: 2..2, chars: 18..47) - assert_node(AryPtn, "aryptn", source, at: at) do |node| - node.consequent.pattern - end + assert_node(AryPtn, source, at: at) { |node| node.consequent.pattern } end def test_assign - assert_node(Assign, "assign", "variable = value") + assert_node(Assign, "variable = value") end def test_assoc source = "{ key1: value1, key2: value2 }" at = location(chars: 2..14) - assert_node(Assoc, "assoc", source, at: at) { |node| node.assocs.first } + assert_node(Assoc, source, at: at) { |node| node.assocs.first } end guard_version("3.1.0") do @@ -153,7 +149,7 @@ def test_assoc_no_value source = "{ key1:, key2: }" at = location(chars: 2..7) - assert_node(Assoc, "assoc", source, at: at) { |node| node.assocs.first } + assert_node(Assoc, source, at: at) { |node| node.assocs.first } end end @@ -161,29 +157,42 @@ def test_assoc_splat source = "{ **pairs }" at = location(chars: 2..9) - assert_node(AssocSplat, "assoc_splat", source, at: at) do |node| - node.assocs.first - end + assert_node(AssocSplat, source, at: at) { |node| node.assocs.first } end def test_backref - assert_node(Backref, "backref", "$1") + assert_node(Backref, "$1") end def test_backtick at = location(chars: 4..5) - assert_node(Backtick, "backtick", "def `() end", at: at, &:name) + assert_node(Backtick, "def `() end", at: at, &:name) end def test_bare_assoc_hash source = "method(key1: value1, key2: value2)" at = location(chars: 7..33) - assert_node(BareAssocHash, "bare_assoc_hash", source, at: at) do |node| + assert_node(BareAssocHash, source, at: at) do |node| node.arguments.arguments.parts.first end end + guard_version("3.1.0") do + def test_pinned_begin + source = <<~SOURCE + case value + in ^(expression) + end + SOURCE + + at = location(lines: 2..2, chars: 14..27, columns: 3..16) + assert_node(PinnedBegin, source, at: at) do |node| + node.consequent.pattern + end + end + end + def test_begin source = <<~SOURCE begin @@ -191,7 +200,7 @@ def test_begin end SOURCE - assert_node(Begin, "begin", source) + assert_node(Begin, source) end def test_begin_clauses @@ -207,11 +216,11 @@ def test_begin_clauses end SOURCE - assert_node(Begin, "begin", source) + assert_node(Begin, source) end def test_binary - assert_node(Binary, "binary", "collection << value") + assert_node(Binary, "collection << value") end def test_block_var @@ -221,16 +230,14 @@ def test_block_var SOURCE at = location(chars: 10..65) - assert_node(BlockVar, "block_var", source, at: at) do |node| - node.block.block_var - end + assert_node(BlockVar, source, at: at) { |node| node.block.block_var } end def test_blockarg source = "def method(&block); end" at = location(chars: 11..17) - assert_node(BlockArg, "blockarg", source, at: at) do |node| + assert_node(BlockArg, source, at: at) do |node| node.params.contents.block end end @@ -240,7 +247,7 @@ def test_blockarg_anonymous source = "def method(&); end" at = location(chars: 11..12) - assert_node(BlockArg, "blockarg", source, at: at) do |node| + assert_node(BlockArg, source, at: at) do |node| node.params.contents.block end end @@ -260,22 +267,22 @@ def test_bodystmt SOURCE at = location(lines: 9..9, chars: 5..64) - assert_node(BodyStmt, "bodystmt", source, at: at, &:bodystmt) + assert_node(BodyStmt, source, at: at, &:bodystmt) end def test_brace_block source = "method { |variable| variable + 1 }" at = location(chars: 7..34) - assert_node(BraceBlock, "brace_block", source, at: at, &:block) + assert_node(BraceBlock, source, at: at, &:block) end def test_break - assert_node(Break, "break", "break value") + assert_node(Break, "break value") end def test_call - assert_node(Call, "call", "receiver.message") + assert_node(Call, "receiver.message") end def test_case @@ -286,63 +293,77 @@ def test_case end SOURCE - assert_node(Case, "case", source) + assert_node(Case, source) end guard_version("3.0.0") do def test_rassign_in - assert_node(RAssign, "rassign", "value in pattern") + assert_node(RAssign, "value in pattern") end def test_rassign_rocket - assert_node(RAssign, "rassign", "value => pattern") + assert_node(RAssign, "value => pattern") end end def test_class - assert_node(ClassDeclaration, "class", "class Child < Parent; end") + assert_node(ClassDeclaration, "class Child < Parent; end") end def test_command - assert_node(Command, "command", "method argument") + assert_node(Command, "method argument") end def test_command_call - assert_node(CommandCall, "command_call", "object.method argument") + assert_node(CommandCall, "object.method argument") end def test_comment - assert_node(Comment, "comment", "# comment", at: location(chars: 0..8)) + assert_node(Comment, "# comment", at: location(chars: 0..8)) + end + + # This test is to ensure that comments get parsed and printed properly in + # all of the visitors. We do this by checking against a node that we're sure + # will have comments attached to it in order to exercise all of the various + # comments methods on the visitors. + def test_comment_attached + source = <<~SOURCE + def method # comment + end + SOURCE + + at = location(chars: 10..10) + assert_node(Params, source, at: at, &:params) end def test_const - assert_node(Const, "const", "Constant", &:value) + assert_node(Const, "Constant", &:value) end def test_const_path_field source = "object::Const = value" at = location(chars: 0..13) - assert_node(ConstPathField, "const_path_field", source, at: at, &:target) + assert_node(ConstPathField, source, at: at, &:target) end def test_const_path_ref - assert_node(ConstPathRef, "const_path_ref", "object::Const") + assert_node(ConstPathRef, "object::Const") end def test_const_ref source = "class Container; end" at = location(chars: 6..15) - assert_node(ConstRef, "const_ref", source, at: at, &:constant) + assert_node(ConstRef, source, at: at, &:constant) end def test_cvar - assert_node(CVar, "cvar", "@@variable", &:value) + assert_node(CVar, "@@variable", &:value) end def test_def - assert_node(Def, "def", "def method(param) result end") + assert_node(Def, "def method(param) result end") end def test_def_paramless @@ -351,27 +372,27 @@ def method end SOURCE - assert_node(Def, "def", source) + assert_node(Def, source) end guard_version("3.0.0") do def test_def_endless - assert_node(DefEndless, "def_endless", "def method = result") + assert_node(DefEndless, "def method = result") end end guard_version("3.1.0") do def test_def_endless_command - assert_node(DefEndless, "def_endless", "def method = result argument") + assert_node(DefEndless, "def method = result argument") end end def test_defined - assert_node(Defined, "defined", "defined?(variable)") + assert_node(Defined, "defined?(variable)") end def test_defs - assert_node(Defs, "defs", "def object.method(param) result end") + assert_node(Defs, "def object.method(param) result end") end def test_defs_paramless @@ -380,35 +401,33 @@ def object.method end SOURCE - assert_node(Defs, "defs", source) + assert_node(Defs, source) end def test_do_block source = "method do |variable| variable + 1 end" at = location(chars: 7..37) - assert_node(DoBlock, "do_block", source, at: at, &:block) + assert_node(DoBlock, source, at: at, &:block) end def test_dot2 - assert_node(Dot2, "dot2", "1..3") + assert_node(Dot2, "1..3") end def test_dot3 - assert_node(Dot3, "dot3", "1...3") + assert_node(Dot3, "1...3") end def test_dyna_symbol - assert_node(DynaSymbol, "dyna_symbol", ':"#{variable}"') + assert_node(DynaSymbol, ':"#{variable}"') end def test_dyna_symbol_hash_key source = '{ "#{key}": value }' at = location(chars: 2..11) - assert_node(DynaSymbol, "dyna_symbol", source, at: at) do |node| - node.assocs.first.key - end + assert_node(DynaSymbol, source, at: at) { |node| node.assocs.first.key } end def test_else @@ -419,7 +438,7 @@ def test_else SOURCE at = location(lines: 2..3, chars: 9..17) - assert_node(Else, "else", source, at: at, &:consequent) + assert_node(Else, source, at: at, &:consequent) end def test_elsif @@ -431,7 +450,7 @@ def test_elsif SOURCE at = location(lines: 2..4, chars: 9..30) - assert_node(Elsif, "elsif", source, at: at, &:consequent) + assert_node(Elsif, source, at: at, &:consequent) end def test_embdoc @@ -442,7 +461,7 @@ def test_embdoc =end SOURCE - assert_node(EmbDoc, "embdoc", source) + assert_node(EmbDoc, source) end def test_ensure @@ -453,33 +472,31 @@ def test_ensure SOURCE at = location(lines: 2..3, chars: 6..16) - assert_node(Ensure, "ensure", source, at: at) do |node| - node.bodystmt.ensure_clause - end + assert_node(Ensure, source, at: at) { |node| node.bodystmt.ensure_clause } end def test_excessed_comma source = "proc { |x,| }" at = location(chars: 9..10) - assert_node(ExcessedComma, "excessed_comma", source, at: at) do |node| + assert_node(ExcessedComma, source, at: at) do |node| node.block.block_var.params.rest end end def test_fcall - assert_node(FCall, "fcall", "method(argument)") + assert_node(FCall, "method(argument)") end def test_field source = "object.variable = value" at = location(chars: 0..15) - assert_node(Field, "field", source, at: at, &:target) + assert_node(Field, source, at: at, &:target) end def test_float_literal - assert_node(FloatLiteral, "float", "1.0") + assert_node(FloatLiteral, "1.0") end guard_version("3.0.0") do @@ -491,22 +508,20 @@ def test_fndptn SOURCE at = location(lines: 2..2, chars: 14..32) - assert_node(FndPtn, "fndptn", source, at: at) do |node| - node.consequent.pattern - end + assert_node(FndPtn, source, at: at) { |node| node.consequent.pattern } end end def test_for - assert_node(For, "for", "for value in list do end") + assert_node(For, "for value in list do end") end def test_gvar - assert_node(GVar, "gvar", "$variable", &:value) + assert_node(GVar, "$variable", &:value) end def test_hash - assert_node(HashLiteral, "hash", "{ key => value }") + assert_node(HashLiteral, "{ key => value }") end def test_heredoc @@ -517,7 +532,7 @@ def test_heredoc SOURCE at = location(lines: 1..3, chars: 0..22) - assert_node(Heredoc, "heredoc", source, at: at) + assert_node(Heredoc, source, at: at) end def test_heredoc_beg @@ -528,7 +543,7 @@ def test_heredoc_beg SOURCE at = location(chars: 0..11) - assert_node(HeredocBeg, "heredoc_beg", source, at: at, &:beginning) + assert_node(HeredocBeg, source, at: at, &:beginning) end def test_hshptn @@ -539,29 +554,27 @@ def test_hshptn SOURCE at = location(lines: 2..2, chars: 14..36) - assert_node(HshPtn, "hshptn", source, at: at) do |node| - node.consequent.pattern - end + assert_node(HshPtn, source, at: at) { |node| node.consequent.pattern } end def test_ident - assert_node(Ident, "ident", "value", &:value) + assert_node(Ident, "value", &:value) end def test_if - assert_node(If, "if", "if value then else end") + assert_node(If, "if value then else end") end - def test_ifop - assert_node(IfOp, "ifop", "value ? true : false") + def test_if_op + assert_node(IfOp, "value ? true : false") end def test_if_mod - assert_node(IfMod, "if_mod", "expression if predicate") + assert_node(IfMod, "expression if predicate") end def test_imaginary - assert_node(Imaginary, "imaginary", "1i") + assert_node(Imaginary, "1i") end def test_in @@ -573,27 +586,27 @@ def test_in SOURCE at = location(lines: 2..4, chars: 11..33) - assert_node(In, "in", source, at: at, &:consequent) + assert_node(In, source, at: at, &:consequent) end def test_int - assert_node(Int, "int", "1") + assert_node(Int, "1") end def test_ivar - assert_node(IVar, "ivar", "@variable", &:value) + assert_node(IVar, "@variable", &:value) end def test_kw at = location(chars: 1..3) - assert_node(Kw, "kw", ":if", at: at, &:value) + assert_node(Kw, ":if", at: at, &:value) end def test_kwrest_param source = "def method(**kwargs) end" at = location(chars: 11..19) - assert_node(KwRestParam, "kwrest_param", source, at: at) do |node| + assert_node(KwRestParam, source, at: at) do |node| node.params.contents.keyword_rest end end @@ -602,64 +615,62 @@ def test_label source = "{ key: value }" at = location(chars: 2..6) - assert_node(Label, "label", source, at: at) do |node| - node.assocs.first.key - end + assert_node(Label, source, at: at) { |node| node.assocs.first.key } end def test_lambda source = "->(value) { value * 2 }" - assert_node(Lambda, "lambda", source) + assert_node(Lambda, source) end def test_lambda_do source = "->(value) do value * 2 end" - assert_node(Lambda, "lambda", source) + assert_node(Lambda, source) end def test_lbrace source = "method {}" at = location(chars: 7..8) - assert_node(LBrace, "lbrace", source, at: at) { |node| node.block.lbrace } + assert_node(LBrace, source, at: at) { |node| node.block.lbrace } end def test_lparen source = "(1 + 1)" at = location(chars: 0..1) - assert_node(LParen, "lparen", source, at: at, &:lparen) + assert_node(LParen, source, at: at, &:lparen) end def test_massign - assert_node(MAssign, "massign", "first, second, third = value") + assert_node(MAssign, "first, second, third = value") end def test_method_add_block - assert_node(MethodAddBlock, "method_add_block", "method {}") + assert_node(MethodAddBlock, "method {}") end def test_mlhs source = "left, right = value" at = location(chars: 0..11) - assert_node(MLHS, "mlhs", source, at: at, &:target) + assert_node(MLHS, source, at: at, &:target) end def test_mlhs_add_post source = "left, *middle, right = values" at = location(chars: 0..20) - assert_node(MLHS, "mlhs", source, at: at, &:target) + assert_node(MLHS, source, at: at, &:target) end def test_mlhs_paren source = "(left, right) = value" at = location(chars: 0..13) - assert_node(MLHSParen, "mlhs_paren", source, at: at, &:target) + assert_node(MLHSParen, source, at: at, &:target) end def test_module @@ -668,34 +679,34 @@ module Container end SOURCE - assert_node(ModuleDeclaration, "module", source) + assert_node(ModuleDeclaration, source) end def test_mrhs source = "values = first, second, third" at = location(chars: 9..29) - assert_node(MRHS, "mrhs", source, at: at, &:value) + assert_node(MRHS, source, at: at, &:value) end def test_mrhs_add_star source = "values = first, *rest" at = location(chars: 9..21) - assert_node(MRHS, "mrhs", source, at: at, &:value) + assert_node(MRHS, source, at: at, &:value) end def test_next - assert_node(Next, "next", "next(value)") + assert_node(Next, "next(value)") end def test_op at = location(chars: 4..5) - assert_node(Op, "op", "def +(value) end", at: at, &:name) + assert_node(Op, "def +(value) end", at: at, &:name) end def test_opassign - assert_node(OpAssign, "opassign", "variable += value") + assert_node(OpAssign, "variable += value") end def test_params @@ -711,27 +722,23 @@ def method( SOURCE at = location(lines: 2..7, chars: 11..93) - assert_node(Params, "params", source, at: at) do |node| - node.params.contents - end + assert_node(Params, source, at: at) { |node| node.params.contents } end def test_params_posts source = "def method(*rest, post) end" at = location(chars: 11..22) - assert_node(Params, "params", source, at: at) do |node| - node.params.contents - end + assert_node(Params, source, at: at) { |node| node.params.contents } end def test_paren - assert_node(Paren, "paren", "(1 + 2)") + assert_node(Paren, "(1 + 2)") end def test_period at = location(chars: 6..7) - assert_node(Period, "period", "object.method", at: at, &:operator) + assert_node(Period, "object.method", at: at, &:operator) end def test_program @@ -739,6 +746,11 @@ def test_program program = parser.parse refute(parser.error?) + case program + in statements: { body: [statement] } + assert_kind_of(VCall, statement) + end + json = JSON.parse(program.to_json) io = StringIO.new PP.singleline_pp(program, io) @@ -750,23 +762,23 @@ def test_program end def test_qsymbols - assert_node(QSymbols, "qsymbols", "%i[one two three]") + assert_node(QSymbols, "%i[one two three]") end def test_qwords - assert_node(QWords, "qwords", "%w[one two three]") + assert_node(QWords, "%w[one two three]") end def test_rational - assert_node(RationalLiteral, "rational", "1r") + assert_node(RationalLiteral, "1r") end def test_redo - assert_node(Redo, "redo", "redo") + assert_node(Redo, "redo") end def test_regexp_literal - assert_node(RegexpLiteral, "regexp_literal", "/abc/") + assert_node(RegexpLiteral, "/abc/") end def test_rescue_ex @@ -777,7 +789,7 @@ def test_rescue_ex SOURCE at = location(lines: 2..2, chars: 13..35) - assert_node(RescueEx, "rescue_ex", source, at: at) do |node| + assert_node(RescueEx, source, at: at) do |node| node.bodystmt.rescue_clause.exception end end @@ -792,43 +804,41 @@ def test_rescue SOURCE at = location(lines: 2..5, chars: 6..58) - assert_node(Rescue, "rescue", source, at: at) do |node| - node.bodystmt.rescue_clause - end + assert_node(Rescue, source, at: at) { |node| node.bodystmt.rescue_clause } end def test_rescue_mod - assert_node(RescueMod, "rescue_mod", "expression rescue value") + assert_node(RescueMod, "expression rescue value") end def test_rest_param source = "def method(*rest) end" at = location(chars: 11..16) - assert_node(RestParam, "rest_param", source, at: at) do |node| + assert_node(RestParam, source, at: at) do |node| node.params.contents.rest end end def test_retry - assert_node(Retry, "retry", "retry") + assert_node(Retry, "retry") end def test_return - assert_node(Return, "return", "return value") + assert_node(Return, "return value") end def test_return0 - assert_node(Return0, "return0", "return") + assert_node(Return0, "return") end def test_sclass - assert_node(SClass, "sclass", "class << self; end") + assert_node(SClass, "class << self; end") end def test_statements at = location(chars: 1..6) - assert_node(Statements, "statements", "(value)", at: at, &:contents) + assert_node(Statements, "(value)", at: at, &:contents) end def test_string_concat @@ -837,12 +847,12 @@ def test_string_concat 'right' SOURCE - assert_node(StringConcat, "string_concat", source) + assert_node(StringConcat, source) end def test_string_dvar at = location(chars: 1..11) - assert_node(StringDVar, "string_dvar", '"#@variable"', at: at) do |node| + assert_node(StringDVar, '"#@variable"', at: at) do |node| node.parts.first end end @@ -851,94 +861,99 @@ def test_string_embexpr source = '"#{variable}"' at = location(chars: 1..12) - assert_node(StringEmbExpr, "string_embexpr", source, at: at) do |node| - node.parts.first - end + assert_node(StringEmbExpr, source, at: at) { |node| node.parts.first } end def test_string_literal - assert_node(StringLiteral, "string_literal", "\"string\"") + assert_node(StringLiteral, "\"string\"") end def test_super - assert_node(Super, "super", "super value") + assert_node(Super, "super value") end def test_symbol_literal - assert_node(SymbolLiteral, "symbol_literal", ":symbol") + assert_node(SymbolLiteral, ":symbol") end def test_symbols - assert_node(Symbols, "symbols", "%I[one two three]") + assert_node(Symbols, "%I[one two three]") end def test_top_const_field source = "::Constant = value" at = location(chars: 0..10) - assert_node(TopConstField, "top_const_field", source, at: at, &:target) + assert_node(TopConstField, source, at: at, &:target) end def test_top_const_ref - assert_node(TopConstRef, "top_const_ref", "::Constant") + assert_node(TopConstRef, "::Constant") end def test_tstring_content source = "\"string\"" at = location(chars: 1..7) - assert_node(TStringContent, "tstring_content", source, at: at) do |node| - node.parts.first - end + assert_node(TStringContent, source, at: at) { |node| node.parts.first } end def test_not - assert_node(Not, "not", "not(value)") + assert_node(Not, "not(value)") end def test_unary - assert_node(Unary, "unary", "+value") + assert_node(Unary, "+value") end def test_undef - assert_node(Undef, "undef", "undef value") + assert_node(Undef, "undef value") end def test_unless - assert_node(Unless, "unless", "unless value then else end") + assert_node(Unless, "unless value then else end") end def test_unless_mod - assert_node(UnlessMod, "unless_mod", "expression unless predicate") + assert_node(UnlessMod, "expression unless predicate") end def test_until - assert_node(Until, "until", "until value do end") + assert_node(Until, "until value do end") end def test_until_mod - assert_node(UntilMod, "until_mod", "expression until predicate") + assert_node(UntilMod, "expression until predicate") end def test_var_alias - assert_node(VarAlias, "var_alias", "alias $new $old") + assert_node(VarAlias, "alias $new $old") end def test_var_field at = location(chars: 0..8) - assert_node(VarField, "var_field", "variable = value", at: at, &:target) + assert_node(VarField, "variable = value", at: at, &:target) + end + + guard_version("3.1.0") do + def test_pinned_var_ref + source = "foo in ^bar" + at = location(chars: 7..11) + + assert_node(PinnedVarRef, source, at: at, &:pattern) + end end def test_var_ref - assert_node(VarRef, "var_ref", "true") + assert_node(VarRef, "true") end def test_vcall - assert_node(VCall, "vcall", "variable") + assert_node(VCall, "variable") end def test_void_stmt - assert_node(VoidStmt, "void_stmt", ";;", at: location(chars: 0..0)) + assert_node(VoidStmt, ";;", at: location(chars: 0..0)) end def test_when @@ -950,30 +965,28 @@ def test_when SOURCE at = location(lines: 2..4, chars: 11..52) - assert_node(When, "when", source, at: at, &:consequent) + assert_node(When, source, at: at, &:consequent) end def test_while - assert_node(While, "while", "while value do end") + assert_node(While, "while value do end") end def test_while_mod - assert_node(WhileMod, "while_mod", "expression while predicate") + assert_node(WhileMod, "expression while predicate") end def test_word at = location(chars: 3..7) - assert_node(Word, "word", "%W[word]", at: at) do |node| - node.elements.first - end + assert_node(Word, "%W[word]", at: at) { |node| node.elements.first } end def test_words - assert_node(Words, "words", "%W[one two three]") + assert_node(Words, "%W[one two three]") end def test_xstring_literal - assert_node(XStringLiteral, "xstring_literal", "`ls`") + assert_node(XStringLiteral, "`ls`") end def test_xstring_heredoc @@ -984,19 +997,19 @@ def test_xstring_heredoc SOURCE at = location(lines: 1..3, chars: 0..18) - assert_node(Heredoc, "heredoc", source, at: at) + assert_node(Heredoc, source, at: at) end def test_yield - assert_node(Yield, "yield", "yield value") + assert_node(Yield, "yield value") end def test_yield0 - assert_node(Yield0, "yield0", "yield") + assert_node(Yield0, "yield") end def test_zsuper - assert_node(ZSuper, "zsuper", "super") + assert_node(ZSuper, "super") end def test_column_positions @@ -1006,7 +1019,7 @@ def test_column_positions SOURCE at = location(lines: 2..2, chars: 13..27, columns: 0..14) - assert_node(Command, "command", source, at: at) + assert_node(Command, source, at: at) end def test_multibyte_column_positions @@ -1016,7 +1029,7 @@ def test_multibyte_column_positions SOURCE at = location(lines: 2..2, chars: 16..26, columns: 0..10) - assert_node(Command, "command", source, at: at) + assert_node(Command, source, at: at) end private @@ -1032,7 +1045,7 @@ def location(lines: 1..1, chars: 0..0, columns: 0..0) ) end - def assert_node(kind, type, source, at: nil) + def assert_node(kind, source, at: nil) at ||= location( lines: 1..[1, source.count("\n")].max, @@ -1057,16 +1070,8 @@ def assert_node(kind, type, source, at: nil) assert_kind_of(kind, node) assert_equal(at, node.location) - # Serialize the node to JSON, parse it back out, and assert that we have - # found the expected type. - json = JSON.parse(node.to_json) - assert_equal(type, json["type"]) - - # Pretty-print the node to a singleline and then assert that the top - # s-expression of the printed output matches the expected type. - io = StringIO.new - PP.singleline_pp(node, io) - assert_match(/^\(#{type}.*\)$/, io.string) + # Finally, test that this node responds to everything it should. + assert_syntax_tree(node) end end end diff --git a/test/parser_test.rb b/test/parser_test.rb index e5861398..8aadbfc2 100644 --- a/test/parser_test.rb +++ b/test/parser_test.rb @@ -9,7 +9,8 @@ def test_parses_ripper_methods events = Ripper::EVENTS # Next, subtract all of the events that we have explicitly defined. - events -= Parser.private_instance_methods(false).grep(/^on_(\w+)/) { $1.to_sym } + events -= + Parser.private_instance_methods(false).grep(/^on_(\w+)/) { $1.to_sym } # Next, subtract the list of events that we purposefully skipped. events -= %i[ diff --git a/test/pretty_print_visitor_test.rb b/test/pretty_print_visitor_test.rb deleted file mode 100644 index 8ca7cdf8..00000000 --- a/test/pretty_print_visitor_test.rb +++ /dev/null @@ -1,19 +0,0 @@ -# frozen_string_literal: true - -require_relative "test_helper" - -module SyntaxTree - class PrettyPrintVisitorTest < Minitest::Test - Fixtures.each_fixture do |fixture| - define_method(:"test_pretty_print_#{fixture.name}") do - formatter = PP.new([]) - - program = SyntaxTree.parse(fixture.source) - program.pretty_print(formatter) - - formatter.flush - refute_includes(formatter.output.join, "#<") - end - end - end -end diff --git a/test/behavior_test.rb b/test/syntax_tree_test.rb similarity index 72% rename from test/behavior_test.rb rename to test/syntax_tree_test.rb index 707cdd9b..3d5ae90e 100644 --- a/test/behavior_test.rb +++ b/test/syntax_tree_test.rb @@ -3,7 +3,7 @@ require_relative "test_helper" module SyntaxTree - class BehaviorTest < Minitest::Test + class SyntaxTreeTest < Minitest::Test def test_empty void_stmt = SyntaxTree.parse("").statements.body.first assert_kind_of(VoidStmt, void_stmt) @@ -14,10 +14,6 @@ def test_multibyte assert_equal(5, assign.location.end_char) end - def test_parse_error - assert_raises(Parser::ParseError) { SyntaxTree.parse("<>") } - end - def test_next_statement_start source = <<~SOURCE def method # comment @@ -29,6 +25,18 @@ def method # comment assert_equal(20, bodystmt.location.start_char) end + def test_parse_error + assert_raises(Parser::ParseError) { SyntaxTree.parse("<>") } + end + + def test_read + source = SyntaxTree.read(__FILE__) + assert_equal(Encoding.default_external, source.encoding) + + source = SyntaxTree.read(File.expand_path("encoded.rb", __dir__)) + assert_equal(Encoding::Shift_JIS, source.encoding) + end + def test_version refute_nil(VERSION) end diff --git a/test/test_helper.rb b/test/test_helper.rb index 09c4dd0a..ce75aeb2 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,15 +1,89 @@ # frozen_string_literal: true require "simplecov" -SimpleCov.start { add_filter("prettyprint.rb") } +SimpleCov.start do + add_filter("prettyprint.rb") + + unless ENV["CI"] + add_filter("accept_methods_test.rb") + add_filter("idempotency_test.rb") + end +end $LOAD_PATH.unshift(File.expand_path("../lib", __dir__)) require "syntax_tree" +require "syntax_tree/cli" require "json" +require "tempfile" require "pp" require "minitest/autorun" +module SyntaxTree + module Assertions + class Recorder + attr_reader :called + + def initialize + @called = nil + end + + def method_missing(called, ...) + @called = called + end + end + + private + + # This is a special kind of assertion that is going to get loaded into all + # of test cases. It asserts against a whole bunch of stuff that every node + # type should be able to handle. It's here so that we can use it in a bunch + # of tests. + def assert_syntax_tree(node) + # First, get the visit method name. + recorder = Recorder.new + node.accept(recorder) + + # Next, get the "type" which is effectively an underscored version of + # the name of the class. + type = recorder.called[/^visit_(.+)$/, 1] + + # Test that the method that is called when you call accept is a valid + # visit method on the visitor. + assert_respond_to(Visitor.new, recorder.called) + + # Test that you can call child_nodes and the pattern matching methods on + # this class. + assert_kind_of(Array, node.child_nodes) + assert_kind_of(Array, node.deconstruct) + assert_kind_of(Hash, node.deconstruct_keys([])) + + # Assert that it can be pretty printed to a string. + pretty = PP.singleline_pp(node, +"") + refute_includes(pretty, "#<") + assert_includes(pretty, type) + + # Serialize the node to JSON, parse it back out, and assert that we have + # found the expected type. + json = node.to_json + refute_includes(json, "#<") + assert_equal(type, JSON.parse(json)["type"]) + + # Get a match expression from the node, then assert that it can in fact + # match the node. + # rubocop:disable all + assert(eval(<<~RUBY)) + case node + in #{node.construct_keys} + true + end + RUBY + end + end +end + +Minitest::Test.include(SyntaxTree::Assertions) + # 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, @@ -22,12 +96,9 @@ module Fixtures fndptn rassign rassign_rocket - ] + ].freeze - FIXTURES_3_1_0 = %w[ - pinned_begin - var_field_rassign - ] + FIXTURES_3_1_0 = %w[pinned_begin var_field_rassign].freeze Fixture = Struct.new(:name, :source, :formatted, keyword_init: true) @@ -50,20 +121,30 @@ def self.each_fixture filepath = File.expand_path("fixtures/#{fixture}.rb", __dir__) # For each fixture in the fixture file yield a Fixture object. - File.readlines(filepath).slice_before(delimiter).each_with_index do |source, index| - comment = source.shift.match(delimiter)[1] - source, formatted = source.join.split("-\n") - - # If there's a comment starting with >= that starts after the % that - # delineates the test, then we're going to check if the version - # satisfies that constraint. - if comment&.start_with?(">=") - next if ruby_version < Gem::Version.new(comment.split[1]) - end + File + .readlines(filepath) + .slice_before(delimiter) + .each_with_index do |source, index| + comment = source.shift.match(delimiter)[1] + source, formatted = source.join.split("-\n") - name = :"#{fixture}_#{index}" - yield Fixture.new(name: name, source: source, formatted: formatted || source) - end + # If there's a comment starting with >= that starts after the % that + # delineates the test, then we're going to check if the version + # satisfies that constraint. + if comment&.start_with?(">=") && + (ruby_version < Gem::Version.new(comment.split[1])) + next + end + + name = :"#{fixture}_#{index}" + yield( + Fixture.new( + name: name, + source: source, + formatted: formatted || source + ) + ) + end end end end diff --git a/test/visitor_test.rb b/test/visitor_test.rb index 00cedda4..5e4f134d 100644 --- a/test/visitor_test.rb +++ b/test/visitor_test.rb @@ -2,73 +2,47 @@ require_relative "test_helper" -class VisitorTest < Minitest::Test - if ENV["CI"] - def test_visit_all_nodes - visitor = SyntaxTree::Visitor.new - - filepath = File.expand_path("../lib/syntax_tree/node.rb", __dir__) - program = SyntaxTree.parse(SyntaxTree.read(filepath)) - - program.statements.body.last.bodystmt.statements.body.each do |node| - case node - in SyntaxTree::ClassDeclaration[superclass: { value: { value: "Node" } }] - # this is a class we want to look at - else - next +module SyntaxTree + class VisitorTest < Minitest::Test + def test_visit_tree + parsed_tree = SyntaxTree.parse(<<~RUBY) + class Foo + def foo; end + + class Bar + def bar; end + end end - accept = - node.bodystmt.statements.body.detect do |defm| - case defm - in SyntaxTree::Def[name: { value: "accept" }] - true - else - false - end - end + def baz; end + RUBY - case accept - in { bodystmt: { statements: { body: [SyntaxTree::Call[message: { value: visit_method }]] } } } - assert_respond_to(visitor, visit_method) - end - end + visitor = DummyVisitor.new + visitor.visit(parsed_tree) + assert_equal(%w[Foo foo Bar bar baz], visitor.visited_nodes) end - end - def test_visit_tree - parsed_tree = SyntaxTree.parse(<<~RUBY) - class Foo - def foo; end + class DummyVisitor < Visitor + attr_reader :visited_nodes - class Bar - def bar; end - end + def initialize + super + @visited_nodes = [] end - def baz; end - RUBY - - visitor = DummyVisitor.new - visitor.visit(parsed_tree) - assert_equal(["Foo", "foo", "Bar", "bar", "baz"], visitor.visited_nodes) - end - - class DummyVisitor < SyntaxTree::Visitor - attr_reader :visited_nodes - - def initialize - super - @visited_nodes = [] - end + visit_method def visit_class(node) + @visited_nodes << node.constant.constant.value + super + end - visit_method def visit_class(node) - @visited_nodes << node.constant.constant.value - super + visit_method def visit_def(node) + @visited_nodes << node.name.value + end end - visit_method def visit_def(node) - @visited_nodes << node.name.value + def test_visit_method_correction + error = assert_raises { Visitor.visit_method(:visit_binar) } + assert_match(/visit_binary/, error.message) end end end