From 0d3e9b7bcc0b198ca92b617cc787b17744035dd8 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Mon, 10 Jan 2022 17:10:27 -0500 Subject: [PATCH 01/25] Language server --- exe/stree | 1 + lib/syntax_tree/cli.rb | 46 +++++++++-- lib/syntax_tree/language_server.rb | 80 +++++++++++++++++++ vscode/.gitignore | 3 + vscode/.vscode/launch.json | 21 +++++ vscode/.vscode/settings.json | 11 +++ vscode/.vscode/tasks.json | 20 +++++ vscode/.vscodeignore | 11 +++ vscode/package.json | 67 ++++++++++++++++ vscode/src/extension.ts | 123 +++++++++++++++++++++++++++++ vscode/tsconfig.json | 16 ++++ vscode/yarn.lock | 89 +++++++++++++++++++++ 12 files changed, 483 insertions(+), 5 deletions(-) create mode 100644 lib/syntax_tree/language_server.rb create mode 100644 vscode/.gitignore create mode 100644 vscode/.vscode/launch.json create mode 100644 vscode/.vscode/settings.json create mode 100644 vscode/.vscode/tasks.json create mode 100644 vscode/.vscodeignore create mode 100644 vscode/package.json create mode 100644 vscode/src/extension.ts create mode 100644 vscode/tsconfig.json create mode 100644 vscode/yarn.lock diff --git a/exe/stree b/exe/stree index 6ec4acae..3bae88e9 100755 --- a/exe/stree +++ b/exe/stree @@ -2,6 +2,7 @@ # frozen_string_literal: true $:.unshift(File.expand_path("../lib", __dir__)) + require "syntax_tree" require "syntax_tree/cli" diff --git a/lib/syntax_tree/cli.rb b/lib/syntax_tree/cli.rb index 82545df7..ac43eba9 100644 --- a/lib/syntax_tree/cli.rb +++ b/lib/syntax_tree/cli.rb @@ -135,22 +135,58 @@ def run(filepath, source) # The help message displayed if the input arguments are not correctly # ordered or formatted. HELP = <<~HELP - stree MODE FILE + stree ast [FILE] + Print out the AST corresponding to the given files - MODE: ast | check | debug | doc | format | write - FILE: one or more paths to files to parse + stree check [FILE] + Check that the given files are formatted as syntax tree would format them + + stree debug [FILE] + Check that the given files can be formatted idempotently + + stree doc [FILE] + Print out the doc tree that would be used to format the given files + + stree format [FILE] + Print out the formatted version of the given files + + stree help + Display this help message + + stree lsp + Run syntax tree in language server mode + + stree version + Output the current version of syntax tree + + stree write [FILE] + Read, format, and write back the source of the given files HELP class << self # Run the CLI over the given array of strings that make up the arguments # passed to the invocation. def run(argv) - if argv.length < 2 + arg, *patterns = argv + + case arg + when "help" + puts HELP + return 0 + when "lsp" + require "syntax_tree/language_server" + LanguageServer.new.run + return 0 + when "version" + puts SyntaxTree::VERSION + return 0 + end + + if patterns.empty? warn(HELP) return 1 end - arg, *patterns = argv action = case arg when "a", "ast" diff --git a/lib/syntax_tree/language_server.rb b/lib/syntax_tree/language_server.rb new file mode 100644 index 00000000..318efa3f --- /dev/null +++ b/lib/syntax_tree/language_server.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +require "cgi" +require "json" +require "uri" + +class SyntaxTree + class LanguageServer + attr_reader :input, :output + + def initialize(input: STDIN, output: STDOUT) + @input = input.binmode + @output = output.binmode + end + + def run + store = + Hash.new do |hash, uri| + hash[uri] = File.binread(CGI.unescape(URI.parse(uri).path)) + end + + while headers = input.gets("\r\n\r\n") + length = input.read(headers[/Content-Length: (\d+)/i, 1].to_i) + request = JSON.parse(length, symbolize_names: true) + + case request + in { method: "initialize", id: } + store.clear + write(id: id, result: { capabilities: capabilities }) + in { method: "initialized" } + # ignored + in { method: "shutdown" } + store.clear + return + in { method: "textDocument/didChange", params: { textDocument: { uri: }, contentChanges: [{ text: }, *] } } + store[uri] = text + in { method: "textDocument/didOpen", params: { textDocument: { uri:, text: } } } + store[uri] = text + in { method: "textDocument/didClose", params: { textDocument: { uri: } } } + store.delete(uri) + in { method: "textDocument/formatting", id:, params: { textDocument: { uri: } } } + write(id: id, result: [format(store[uri])]) + in { method: "syntaxTree/visualizing", id:, params: { textDocument: { uri: } } } + output = [] + PP.pp(SyntaxTree.parse(store[uri]), output) + write(id: id, result: output.join) + in { method: %r{\$/.+} } + # ignored + else + raise "Unhandled: #{request}" + end + end + end + + private + + def capabilities + { + documentFormattingProvider: true, + textDocumentSync: { change: 1, openClose: true }, + } + end + + def format(source) + { + range: { + start: { line: 0, character: 0 }, + end: { line: source.lines.size + 1, character: 0 } + }, + newText: SyntaxTree.format(source) + } + end + + def write(value) + response = value.merge(jsonrpc: "2.0").to_json + output.print("Content-Length: #{response.bytesize}\r\n\r\n#{response}") + output.flush + end + end +end diff --git a/vscode/.gitignore b/vscode/.gitignore new file mode 100644 index 00000000..9adedec5 --- /dev/null +++ b/vscode/.gitignore @@ -0,0 +1,3 @@ +node_modules +*.vsix +out diff --git a/vscode/.vscode/launch.json b/vscode/.vscode/launch.json new file mode 100644 index 00000000..42f351ce --- /dev/null +++ b/vscode/.vscode/launch.json @@ -0,0 +1,21 @@ +// A launch configuration that compiles the extension and then opens it inside a new window +// Use IntelliSense to learn about possible attributes. +// Hover to view descriptions of existing attributes. +// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Run Extension", + "type": "extensionHost", + "request": "launch", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}" + ], + "outFiles": [ + "${workspaceFolder}/out/**/*.js" + ], + "preLaunchTask": "${defaultBuildTask}" + }, + ] +} diff --git a/vscode/.vscode/settings.json b/vscode/.vscode/settings.json new file mode 100644 index 00000000..ebfbdcd6 --- /dev/null +++ b/vscode/.vscode/settings.json @@ -0,0 +1,11 @@ +// Place your settings in this file to overwrite default and user settings. +{ + "files.exclude": { + "out": false // set this to true to hide the "out" folder with the compiled JS files + }, + "search.exclude": { + "out": true // set this to false to include "out" folder in search results + }, + // Turn off tsc task auto detection since we have the necessary tasks as npm scripts + "typescript.tsc.autoDetect": "off" +} diff --git a/vscode/.vscode/tasks.json b/vscode/.vscode/tasks.json new file mode 100644 index 00000000..3b17e53b --- /dev/null +++ b/vscode/.vscode/tasks.json @@ -0,0 +1,20 @@ +// See https://go.microsoft.com/fwlink/?LinkId=733558 +// for the documentation about the tasks.json format +{ + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "watch", + "problemMatcher": "$tsc-watch", + "isBackground": true, + "presentation": { + "reveal": "never" + }, + "group": { + "kind": "build", + "isDefault": true + } + } + ] +} diff --git a/vscode/.vscodeignore b/vscode/.vscodeignore new file mode 100644 index 00000000..0a5a2bfe --- /dev/null +++ b/vscode/.vscodeignore @@ -0,0 +1,11 @@ +.vscode/** +.vscode-test/** +out/test/** +src/** +.gitignore +.yarnrc +vsc-extension-quickstart.md +**/tsconfig.json +**/.eslintrc.json +**/*.map +**/*.ts diff --git a/vscode/package.json b/vscode/package.json new file mode 100644 index 00000000..56822d86 --- /dev/null +++ b/vscode/package.json @@ -0,0 +1,67 @@ +{ + "name": "syntax_tree", + "displayName": "Syntax Tree", + "description": "VSCode integration for syntax tree", + "version": "0.1.0", + "publisher": "kddnewton", + "repository": { + "type": "git", + "url": "https://github.com/kddnewton/syntax_tree.git" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/kddnewton/syntax_tree/issues" + }, + "engines": { + "vscode": "^1.46.0" + }, + "activationEvents": [ + "onLanguage:ruby", + "workspaceContains:Gemfile.lock", + "onCommand:syntaxTree.start", + "onCommand:syntaxTree.stop", + "onCommand:syntaxTree.restart", + "onCommand:syntaxTree.showOutputChannel", + "onCommand:syntaxTree.visualize" + ], + "main": "./out/extension", + "contributes": { + "commands": [ + { + "command": "syntaxTree.start", + "title": "Syntax Tree: Start" + }, + { + "command": "syntaxTree.stop", + "title": "Syntax Tree: Stop" + }, + { + "command": "syntaxTree.restart", + "title": "Syntax Tree: Restart" + }, + { + "command": "syntaxTree.showOutputChannel", + "title": "Syntax Tree: Show Output Channel" + }, + { + "command": "syntaxTree.visualize", + "title": "Syntax Tree: Visualize" + } + ] + }, + "scripts": { + "compile": "tsc -p ./", + "package": "vsce package --yarn --githubBranch main", + "publish": "vsce publish --yarn --githubBranch main", + "vscode:prepublish": "yarn compile", + "watch": "tsc --watch -p ./" + }, + "dependencies": { + "vscode-languageclient": "^7.0.0" + }, + "devDependencies": { + "@types/node": "^17.0.8", + "@types/vscode": "^1.46.0", + "typescript": "^4.2.2" + } +} diff --git a/vscode/src/extension.ts b/vscode/src/extension.ts new file mode 100644 index 00000000..43f6e806 --- /dev/null +++ b/vscode/src/extension.ts @@ -0,0 +1,123 @@ +"use strict"; + +import { ExtensionContext, TextDocumentContentProvider, Uri, commands, languages, window, workspace, ViewColumn } from "vscode"; +import { LanguageClient } from "vscode-languageclient/node"; +import { promisify } from "util"; +import { exec } from "child_process"; + +const promiseExec = promisify(exec); + +export function activate(context: ExtensionContext) { + const scheme = "syntaxTree"; + + let languageClient: LanguageClient | null = null; + const outputChannel = window.createOutputChannel("Syntax Tree"); + + context.subscriptions.push( + commands.registerCommand(`${scheme}.start`, startClient), + commands.registerCommand(`${scheme}.stop`, stopClient), + commands.registerCommand(`${scheme}.restart`, restartClient), + commands.registerCommand(`${scheme}.showOutputChannel`, showOutputChannel), + commands.registerCommand(`${scheme}.visualize`, visualize), + outputChannel + ); + + context.subscriptions.push(workspace.registerTextDocumentContentProvider(scheme, new class implements TextDocumentContentProvider { + provideTextDocumentContent(uri: Uri): Promise { + if (languageClient) { + return languageClient.sendRequest(`${scheme}/visualizing`, { + textDocument: { uri: uri.path } + }); + } else { + return Promise.resolve("Language client not initialized."); + } + } + })); + + return startClient(); + + async function startLocalClient() { + const rootFolder = workspace.workspaceFolders![0]; + const cwd = rootFolder.uri.fsPath; + + try { + await promiseExec("bundle show syntax_tree", { cwd }); + } catch { + outputChannel.appendLine(""); + outputChannel.appendLine("Error: Cannot find `syntax_tree` in Gemfile."); + outputChannel.appendLine("Please add `syntax_tree` in your Gemfile and `bundle install`."); + outputChannel.appendLine(""); + return; + } + + // In this case we're running inside a project, so we're going to attempt + // to run stree with bundler. + const run = { + command: "bundle", + args: ["exec", "stree", "lsp"], + options: { cwd } + }; + + languageClient = new LanguageClient("Syntax Tree", { run, debug: run }, { + documentSelector: [ + { scheme: "file", language: "ruby" }, + ], + outputChannel + }); + } + + function startGlobalClient() { + const run = { command: "stree", args: ["lsp"] } + + languageClient = new LanguageClient("Syntax Tree", { run, debug: run }, { + documentSelector: [ + { scheme: "file", language: "ruby" }, + ], + outputChannel + }); + } + + async function startClient() { + outputChannel.appendLine("Starting language server..."); + + if (workspace.workspaceFolders) { + await startLocalClient(); + } else { + startGlobalClient(); + } + + if (languageClient) { + context.subscriptions.push(languageClient.start()); + } + } + + async function stopClient() { + if (languageClient) { + outputChannel.appendLine("Stopping language server..."); + await languageClient.stop(); + } + } + + async function restartClient() { + outputChannel.appendLine("Restarting language server..."); + await stopClient(); + await startClient(); + } + + function showOutputChannel() { + outputChannel.show(); + } + + async function visualize() { + const document = window.activeTextEditor?.document; + + if (languageClient && document && document.languageId === "ruby" && document.uri.scheme === "file") { + const uri = Uri.parse(`${scheme}:${document.uri.toString()}`); + + const doc = await workspace.openTextDocument(uri); + languages.setTextDocumentLanguage(doc, "plaintext"); + + await window.showTextDocument(doc, ViewColumn.Beside, true); + } + } +} diff --git a/vscode/tsconfig.json b/vscode/tsconfig.json new file mode 100644 index 00000000..53af4bb5 --- /dev/null +++ b/vscode/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "outDir": "out", + "lib": [ + "es6" + ], + "sourceMap": true, + "rootDir": "src", + "strict": true + }, + "exclude": [ + "node_modules" + ] +} diff --git a/vscode/yarn.lock b/vscode/yarn.lock new file mode 100644 index 00000000..986cb186 --- /dev/null +++ b/vscode/yarn.lock @@ -0,0 +1,89 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/node@^17.0.8": + version "17.0.8" + resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.8.tgz#50d680c8a8a78fe30abe6906453b21ad8ab0ad7b" + integrity sha512-YofkM6fGv4gDJq78g4j0mMuGMkZVxZDgtU0JRdx6FgiJDG+0fY0GKVolOV8WqVmEhLCXkQRjwDdKyPxJp/uucg== + +"@types/vscode@^1.46.0": + version "1.63.1" + resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.63.1.tgz#b40f9f18055e2c9498ae543d18c59fbd6ef2e8a3" + integrity sha512-Z+ZqjRcnGfHP86dvx/BtSwWyZPKQ/LBdmAVImY82TphyjOw2KgTKcp7Nx92oNwCTsHzlshwexAG/WiY2JuUm3g== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +semver@^7.3.4: + version "7.3.5" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" + integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== + dependencies: + lru-cache "^6.0.0" + +typescript@^4.2.2: + version "4.5.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.4.tgz#a17d3a0263bf5c8723b9c52f43c5084edf13c2e8" + integrity sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg== + +vscode-jsonrpc@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz#108bdb09b4400705176b957ceca9e0880e9b6d4e" + integrity sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg== + +vscode-languageclient@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-7.0.0.tgz#b505c22c21ffcf96e167799757fca07a6bad0fb2" + integrity sha512-P9AXdAPlsCgslpP9pRxYPqkNYV7Xq8300/aZDpO35j1fJm/ncize8iGswzYlcvFw5DQUx4eVk+KvfXdL0rehNg== + dependencies: + minimatch "^3.0.4" + semver "^7.3.4" + vscode-languageserver-protocol "3.16.0" + +vscode-languageserver-protocol@3.16.0: + version "3.16.0" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0.tgz#34135b61a9091db972188a07d337406a3cdbe821" + integrity sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A== + dependencies: + vscode-jsonrpc "6.0.0" + vscode-languageserver-types "3.16.0" + +vscode-languageserver-types@3.16.0: + version "3.16.0" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz#ecf393fc121ec6974b2da3efb3155644c514e247" + integrity sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== From 4ead45cb39429dec86a2007a481c1fd93c6c1651 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Mon, 24 Jan 2022 10:45:29 -0500 Subject: [PATCH 02/25] Implicits --- bin/bench | 22 ++++---- bin/console | 6 +- bin/profile | 6 +- lib/syntax_tree.rb | 67 ++++++++++++++++++++-- lib/syntax_tree/cli.rb | 22 +++++--- lib/syntax_tree/implicits.rb | 91 ++++++++++++++++++++++++++++++ lib/syntax_tree/language_server.rb | 14 +++++ vscode/package.json | 11 ++++ vscode/src/Implicits.ts | 58 +++++++++++++++++++ vscode/src/extension.ts | 30 ++++++++++ 10 files changed, 297 insertions(+), 30 deletions(-) create mode 100644 lib/syntax_tree/implicits.rb create mode 100644 vscode/src/Implicits.ts diff --git a/bin/bench b/bin/bench index 307d3044..76270c87 100755 --- a/bin/bench +++ b/bin/bench @@ -1,23 +1,23 @@ #!/usr/bin/env ruby # frozen_string_literal: true -require 'bundler/setup' -require 'benchmark/ips' +require "bundler/setup" +require "benchmark/ips" -require_relative '../lib/syntax_tree' -require 'ruby_parser' -require 'parser/current' +require_relative "../lib/syntax_tree" +require "ruby_parser" +require "parser/current" def compare(filepath) - prefix = "#{File.expand_path('..', __dir__)}/" + prefix = "#{File.expand_path("..", __dir__)}/" puts "=== #{filepath.delete_prefix(prefix)} ===" source = File.read(filepath) Benchmark.ips do |x| - x.report('syntax_tree') { SyntaxTree.new(source).parse } - x.report('parser') { Parser::CurrentRuby.parse(source) } - x.report('ruby_parser') { RubyParser.new.parse(source) } + x.report("syntax_tree") { SyntaxTree.new(source).parse } + x.report("parser") { Parser::CurrentRuby.parse(source) } + x.report("ruby_parser") { RubyParser.new.parse(source) } x.compare! end end @@ -29,8 +29,8 @@ filepaths = ARGV # file). if filepaths.empty? filepaths = [ - File.expand_path('bench', __dir__), - File.expand_path('../lib/syntax_tree.rb', __dir__) + File.expand_path("bench", __dir__), + File.expand_path("../lib/syntax_tree.rb", __dir__) ] end diff --git a/bin/console b/bin/console index e19966f5..1c18bd62 100755 --- a/bin/console +++ b/bin/console @@ -1,8 +1,8 @@ #!/usr/bin/env ruby # frozen_string_literal: true -require 'bundler/setup' -require 'syntax_tree' +require "bundler/setup" +require "syntax_tree" -require 'irb' +require "irb" IRB.start(__FILE__) diff --git a/bin/profile b/bin/profile index 86c025e4..550402e3 100755 --- a/bin/profile +++ b/bin/profile @@ -1,10 +1,10 @@ #!/usr/bin/env ruby # frozen_string_literal: true -require 'bundler/setup' -require 'stackprof' +require "bundler/setup" +require "stackprof" -filepath = File.expand_path('../lib/syntax_tree', __dir__) +filepath = File.expand_path("../lib/syntax_tree", __dir__) require_relative filepath GC.disable diff --git a/lib/syntax_tree.rb b/lib/syntax_tree.rb index 22cc283a..6bcf710f 100644 --- a/lib/syntax_tree.rb +++ b/lib/syntax_tree.rb @@ -1825,6 +1825,15 @@ def child_nodes [target, value] end + def deconstruct_keys(keys) + { + target: target, + value: value, + location: location, + comments: comments + } + end + def format(q) q.group do q.format(target) @@ -1917,6 +1926,15 @@ def child_nodes [key, value] end + def deconstruct_keys(keys) + { + key: key, + value: value, + location: location, + comments: comments + } + end + def format(q) if value&.is_a?(HashLiteral) format_contents(q) @@ -2472,6 +2490,16 @@ def child_nodes [left, right] end + def deconstruct_keys(keys) + { + left: left, + operator: operator, + right: right, + location: location, + comments: comments + } + end + def format(q) power = operator == :** @@ -6799,9 +6827,7 @@ def format(q) if constant q.format(constant) - q.text("[") - contents.call - q.text("]") + q.group(0, "[", "]", &contents) return end @@ -7145,6 +7171,16 @@ def child_nodes [predicate, truthy, falsy] end + def deconstruct_keys(keys) + { + predicate: predicate, + truthy: truthy, + falsy: falsy, + location: location, + comments: comments + } + end + def format(q) force_flat = [ Alias, Assign, Break, Command, CommandCall, Heredoc, If, IfMod, IfOp, @@ -7252,7 +7288,7 @@ def initialize(keyword, node) end def format(q) - if ContainsAssignment.call(node.statement) + if ContainsAssignment.call(node.statement) || q.parent.is_a?(In) q.group { format_flat(q) } else q.group { q.if_break { format_break(q) }.if_flat { format_flat(q) } } @@ -7808,6 +7844,10 @@ def child_nodes [] end + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + def format(q) q.text(value) end @@ -10418,6 +10458,16 @@ def child_nodes [exception, statements, consequent] end + def deconstruct_keys(keys) + { + exception: exception, + statements: statements, + consequent: consequent, + location: location, + comments: comments + } + end + def format(q) q.group do q.text("rescue") @@ -12287,6 +12337,15 @@ def child_nodes [statement] end + def deconstruct_keys(keys) + { + operator: operator, + statement: statement, + location: location, + comments: comments + } + end + def format(q) q.text(operator) q.format(statement) diff --git a/lib/syntax_tree/cli.rb b/lib/syntax_tree/cli.rb index ac43eba9..91fa4cb6 100644 --- a/lib/syntax_tree/cli.rb +++ b/lib/syntax_tree/cli.rb @@ -15,6 +15,10 @@ def to_s "\033[#{code}m#{value}\033[0m" end + def self.bold(value) + new(value, "1") + end + def self.gray(value) new(value, "38;5;102") end @@ -135,31 +139,31 @@ def run(filepath, source) # The help message displayed if the input arguments are not correctly # ordered or formatted. HELP = <<~HELP - stree ast [FILE] + #{Color.bold("stree ast [FILE]")} Print out the AST corresponding to the given files - stree check [FILE] + #{Color.bold("stree check [FILE]")} Check that the given files are formatted as syntax tree would format them - stree debug [FILE] + #{Color.bold("stree debug [FILE]")} Check that the given files can be formatted idempotently - stree doc [FILE] + #{Color.bold("stree doc [FILE]")} Print out the doc tree that would be used to format the given files - stree format [FILE] + #{Color.bold("stree format [FILE]")} Print out the formatted version of the given files - stree help + #{Color.bold("stree help")} Display this help message - stree lsp + #{Color.bold("stree lsp")} Run syntax tree in language server mode - stree version + #{Color.bold("stree version")} Output the current version of syntax tree - stree write [FILE] + #{Color.bold("stree write [FILE]")} Read, format, and write back the source of the given files HELP diff --git a/lib/syntax_tree/implicits.rb b/lib/syntax_tree/implicits.rb new file mode 100644 index 00000000..191f7a74 --- /dev/null +++ b/lib/syntax_tree/implicits.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +class SyntaxTree + class Implicits + attr_reader :before, :after + + def initialize + @before = Hash.new { |hash, key| hash[key] = +"" } + @after = Hash.new { |hash, key| hash[key] = +"" } + end + + # Adds the implicitly rescued StandardError into a bare rescue clause. For + # example, + # + # begin + # rescue + # end + # + # becomes + # + # begin + # rescue StandardError + # end + # + def bare_rescue(location) + after[location.start_char + "rescue".length] << " StandardError" + end + + # Adds the implicitly referenced value (local variable or method call) that + # is added into a hash when the value of a key-value pair is omitted. For + # example, + # + # { value: } + # + # becomes + # + # { value: value } + # + def missing_hash_value(key, location) + after[location.end_char] << " #{key}" + end + + # Adds implicit parentheses around certain expressions to make it clear + # which subexpression will be evaluated first. For example, + # + # a + b * c + # + # becomes + # + # a + ₍b * c₎ + # + def precedence_parentheses(location) + before[location.start_char] << "₍" + after[location.end_char] << "₎" + end + + def self.find(program) + implicits = new + queue = [[nil, program]] + + until queue.empty? + parent_node, child_node = queue.shift + + child_node.child_nodes.each do |grand_child_node| + queue << [child_node, grand_child_node] if grand_child_node + end + + case [parent_node, child_node] + in _, Rescue[exception: nil, location:] + implicits.bare_rescue(location) + in _, Assoc[key: Label[value: key], value: nil, location:] + implicits.missing_hash_value(key[0...-1], location) + in Assign | Binary | IfOp | OpAssign, IfOp[location:] + implicits.precedence_parentheses(location) + in Assign | OpAssign, Binary[location:] + implicits.precedence_parentheses(location) + in Binary[operator: parent_oper], Binary[operator: child_oper, location:] if parent_oper != child_oper + implicits.precedence_parentheses(location) + in Binary, Unary[operator: "-", location:] + implicits.precedence_parentheses(location) + in Params, Assign[location:] + implicits.precedence_parentheses(location) + else + # do nothing + end + end + + implicits + end + end +end diff --git a/lib/syntax_tree/language_server.rb b/lib/syntax_tree/language_server.rb index 318efa3f..ef8172ac 100644 --- a/lib/syntax_tree/language_server.rb +++ b/lib/syntax_tree/language_server.rb @@ -4,6 +4,8 @@ require "json" require "uri" +require_relative "implicits" + class SyntaxTree class LanguageServer attr_reader :input, :output @@ -40,6 +42,8 @@ def run store.delete(uri) in { method: "textDocument/formatting", id:, params: { textDocument: { uri: } } } write(id: id, result: [format(store[uri])]) + in { method: "syntaxTree/implicits", id:, params: { textDocument: { uri: } } } + write(id: id, result: implicits(store[uri])) in { method: "syntaxTree/visualizing", id:, params: { textDocument: { uri: } } } output = [] PP.pp(SyntaxTree.parse(store[uri]), output) @@ -71,6 +75,16 @@ def format(source) } end + def implicits(source) + implicits = Implicits.find(SyntaxTree.parse(source)) + serialize = ->(position, text) { { position: position, text: text } } + + { + before: implicits.before.map(&serialize), + after: implicits.after.map(&serialize) + } + end + def write(value) response = value.merge(jsonrpc: "2.0").to_json output.print("Content-Length: #{response.bytesize}\r\n\r\n#{response}") diff --git a/vscode/package.json b/vscode/package.json index 56822d86..a82c0857 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -47,6 +47,17 @@ "command": "syntaxTree.visualize", "title": "Syntax Tree: Visualize" } + ], + "colors": [ + { + "id": "syntaxTree.implicits", + "description": "Text color for the inserted implicits", + "defaults": { + "dark": "#707070", + "light": "#999999", + "highContrast": "foreground" + } + } ] }, "scripts": { diff --git a/vscode/src/Implicits.ts b/vscode/src/Implicits.ts new file mode 100644 index 00000000..fb171fdd --- /dev/null +++ b/vscode/src/Implicits.ts @@ -0,0 +1,58 @@ +import { DecorationOptions, DecorationRangeBehavior, Disposable, OutputChannel, Range, TextEditor, TextEditorDecorationType, ThemeColor, window } from "vscode"; +import { LanguageClient } from "vscode-languageclient/node"; + +type Implicit = { position: number, text: string }; +type ImplicitsResponse = { before: Implicit[], after: Implicit[] }; + +class Implicits implements Disposable { + private languageClient: LanguageClient; + private outputChannel: OutputChannel; + private decorationType: TextEditorDecorationType; + + constructor(languageClient: LanguageClient, outputChannel: OutputChannel) { + this.languageClient = languageClient; + this.outputChannel = outputChannel; + + const color = new ThemeColor("syntaxTree.implicits"); + this.decorationType = window.createTextEditorDecorationType({ + before: { color, fontStyle: "normal" }, + after: { color, fontStyle: "normal" }, + rangeBehavior: DecorationRangeBehavior.ClosedClosed + }); + + for (const editor of window.visibleTextEditors) { + this.setImplicitsForEditor(editor); + } + } + + dispose() { + this.decorationType.dispose(); + } + + async setImplicitsForEditor(editor: TextEditor) { + if (editor.document.languageId != "ruby") { + return; + } + + this.outputChannel.appendLine("Requesting implicits"); + const implicits = await this.languageClient.sendRequest("syntaxTree/implicits", { + textDocument: { uri: editor.document.uri.toString() } + }); + + const decorations: DecorationOptions[] = [ + ...implicits.before.map(({ position, text: contentText }) => ({ + range: new Range(editor.document.positionAt(position), editor.document.positionAt(position)), + renderOptions: { before: { contentText } } + })), + ...implicits.after.map(({ position, text: contentText }) => ({ + range: new Range(editor.document.positionAt(position), editor.document.positionAt(position)), + renderOptions: { after: { contentText } } + })) + ]; + + this.outputChannel.appendLine("Settings implicits"); + editor.setDecorations(this.decorationType, decorations); + } +} + +export default Implicits; diff --git a/vscode/src/extension.ts b/vscode/src/extension.ts index 43f6e806..b8505558 100644 --- a/vscode/src/extension.ts +++ b/vscode/src/extension.ts @@ -5,6 +5,8 @@ import { LanguageClient } from "vscode-languageclient/node"; import { promisify } from "util"; import { exec } from "child_process"; +import Implicits from "./Implicits"; + const promiseExec = promisify(exec); export function activate(context: ExtensionContext) { @@ -88,6 +90,10 @@ export function activate(context: ExtensionContext) { if (languageClient) { context.subscriptions.push(languageClient.start()); + await languageClient.onReady(); + + const implicits = new Implicits(languageClient, outputChannel) + context.subscriptions.push(implicits); } } @@ -121,3 +127,27 @@ export function activate(context: ExtensionContext) { } } } + + + +// # const paren = { before: "₍", after: "₎" }; + +// # def to_vscode +// # { before: before, after: after }.flat_map do |type, positions| +// # positions.map do |position, count| + +// # end +// # end + +// # left.map do |position, count| + +// # end +// # end + +// # # range: new Range( +// # # editor.document.positionAt(pos), +// # # editor.document.positionAt(pos) +// # # ), +// # # renderOptions: { +// # # [beforeAfter]: { contentText: repeat(paren[beforeAfter], count) }, +// # # }, \ No newline at end of file From 9c75ed289bebed1acdff5065aa933c44e2c964cf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Jan 2022 17:21:18 +0000 Subject: [PATCH 03/25] Bump benchmark-ips from 2.9.2 to 2.9.3 Bumps [benchmark-ips](https://github.com/evanphx/benchmark-ips) from 2.9.2 to 2.9.3. - [Release notes](https://github.com/evanphx/benchmark-ips/releases) - [Changelog](https://github.com/evanphx/benchmark-ips/blob/master/History.md) - [Commits](https://github.com/evanphx/benchmark-ips/compare/v2.9.2...v2.9.3) --- updated-dependencies: - dependency-name: benchmark-ips dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 8dcdb592..1ae88ac5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,7 +7,7 @@ GEM remote: https://rubygems.org/ specs: ast (2.4.2) - benchmark-ips (2.9.2) + benchmark-ips (2.9.3) docile (1.4.0) minitest (5.15.0) parser (3.1.0.0) From 748efa131f5bdb5cff607d01de46ab40e29921f3 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Tue, 25 Jan 2022 10:59:02 -0500 Subject: [PATCH 04/25] Handle document changes in implicits --- lib/syntax_tree/language_server.rb | 1 + vscode/src/Implicits.ts | 158 ++++++++++++++++++++++++++--- vscode/src/Visualize.ts | 48 +++++++++ vscode/src/extension.ts | 138 ++++--------------------- vscode/src/startLanguageClient.ts | 57 +++++++++++ 5 files changed, 269 insertions(+), 133 deletions(-) create mode 100644 vscode/src/Visualize.ts create mode 100644 vscode/src/startLanguageClient.ts diff --git a/lib/syntax_tree/language_server.rb b/lib/syntax_tree/language_server.rb index ef8172ac..54abf5c8 100644 --- a/lib/syntax_tree/language_server.rb +++ b/lib/syntax_tree/language_server.rb @@ -83,6 +83,7 @@ def implicits(source) before: implicits.before.map(&serialize), after: implicits.after.map(&serialize) } + rescue ParseError end def write(value) diff --git a/vscode/src/Implicits.ts b/vscode/src/Implicits.ts index fb171fdd..52b1b029 100644 --- a/vscode/src/Implicits.ts +++ b/vscode/src/Implicits.ts @@ -1,13 +1,23 @@ -import { DecorationOptions, DecorationRangeBehavior, Disposable, OutputChannel, Range, TextEditor, TextEditorDecorationType, ThemeColor, window } from "vscode"; +import { DecorationOptions, DecorationRangeBehavior, Disposable, OutputChannel, Range, TextDocument, TextDocumentChangeEvent, TextEditor, TextEditorDecorationType, ThemeColor, window, workspace } from "vscode"; import { LanguageClient } from "vscode-languageclient/node"; type Implicit = { position: number, text: string }; -type ImplicitsResponse = { before: Implicit[], after: Implicit[] }; +type ImplicitSet = { before: Implicit[], after: Implicit[] }; class Implicits implements Disposable { - private languageClient: LanguageClient; - private outputChannel: OutputChannel; - private decorationType: TextEditorDecorationType; + // The client used to communicate with the language server. In the case of + // this class it's used to send a syntaxTree/implicits request. + private readonly languageClient: LanguageClient; + + // The output channel used for logging for this class. It's given from the + // main file so that it uses the same as the rest of the extension. + private readonly outputChannel: OutputChannel; + + private readonly decorationType: TextEditorDecorationType; + private readonly implicitsCache: WeakMap; + private readonly debouncedHandleTextDocumentChange: Debounced; + + private readonly disposables: Disposable[]; constructor(languageClient: LanguageClient, outputChannel: OutputChannel) { this.languageClient = languageClient; @@ -20,25 +30,95 @@ class Implicits implements Disposable { rangeBehavior: DecorationRangeBehavior.ClosedClosed }); - for (const editor of window.visibleTextEditors) { + this.implicitsCache = new WeakMap(); + this.setImplicitsForEditors(window.visibleTextEditors); + + // Here we're going to debounce the handleTextDocumentChange callback so + // that we're not reacting too quickly to user inputs and making it flash + // alll around the editor. + this.debouncedHandleTextDocumentChange = debounce(300, (editor: TextEditor) => { + this.outputChannel.appendLine("Handling text document changes (debounced)"); + this.implicitsCache.delete(editor.document); this.setImplicitsForEditor(editor); - } + }); + + // Track all of the various callbacks and objects that implement Disposable + // so that when we need to dispose of the entire Implicits instance we can + // iterate through and dispose all of them. + this.disposables = [ + this.decorationType, + window.onDidChangeVisibleTextEditors(this.setImplicitsForEditors, this), + workspace.onDidChangeTextDocument(this.handleTextDocumentChange, this), + this.debouncedHandleTextDocumentChange + ]; } dispose() { - this.decorationType.dispose(); + this.disposables.forEach((disposable) => disposable.dispose()); } - async setImplicitsForEditor(editor: TextEditor) { - if (editor.document.languageId != "ruby") { - return; + handleTextDocumentChange(event: TextDocumentChangeEvent) { + const editor = window.activeTextEditor; + + if (editor !== undefined && event.document === editor.document) { + this.debouncedHandleTextDocumentChange(editor); + } else { + this.implicitsCache.delete(event.document); } + } + + // Asynchronously get the implicits for a given text document, optionally + // using a cache if it has already been populated. + async getImplicitsForTextDocument(document: TextDocument): Promise { + if (document.languageId !== "ruby") { + // This editor may have previously been a Ruby file, but that has now + // changed. So we should delete the implicits that may be there. + if (this.implicitsCache.has(document)) { + this.implicitsCache.delete(document); + // Return an empty set of implicits so that it gets properly cleared + // from the document. + return { before: [], after: [] }; + } + + // Otherwise, we're going to return undefined so that we don't bother with + // this file, as it's not a Ruby file. + return undefined; + } + + // Check the cache first to see if we have already computed the implicits + // for this document. Return them if we have them. + let implicits = this.implicitsCache.get(document); + if (implicits) { + this.outputChannel.appendLine("Loading implicits from cache"); + return implicits; + } + + // Otherwise, asynchronously request the implicits from the language server, + // cache the response, and return it. this.outputChannel.appendLine("Requesting implicits"); - const implicits = await this.languageClient.sendRequest("syntaxTree/implicits", { - textDocument: { uri: editor.document.uri.toString() } + implicits = await this.languageClient.sendRequest("syntaxTree/implicits", { + textDocument: { uri: document.uri.toString() } }); + // In case of a syntax error, this is not going to return anything. In that + // case, we don't want to set the cache to anything, but we also don't want + // to clear the previous implicits either. So we're just going to return + // undefined + if (!implicits) { + return undefined; + } + + this.implicitsCache.set(document, implicits); + return implicits; + } + + async setImplicitsForEditor(editor: TextEditor) { + const implicits = await this.getImplicitsForTextDocument(editor.document); + if (!implicits) { + return; + } + const decorations: DecorationOptions[] = [ ...implicits.before.map(({ position, text: contentText }) => ({ range: new Range(editor.document.positionAt(position), editor.document.positionAt(position)), @@ -53,6 +133,58 @@ class Implicits implements Disposable { this.outputChannel.appendLine("Settings implicits"); editor.setDecorations(this.decorationType, decorations); } + + setImplicitsForEditors(editors: readonly TextEditor[]) { + editors.forEach((editor) => this.setImplicitsForEditor(editor)); + } +} + +// The return value of the debounce function below. +type Debounced = Disposable & ((argument: T) => void); + +// This function will take a given callback and delay it by a given delay. If +// another call to the same function with the same argument comes in before the +// first one finishes, it will cancel the first invocation and start the delay +// again. +function debounce(delay: number, callback: (argument: T) => void): Debounced { + let allTimeouts = new WeakMap(); + const liveTimeouts = new Set(); + + const debounced = (argument: T) => { + // First, clear out the timeout for the previous call to this debounced + // callback. + const previousTimeout = allTimeouts.get(argument); + if (previousTimeout !== undefined) { + clearTimeout(previousTimeout); + liveTimeouts.delete(previousTimeout); + } + + // Next, create a new timeout for this call to the debounced callback. + // If it doesn't get cancelled by a subsequent call, it will be invoked with + // the given argument. + const timeout = setTimeout(() => { + allTimeouts.delete(argument); + liveTimeouts.delete(timeout); + callback(argument); + }, delay); + + // Finally, track the timeout that we just created in both a WeakMap + // that links the editor to the timeout and a set. We track both so that + // we can iterate through the timeouts when VSCode needs to dispose of + // this subscription. + allTimeouts.set(argument, timeout); + liveTimeouts.add(timeout); + }; + + // Define the necessary dispose function so that VSCode knows how to + // trigger disposal of this entire debounced function. + debounced.dispose = () => { + liveTimeouts.forEach((timeout) => clearTimeout(timeout)); + liveTimeouts.clear(); + allTimeouts = new WeakMap(); + }; + + return debounced; } export default Implicits; diff --git a/vscode/src/Visualize.ts b/vscode/src/Visualize.ts new file mode 100644 index 00000000..baf1996e --- /dev/null +++ b/vscode/src/Visualize.ts @@ -0,0 +1,48 @@ +import { commands, Disposable, languages, OutputChannel, ProviderResult, TextDocumentContentProvider, Uri, ViewColumn, window, workspace } from "vscode"; +import { LanguageClient } from "vscode-languageclient/node"; + +class Visualize implements Disposable, TextDocumentContentProvider { + // The client used to communicate with the language server. + private readonly languageClient: LanguageClient; + + // The output channel used for logging for this class. It's given from the + // main file so that it uses the same as the rest of the extension. + private readonly outputChannel: OutputChannel; + + // The list of callbacks and objects that should be disposed when an instance + // of Visualize is being disposed. + private readonly disposables: Disposable[]; + + constructor(languageClient: LanguageClient, outputChannel: OutputChannel) { + this.languageClient = languageClient; + this.outputChannel = outputChannel; + this.disposables = [ + commands.registerCommand("syntaxTree.visualize", this.visualize), + workspace.registerTextDocumentContentProvider("syntaxTree", this) + ]; + } + + dispose() { + this.disposables.forEach((disposable) => disposable.dispose()); + } + + provideTextDocumentContent(uri: Uri): ProviderResult { + this.outputChannel.appendLine("Requesting visualization"); + return this.languageClient.sendRequest("syntaxTree/visualizing", { textDocument: { uri: uri.path } }); + } + + async visualize() { + const document = window.activeTextEditor?.document; + + if (document && document.languageId === "ruby" && document.uri.scheme === "file") { + const uri = Uri.parse(`syntaxTree:${document.uri.toString()}`); + + const doc = await workspace.openTextDocument(uri); + languages.setTextDocumentLanguage(doc, "plaintext"); + + await window.showTextDocument(doc, ViewColumn.Beside, true); + } + } +} + +export default Visualize; diff --git a/vscode/src/extension.ts b/vscode/src/extension.ts index b8505558..ef6491cd 100644 --- a/vscode/src/extension.ts +++ b/vscode/src/extension.ts @@ -1,153 +1,51 @@ "use strict"; -import { ExtensionContext, TextDocumentContentProvider, Uri, commands, languages, window, workspace, ViewColumn } from "vscode"; +import { ExtensionContext, commands, window } from "vscode"; import { LanguageClient } from "vscode-languageclient/node"; -import { promisify } from "util"; -import { exec } from "child_process"; import Implicits from "./Implicits"; - -const promiseExec = promisify(exec); +import startLanguageClient from "./startLanguageClient"; +import Visualize from "./Visualize"; export function activate(context: ExtensionContext) { - const scheme = "syntaxTree"; - let languageClient: LanguageClient | null = null; const outputChannel = window.createOutputChannel("Syntax Tree"); context.subscriptions.push( - commands.registerCommand(`${scheme}.start`, startClient), - commands.registerCommand(`${scheme}.stop`, stopClient), - commands.registerCommand(`${scheme}.restart`, restartClient), - commands.registerCommand(`${scheme}.showOutputChannel`, showOutputChannel), - commands.registerCommand(`${scheme}.visualize`, visualize), + commands.registerCommand("syntaxTree.start", startLanguageServer), + commands.registerCommand("syntaxTree.stop", stopLanguageServer), + commands.registerCommand("syntaxTree.restart", restartLanguageServer), + commands.registerCommand("syntaxTree.showOutputChannel", () => outputChannel.show()), outputChannel ); - context.subscriptions.push(workspace.registerTextDocumentContentProvider(scheme, new class implements TextDocumentContentProvider { - provideTextDocumentContent(uri: Uri): Promise { - if (languageClient) { - return languageClient.sendRequest(`${scheme}/visualizing`, { - textDocument: { uri: uri.path } - }); - } else { - return Promise.resolve("Language client not initialized."); - } - } - })); - - return startClient(); - - async function startLocalClient() { - const rootFolder = workspace.workspaceFolders![0]; - const cwd = rootFolder.uri.fsPath; + return startLanguageServer(); - try { - await promiseExec("bundle show syntax_tree", { cwd }); - } catch { - outputChannel.appendLine(""); - outputChannel.appendLine("Error: Cannot find `syntax_tree` in Gemfile."); - outputChannel.appendLine("Please add `syntax_tree` in your Gemfile and `bundle install`."); - outputChannel.appendLine(""); - return; - } - - // In this case we're running inside a project, so we're going to attempt - // to run stree with bundler. - const run = { - command: "bundle", - args: ["exec", "stree", "lsp"], - options: { cwd } - }; - - languageClient = new LanguageClient("Syntax Tree", { run, debug: run }, { - documentSelector: [ - { scheme: "file", language: "ruby" }, - ], - outputChannel - }); - } - - function startGlobalClient() { - const run = { command: "stree", args: ["lsp"] } - - languageClient = new LanguageClient("Syntax Tree", { run, debug: run }, { - documentSelector: [ - { scheme: "file", language: "ruby" }, - ], - outputChannel - }); - } - - async function startClient() { + async function startLanguageServer() { outputChannel.appendLine("Starting language server..."); - - if (workspace.workspaceFolders) { - await startLocalClient(); - } else { - startGlobalClient(); - } + languageClient = await startLanguageClient(outputChannel); if (languageClient) { context.subscriptions.push(languageClient.start()); await languageClient.onReady(); - const implicits = new Implicits(languageClient, outputChannel) - context.subscriptions.push(implicits); + context.subscriptions.push( + new Implicits(languageClient, outputChannel), + new Visualize(languageClient, outputChannel) + ); } } - async function stopClient() { + async function stopLanguageServer() { if (languageClient) { outputChannel.appendLine("Stopping language server..."); await languageClient.stop(); } } - async function restartClient() { + async function restartLanguageServer() { outputChannel.appendLine("Restarting language server..."); - await stopClient(); - await startClient(); - } - - function showOutputChannel() { - outputChannel.show(); - } - - async function visualize() { - const document = window.activeTextEditor?.document; - - if (languageClient && document && document.languageId === "ruby" && document.uri.scheme === "file") { - const uri = Uri.parse(`${scheme}:${document.uri.toString()}`); - - const doc = await workspace.openTextDocument(uri); - languages.setTextDocumentLanguage(doc, "plaintext"); - - await window.showTextDocument(doc, ViewColumn.Beside, true); - } + await stopLanguageServer(); + await startLanguageServer(); } } - - - -// # const paren = { before: "₍", after: "₎" }; - -// # def to_vscode -// # { before: before, after: after }.flat_map do |type, positions| -// # positions.map do |position, count| - -// # end -// # end - -// # left.map do |position, count| - -// # end -// # end - -// # # range: new Range( -// # # editor.document.positionAt(pos), -// # # editor.document.positionAt(pos) -// # # ), -// # # renderOptions: { -// # # [beforeAfter]: { contentText: repeat(paren[beforeAfter], count) }, -// # # }, \ No newline at end of file diff --git a/vscode/src/startLanguageClient.ts b/vscode/src/startLanguageClient.ts new file mode 100644 index 00000000..b1793d38 --- /dev/null +++ b/vscode/src/startLanguageClient.ts @@ -0,0 +1,57 @@ +import { workspace, OutputChannel } from "vscode"; +import { LanguageClient } from "vscode-languageclient/node"; +import { promisify } from "util"; +import { exec } from "child_process"; + +const promiseExec = promisify(exec); + +async function startLocalClient(outputChannel: OutputChannel) { + const rootFolder = workspace.workspaceFolders![0]; + const cwd = rootFolder.uri.fsPath; + + try { + await promiseExec("bundle show syntax_tree", { cwd }); + } catch { + outputChannel.appendLine(""); + outputChannel.appendLine("Error: Cannot find `syntax_tree` in Gemfile."); + outputChannel.appendLine("Please add `syntax_tree` in your Gemfile and `bundle install`."); + outputChannel.appendLine(""); + return null; + } + + // In this case we're running inside a project, so we're going to attempt + // to run stree with bundler. + const run = { + command: "bundle", + args: ["exec", "stree", "lsp"], + options: { cwd } + }; + + return new LanguageClient("Syntax Tree", { run, debug: run }, { + documentSelector: [ + { scheme: "file", language: "ruby" }, + ], + outputChannel + }); +} + +function startGlobalClient(outputChannel: OutputChannel) { + const run = { command: "stree", args: ["lsp"] } + + return new LanguageClient("Syntax Tree", { run, debug: run }, { + documentSelector: [ + { scheme: "file", language: "ruby" }, + ], + outputChannel + }); +} + +async function startLanguageClient(outputChannel: OutputChannel) { + if (workspace.workspaceFolders) { + return await startLocalClient(outputChannel); + } else { + return startGlobalClient(outputChannel); + } +} + +export default startLanguageClient; From 22c0d0ab67936c0d3be49c39e8ecffd4e4223dfe Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Mon, 31 Jan 2022 08:42:26 -0500 Subject: [PATCH 05/25] Format again with syntax tree --- lib/syntax_tree.rb | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/lib/syntax_tree.rb b/lib/syntax_tree.rb index 6bcf710f..c17adfdb 100644 --- a/lib/syntax_tree.rb +++ b/lib/syntax_tree.rb @@ -1826,12 +1826,7 @@ def child_nodes end def deconstruct_keys(keys) - { - target: target, - value: value, - location: location, - comments: comments - } + { target: target, value: value, location: location, comments: comments } end def format(q) @@ -1927,12 +1922,7 @@ def child_nodes end def deconstruct_keys(keys) - { - key: key, - value: value, - location: location, - comments: comments - } + { key: key, value: value, location: location, comments: comments } end def format(q) @@ -2439,7 +2429,7 @@ def on_begin(bodystmt) keyword = find_token(Kw, "begin") end_char = if bodystmt.rescue_clause || bodystmt.ensure_clause || - bodystmt.else_clause + bodystmt.else_clause bodystmt.location.end_char else find_token(Kw, "end").location.end_char @@ -2558,8 +2548,7 @@ def on_binary(left, operator, right) tokens.rindex do |token| location = token.location - token.is_a?(Op) && - token.value == operator.to_s && + token.is_a?(Op) && token.value == operator.to_s && location.start_char > left.location.end_char && location.end_char < right.location.start_char end @@ -10464,7 +10453,7 @@ def deconstruct_keys(keys) statements: statements, consequent: consequent, location: location, - comments: comments + comments: comments } end @@ -13140,8 +13129,12 @@ def pretty_print(q) end def to_json(*opts) - { type: :pinned_var_ref, value: value, loc: location, cmts: comments } - .to_json(*opts) + { + type: :pinned_var_ref, + value: value, + loc: location, + cmts: comments + }.to_json(*opts) end end From df0c1157fd5f6cdb69d91dded4026065cd909638 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Mon, 31 Jan 2022 09:09:33 -0500 Subject: [PATCH 06/25] Add pattern matching to all nodes --- lib/syntax_tree.rb | 1092 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1092 insertions(+) diff --git a/lib/syntax_tree.rb b/lib/syntax_tree.rb index c17adfdb..621e90f3 100644 --- a/lib/syntax_tree.rb +++ b/lib/syntax_tree.rb @@ -439,6 +439,17 @@ def child_nodes [lbrace, statements] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + lbrace: lbrace, + statements: statements, + location: location, + comments: comments + } + end + def format(q) q.group do q.text("BEGIN ") @@ -520,6 +531,12 @@ def child_nodes [] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + def format(q) if value.length != 2 q.text(value) @@ -590,6 +607,17 @@ def child_nodes [lbrace, statements] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + lbrace: lbrace, + statements: statements, + location: location, + comments: comments + } + end + def format(q) q.group do q.text("END ") @@ -674,6 +702,12 @@ def child_nodes [] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + def format(q) q.text("__END__") q.breakable(force: true) @@ -770,6 +804,12 @@ def child_nodes [left, right] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { left: left, right: right, location: location, comments: comments } + end + def format(q) keyword = "alias " left_argument = AliasArgumentFormatter.new(left) @@ -863,6 +903,17 @@ def child_nodes [collection, index] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + collection: collection, + index: index, + location: location, + comments: comments + } + end + def format(q) q.group do q.format(collection) @@ -949,6 +1000,17 @@ def child_nodes [collection, index] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + collection: collection, + index: index, + location: location, + comments: comments + } + end + def format(q) q.group do q.format(collection) @@ -1043,6 +1105,12 @@ def child_nodes [arguments] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { arguments: arguments, location: location, comments: comments } + end + def format(q) unless arguments q.text("()") @@ -1128,6 +1196,12 @@ def child_nodes parts end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { parts: parts, location: location, comments: comments } + end + def format(q) q.seplist(parts) { |part| q.format(part) } end @@ -1192,6 +1266,12 @@ def child_nodes [value] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + def format(q) q.text("&") q.format(value) if value @@ -1278,6 +1358,12 @@ def child_nodes [value] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + def format(q) q.text("*") q.format(value) if value @@ -1360,6 +1446,12 @@ def child_nodes [] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + def format(q) q.text(value) end @@ -1500,6 +1592,17 @@ def child_nodes [lbracket, contents] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + lbracket: lbracket, + contents: contents, + location: location, + comments: comments + } + end + def format(q) if qwords? QWordsFormatter.new(contents).format(q) @@ -1689,6 +1792,19 @@ def child_nodes [constant, *requireds, rest, *posts] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + constant: constant, + requireds: requireds, + rest: rest, + posts: posts, + location: location, + comments: comments + } + end + def format(q) parts = [*requireds] parts << RestFormatter.new(rest) if rest @@ -1825,6 +1941,8 @@ def child_nodes [target, value] end + alias deconstruct child_nodes + def deconstruct_keys(keys) { target: target, value: value, location: location, comments: comments } end @@ -1921,6 +2039,8 @@ def child_nodes [key, value] end + alias deconstruct child_nodes + def deconstruct_keys(keys) { key: key, value: value, location: location, comments: comments } end @@ -2011,6 +2131,12 @@ def child_nodes [value] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + def format(q) q.text("**") q.format(value) @@ -2074,6 +2200,12 @@ def child_nodes [] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + def format(q) q.text(value) end @@ -2128,6 +2260,12 @@ def child_nodes [] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + def format(q) q.text(value) end @@ -2252,6 +2390,12 @@ def child_nodes assocs end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { assocs: assocs, location: location, comments: comments } + end + def format(q) q.seplist(assocs) { |assoc| q.format(assoc) } end @@ -2316,6 +2460,12 @@ def child_nodes [bodystmt] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { bodystmt: bodystmt, location: location, comments: comments } + end + def format(q) q.text("begin") @@ -2377,6 +2527,12 @@ def child_nodes [statement] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { statement: statement, location: location, comments: comments } + end + def format(q) q.group do q.text("^(") @@ -2480,6 +2636,8 @@ def child_nodes [left, right] end + alias deconstruct child_nodes + def deconstruct_keys(keys) { left: left, @@ -2648,6 +2806,12 @@ def child_nodes [params, *locals] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { params: params, locals: locals, location: location, comments: comments } + end + def format(q) q.group(0, "|", "|") do doc = q.format(params) @@ -2730,6 +2894,12 @@ def child_nodes [name] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { name: name, location: location, comments: comments } + end + def format(q) q.text("&") q.format(name) if name @@ -2839,6 +3009,19 @@ def child_nodes [statements, rescue_clause, else_clause, ensure_clause] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + statements: statements, + rescue_clause: rescue_clause, + else_clause: else_clause, + ensure_clause: ensure_clause, + location: location, + comments: comments + } + end + def format(q) q.group do q.format(statements) unless statements.empty? @@ -3115,6 +3298,18 @@ def child_nodes [lbrace, block_var, statements] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + lbrace: lbrace, + block_var: block_var, + statements: statements, + location: location, + comments: comments + } + end + def format(q) BlockFormatter.new(self, lbrace, "}", statements).format(q) end @@ -3256,6 +3451,12 @@ def child_nodes [arguments] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { arguments: arguments, location: location, comments: comments } + end + def format(q) FlowControlFormatter.new("break", self).format(q) end @@ -3360,6 +3561,19 @@ def child_nodes ] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + receiver: receiver, + operator: operator, + message: message, + arguments: arguments, + location: location, + comments: comments + } + end + def format(q) call_operator = CallOperatorFormatter.new(operator) @@ -3481,6 +3695,18 @@ def child_nodes [keyword, value, consequent] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + keyword: keyword, + value: value, + consequent: consequent, + location: location, + comments: comments + } + end + def format(q) q.group do q.format(keyword) @@ -3562,6 +3788,18 @@ def child_nodes [value, operator, pattern] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + value: value, + operator: operator, + pattern: pattern, + location: location, + comments: comments + } + end + def format(q) q.group do q.format(value) @@ -3690,6 +3928,18 @@ def child_nodes [constant, superclass, bodystmt] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + constant: constant, + superclass: superclass, + bodystmt: bodystmt, + location: location, + comments: comments + } + end + def format(q) declaration = -> do q.group do @@ -3835,6 +4085,17 @@ def child_nodes [message, arguments] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + message: message, + arguments: arguments, + location: location, + comments: comments + } + end + def format(q) q.group do q.format(message) @@ -3922,6 +4183,19 @@ def child_nodes [receiver, message, arguments] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + receiver: receiver, + operator: operator, + message: message, + arguments: arguments, + location: location, + comments: comments + } + end + def format(q) q.group do doc = @@ -4110,6 +4384,12 @@ def child_nodes [] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, inline: inline, location: location } + end + def format(q) q.text(value) end @@ -4183,6 +4463,12 @@ def child_nodes [] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + def format(q) q.text(value) end @@ -4244,6 +4530,17 @@ def child_nodes [parent, constant] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + parent: parent, + constant: constant, + location: location, + comments: comments + } + end + def format(q) q.format(parent) q.text("::") @@ -4313,6 +4610,17 @@ def child_nodes [parent, constant] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + parent: parent, + constant: constant, + location: location, + comments: comments + } + end + def format(q) q.format(parent) q.text("::") @@ -4380,6 +4688,12 @@ def child_nodes [constant] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { constant: constant, location: location, comments: comments } + end + def format(q) q.format(constant) end @@ -4435,6 +4749,12 @@ def child_nodes [] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + def format(q) q.text(value) end @@ -4498,6 +4818,18 @@ def child_nodes [name, params, bodystmt] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + name: name, + params: params, + bodystmt: bodystmt, + location: location, + comments: comments + } + end + def format(q) q.group do q.group do @@ -4595,6 +4927,20 @@ def child_nodes [target, operator, name, paren, statement] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + target: target, + operator: operator, + name: name, + paren: paren, + statement: statement, + location: location, + comments: comments + } + end + def format(q) q.group do q.text("def ") @@ -4747,6 +5093,12 @@ def child_nodes [value] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + def format(q) q.group(0, "defined?(", ")") do q.indent do @@ -4838,6 +5190,20 @@ def child_nodes [target, operator, name, params, bodystmt] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + target: target, + operator: operator, + name: name, + params: params, + bodystmt: bodystmt, + location: location, + comments: comments + } + end + def format(q) q.group do q.group do @@ -4994,6 +5360,18 @@ def child_nodes [keyword, block_var, bodystmt] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + keyword: keyword, + block_var: block_var, + bodystmt: bodystmt, + location: location, + comments: comments + } + end + def format(q) BlockFormatter.new(self, keyword, "end", bodystmt).format(q) end @@ -5107,6 +5485,12 @@ def child_nodes [left, right] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { left: left, right: right, location: location, comments: comments } + end + def format(q) DotFormatter.new("..", self).format(q) end @@ -5191,6 +5575,12 @@ def child_nodes [left, right] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { left: left, right: right, location: location, comments: comments } + end + def format(q) DotFormatter.new("...", self).format(q) end @@ -5314,6 +5704,12 @@ def child_nodes parts end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { parts: parts, quote: quote, location: location, comments: comments } + end + def format(q) opening_quote, closing_quote = quotes(q) @@ -5445,6 +5841,12 @@ def child_nodes [statements] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { statements: statements, location: location, comments: comments } + end + def format(q) q.group do q.text("else") @@ -5541,6 +5943,18 @@ def child_nodes [predicate, statements, consequent] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + predicate: predicate, + statements: statements, + consequent: consequent, + location: location, + comments: comments + } + end + def format(q) q.group do q.group do @@ -5650,6 +6064,12 @@ def child_nodes [] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location } + end + def format(q) q.trim q.text(value) @@ -5837,6 +6257,17 @@ def child_nodes [keyword, statements] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + keyword: keyword, + statements: statements, + location: location, + comments: comments + } + end + def format(q) q.format(keyword) @@ -5920,6 +6351,12 @@ def child_nodes [] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + def format(q) q.text(value) end @@ -5987,6 +6424,17 @@ def child_nodes [value, arguments] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + value: value, + arguments: arguments, + location: location, + comments: comments + } + end + def format(q) q.format(value) q.format(arguments) @@ -6058,6 +6506,18 @@ def child_nodes [parent, (operator if operator != :"::"), name] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + parent: parent, + operator: operator, + name: name, + location: location, + comments: comments + } + end + def format(q) q.group do q.format(parent) @@ -6134,6 +6594,12 @@ def child_nodes [] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + def format(q) q.text(value) end @@ -6205,6 +6671,19 @@ def child_nodes [constant, left, *values, right] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + constant: constant, + left: left, + values: values, + right: right, + location: location, + comments: comments + } + end + def format(q) q.format(constant) if constant q.group(0, "[", "]") do @@ -6309,6 +6788,18 @@ def child_nodes [index, collection, statements] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + index: index, + collection: collection, + statements: statements, + location: location, + comments: comments + } + end + def format(q) q.group do q.text("for ") @@ -6418,6 +6909,12 @@ def child_nodes [] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + def format(q) q.text(value) end @@ -6477,6 +6974,12 @@ def child_nodes [lbrace] + assocs end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { lbrace: lbrace, assocs: assocs, location: location, comments: comments } + end + def format(q) if q.parent.is_a?(Assoc) format_contents(q) @@ -6575,6 +7078,18 @@ def child_nodes [beginning, *parts] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + beginning: beginning, + location: location, + ending: ending, + parts: parts, + comments: comments + } + end + def format(q) # This is a very specific behavior that should probably be included in the # prettyprint module. It's when you want to force a newline, but don't @@ -6656,6 +7171,12 @@ def child_nodes [] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + def format(q) q.text(value) end @@ -6807,6 +7328,18 @@ def child_nodes [constant, *keywords.flatten(1), keyword_rest] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + constant: constant, + keywords: keywords, + keyword_rest: keyword_rest, + location: location, + comments: comments + } + end + def format(q) parts = keywords.map { |(key, value)| KeywordFormatter.new(key, value) } parts << KeywordRestFormatter.new(keyword_rest) if keyword_rest @@ -6922,6 +7455,12 @@ def child_nodes [] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + def format(q) q.text(value) end @@ -7073,6 +7612,18 @@ def child_nodes [predicate, statements, consequent] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + predicate: predicate, + statements: statements, + consequent: consequent, + location: location, + comments: comments + } + end + def format(q) ConditionalFormatter.new("if", self).format(q) end @@ -7160,6 +7711,8 @@ def child_nodes [predicate, truthy, falsy] end + alias deconstruct child_nodes + def deconstruct_keys(keys) { predicate: predicate, @@ -7334,6 +7887,17 @@ def child_nodes [statement, predicate] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + statement: statement, + predicate: predicate, + location: location, + comments: comments + } + end + def format(q) ConditionalModFormatter.new("if", self).format(q) end @@ -7407,6 +7971,12 @@ def child_nodes [] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + def format(q) q.text(value) end @@ -7473,6 +8043,18 @@ def child_nodes [pattern, statements, consequent] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + pattern: pattern, + statements: statements, + consequent: consequent, + location: location, + comments: comments + } + end + def format(q) keyword = "in " @@ -7582,6 +8164,12 @@ def child_nodes [] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + def format(q) if !value.start_with?(/\+?0/) && value.length >= 5 && !value.include?("_") # If it's a plain integer and it doesn't have any underscores separating @@ -7643,6 +8231,12 @@ def child_nodes [] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + def format(q) q.text(value) end @@ -7707,6 +8301,12 @@ def child_nodes [] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + def format(q) q.text(value) end @@ -7765,6 +8365,12 @@ def child_nodes [name] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { name: name, location: location, comments: comments } + end + def format(q) q.text("**") q.format(name) if name @@ -7833,6 +8439,8 @@ def child_nodes [] end + alias deconstruct child_nodes + def deconstruct_keys(keys) { value: value, location: location, comments: comments } end @@ -7930,6 +8538,17 @@ def child_nodes [params, statements] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + params: params, + statements: statements, + location: location, + comments: comments + } + end + def format(q) q.group(0, "->") do if params.is_a?(Paren) @@ -8039,6 +8658,12 @@ def child_nodes [] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + def format(q) q.text(value) end @@ -8095,6 +8720,12 @@ def child_nodes [] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + def format(q) q.text(value) end @@ -8151,6 +8782,12 @@ def child_nodes [] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + def format(q) q.text(value) end @@ -8228,6 +8865,12 @@ def child_nodes [target, value] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { target: target, value: value, location: location, comments: comments } + end + def format(q) q.group do q.group { q.format(target) } @@ -8327,6 +8970,12 @@ def child_nodes [call, block] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { call: call, block: block, location: location, comments: comments } + end + def format(q) q.format(call) q.format(block) @@ -8402,6 +9051,12 @@ def child_nodes parts end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { parts: parts, location: location, comma: comma, comments: comments } + end + def format(q) q.seplist(parts) { |part| q.format(part) } q.text(",") if comma @@ -8497,6 +9152,12 @@ def child_nodes [contents] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { contents: contents, location: location, comments: comments } + end + def format(q) parent = q.parent @@ -8579,6 +9240,17 @@ def child_nodes [constant, bodystmt] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + constant: constant, + bodystmt: bodystmt, + location: location, + comments: comments + } + end + def format(q) declaration = -> do q.group do @@ -8679,6 +9351,12 @@ def child_nodes parts end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { parts: parts, location: location, comments: comments } + end + def format(q) q.seplist(parts) { |part| q.format(part) } end @@ -8785,6 +9463,12 @@ def child_nodes [arguments] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { arguments: arguments, location: location, comments: comments } + end + def format(q) FlowControlFormatter.new("next", self).format(q) end @@ -8851,6 +9535,12 @@ def child_nodes [] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + def format(q) q.text(value) end @@ -8918,6 +9608,18 @@ def child_nodes [target, operator, value] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + target: target, + operator: operator, + value: value, + location: location, + comments: comments + } + end + def format(q) q.group do q.format(target) @@ -9182,6 +9884,22 @@ def child_nodes ] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + location: location, + requireds: requireds, + optionals: optionals, + rest: rest, + posts: posts, + keywords: keywords, + keyword_rest: keyword_rest, + block: block, + comments: comments + } + end + def format(q) parts = [ *requireds, @@ -9374,6 +10092,17 @@ def child_nodes [lparen, contents] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + lparen: lparen, + contents: contents, + location: location, + comments: comments + } + end + def format(q) q.group do q.format(lparen) @@ -9480,6 +10209,12 @@ def child_nodes [] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + def format(q) q.text(value) end @@ -9532,6 +10267,12 @@ def child_nodes [statements] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { statements: statements, location: location, comments: comments } + end + def format(q) q.format(statements) @@ -9700,6 +10441,17 @@ def child_nodes [] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + beginning: beginning, + elements: elements, + location: location, + comments: comments + } + end + def format(q) opening, closing = "%i[", "]" @@ -9823,6 +10575,17 @@ def child_nodes [] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + beginning: beginning, + elements: elements, + location: location, + comments: comments + } + end + def format(q) opening, closing = "%w[", "]" @@ -9935,6 +10698,12 @@ def child_nodes [] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + def format(q) q.text(value) end @@ -10044,6 +10813,12 @@ def child_nodes [] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + def format(q) q.text(value) end @@ -10211,6 +10986,18 @@ def child_nodes parts end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + beginning: beginning, + ending: ending, + parts: parts, + location: location, + comments: comments + } + end + def format(q) braces = ambiguous?(q) || include?(%r{\/}) @@ -10351,6 +11138,17 @@ def child_nodes [*exceptions, variable] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + exceptions: exceptions, + variable: variable, + location: location, + comments: comments + } + end + def format(q) q.group do if exceptions @@ -10447,6 +11245,8 @@ def child_nodes [exception, statements, consequent] end + alias deconstruct child_nodes + def deconstruct_keys(keys) { exception: exception, @@ -10591,6 +11391,17 @@ def child_nodes [statement, value] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + statement: statement, + value: value, + location: location, + comments: comments + } + end + def format(q) q.group(0, "begin", "end") do q.indent do @@ -10669,6 +11480,12 @@ def child_nodes [name] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { name: name, location: location, comments: comments } + end + def format(q) q.text("*") q.format(name) if name @@ -10725,6 +11542,12 @@ def child_nodes [] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + def format(q) q.text(value) end @@ -10779,6 +11602,12 @@ def child_nodes [arguments] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { arguments: arguments, location: location, comments: comments } + end + def format(q) FlowControlFormatter.new("return", self).format(q) end @@ -10836,6 +11665,12 @@ def child_nodes [] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + def format(q) q.text(value) end @@ -10924,6 +11759,17 @@ def child_nodes [target, bodystmt] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + target: target, + bodystmt: bodystmt, + location: location, + comments: comments + } + end + def format(q) q.group(0, "class << ", "end") do q.format(target) @@ -11058,6 +11904,12 @@ def child_nodes body end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { parser: parser, body: body, location: location, comments: comments } + end + def format(q) line = nil @@ -11246,6 +12098,12 @@ def child_nodes [left, right] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { left: left, right: right, location: location, comments: comments } + end + def format(q) q.group do q.format(left) @@ -11330,6 +12188,12 @@ def child_nodes [variable] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { variable: variable, location: location, comments: comments } + end + def format(q) q.text('#{') q.format(variable) @@ -11394,6 +12258,12 @@ def child_nodes [statements] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { statements: statements, location: location, comments: comments } + end + def format(q) if location.start_line == location.end_line # If the contents of this embedded expression were originally on the @@ -11490,6 +12360,12 @@ def child_nodes parts end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { parts: parts, quote: quote, location: location, comments: comments } + end + def format(q) if parts.empty? q.text("#{q.quote}#{q.quote}") @@ -11604,6 +12480,12 @@ def child_nodes [arguments] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { arguments: arguments, location: location, comments: comments } + end + def format(q) q.group do q.text("super") @@ -11744,6 +12626,12 @@ def child_nodes [value] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + def format(q) q.text(":") q.format(value) @@ -11819,6 +12707,17 @@ def child_nodes [] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + beginning: beginning, + elements: elements, + location: location, + comments: comments + } + end + def format(q) opening, closing = "%I[", "]" @@ -12004,6 +12903,12 @@ def child_nodes [constant] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { constant: constant, location: location, comments: comments } + end + def format(q) q.text("::") q.format(constant) @@ -12066,6 +12971,12 @@ def child_nodes [constant] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { constant: constant, location: location, comments: comments } + end + def format(q) q.text("::") q.format(constant) @@ -12171,6 +13082,12 @@ def child_nodes [] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + def format(q) q.text(value) end @@ -12269,6 +13186,17 @@ def child_nodes [statement] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + statement: statement, + parentheses: parentheses, + location: location, + comments: comments + } + end + def format(q) q.text(parentheses ? "not(" : "not ") q.format(statement) @@ -12326,6 +13254,8 @@ def child_nodes [statement] end + alias deconstruct child_nodes + def deconstruct_keys(keys) { operator: operator, @@ -12456,6 +13386,12 @@ def child_nodes symbols end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { symbols: symbols, location: location, comments: comments } + end + def format(q) keyword = "undef " formatters = symbols.map { |symbol| UndefArgumentFormatter.new(symbol) } @@ -12536,6 +13472,18 @@ def child_nodes [predicate, statements, consequent] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + predicate: predicate, + statements: statements, + consequent: consequent, + location: location, + comments: comments + } + end + def format(q) ConditionalFormatter.new("unless", self).format(q) end @@ -12619,6 +13567,17 @@ def child_nodes [statement, predicate] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + statement: statement, + predicate: predicate, + location: location, + comments: comments + } + end + def format(q) ConditionalModFormatter.new("unless", self).format(q) end @@ -12738,6 +13697,17 @@ def child_nodes [predicate, statements] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + predicate: predicate, + statements: statements, + location: location, + comments: comments + } + end + def format(q) if statements.empty? keyword = "until " @@ -12830,6 +13800,17 @@ def child_nodes [statement, predicate] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + statement: statement, + predicate: predicate, + location: location, + comments: comments + } + end + def format(q) # If we're in the modifier form and we're modifying a `begin`, then this # is a special case where we need to explicitly use the modifier form @@ -12920,6 +13901,12 @@ def child_nodes [left, right] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { left: left, right: right, location: location, comments: comments } + end + def format(q) keyword = "alias " @@ -12992,6 +13979,12 @@ def child_nodes [value] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + def format(q) q.format(value) if value end @@ -13059,6 +14052,12 @@ def child_nodes [value] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + def format(q) q.format(value) end @@ -13110,6 +14109,12 @@ def child_nodes [value] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + def format(q) q.group do q.text("^") @@ -13176,6 +14181,12 @@ def child_nodes [value] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + def format(q) q.format(value) end @@ -13224,6 +14235,12 @@ def child_nodes [] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { location: location, comments: comments } + end + def format(q) end @@ -13285,6 +14302,18 @@ def child_nodes [arguments, statements, consequent] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + arguments: arguments, + statements: statements, + consequent: consequent, + location: location, + comments: comments + } + end + def format(q) keyword = "when " @@ -13412,6 +14441,17 @@ def child_nodes [predicate, statements] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + predicate: predicate, + statements: statements, + location: location, + comments: comments + } + end + def format(q) if statements.empty? keyword = "while " @@ -13504,6 +14544,17 @@ def child_nodes [statement, predicate] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + statement: statement, + predicate: predicate, + location: location, + comments: comments + } + end + def format(q) # If we're in the modifier form and we're modifying a `begin`, then this # is a special case where we need to explicitly use the modifier form @@ -13597,6 +14648,12 @@ def child_nodes parts end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { parts: parts, location: location, comments: comments } + end + def format(q) q.format_each(parts) end @@ -13665,6 +14722,17 @@ def child_nodes [] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + beginning: beginning, + elements: elements, + location: location, + comments: comments + } + end + def format(q) opening, closing = "%W[", "]" @@ -13828,6 +14896,12 @@ def child_nodes parts end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { parts: parts, location: location, comments: comments } + end + def format(q) q.text("`") q.format_each(parts) @@ -13901,6 +14975,12 @@ def child_nodes [arguments] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { arguments: arguments, location: location, comments: comments } + end + def format(q) q.group do q.text("yield") @@ -13972,6 +15052,12 @@ def child_nodes [] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + def format(q) q.text(value) end @@ -14026,6 +15112,12 @@ def child_nodes [] end + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + def format(q) q.text(value) end From 0af5e70a582d472fa116a5bb51ded86c0fe577c8 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Tue, 1 Feb 2022 13:36:45 -0500 Subject: [PATCH 07/25] Add a logo --- .gitignore | 1 - README.md | 4 + doc/logo.svg | 284 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 288 insertions(+), 1 deletion(-) create mode 100644 doc/logo.svg diff --git a/.gitignore b/.gitignore index 76aa4f36..4d0d86f3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ /.yardoc /_yardoc/ /coverage/ -/doc/ /pkg/ /spec/reports/ /tmp/ diff --git a/README.md b/README.md index 0103db27..52acbfcf 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +
+ Syntax Tree +
+ # SyntaxTree [![Build Status](https://github.com/kddnewton/syntax_tree/workflows/Main/badge.svg)](https://github.com/kddnewton/syntax_tree/actions) diff --git a/doc/logo.svg b/doc/logo.svg new file mode 100644 index 00000000..21c4209c --- /dev/null +++ b/doc/logo.svg @@ -0,0 +1,284 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + yntax + ree + + From 6d0b3181267680104e6c3d35bbd0a8831548351c Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Tue, 8 Feb 2022 08:30:49 -0500 Subject: [PATCH 08/25] Disassemble to YARV --- lib/syntax_tree.rb | 8 ++- lib/syntax_tree/code_actions.rb | 89 ++++++++++++++++++++++++++++++ lib/syntax_tree/language_server.rb | 47 +++++++++++++++- vscode/package.json | 7 ++- vscode/src/Disasm.ts | 55 ++++++++++++++++++ vscode/src/Visualize.ts | 4 +- vscode/src/extension.ts | 2 + 7 files changed, 205 insertions(+), 7 deletions(-) create mode 100644 lib/syntax_tree/code_actions.rb create mode 100644 vscode/src/Disasm.ts diff --git a/lib/syntax_tree.rb b/lib/syntax_tree.rb index 621e90f3..f1014b2f 100644 --- a/lib/syntax_tree.rb +++ b/lib/syntax_tree.rb @@ -72,6 +72,10 @@ def initialize(start_line:, start_char:, end_line:, end_char:) @end_char = end_char end + def lines + start_line..end_line + end + def ==(other) other.is_a?(Location) && start_line == other.start_line && start_char == other.start_char && end_line == other.end_line && @@ -3098,12 +3102,14 @@ def to_json(*opts) # (nil | Ensure) ensure_clause # ) -> BodyStmt def on_bodystmt(statements, rescue_clause, else_clause, ensure_clause) + ending = [ensure_clause, else_clause, rescue_clause, statements].detect(&:itself) + BodyStmt.new( statements: statements, rescue_clause: rescue_clause, else_clause: else_clause, ensure_clause: ensure_clause, - location: Location.fixed(line: lineno, char: char_pos) + location: statements.location.to(ending.location) ) end diff --git a/lib/syntax_tree/code_actions.rb b/lib/syntax_tree/code_actions.rb new file mode 100644 index 00000000..b8f69c42 --- /dev/null +++ b/lib/syntax_tree/code_actions.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +class SyntaxTree + class CodeActions + class DisasmAction + attr_reader :line, :node + + def initialize(line, node) + @line = line + @node = node + end + + def as_json + { + title: "Disasm #{node.name.value}", + command: "syntaxTree.disasm", + arguments: [line, node.name.value] + } + end + end + + attr_reader :line, :actions + + def initialize(line) + @line = line + @actions = [] + end + + def disasm(node) + @actions << DisasmAction.new(line, node) + end + + def self.find(program, line) + code_actions = new(line) + queue = [program] + + until queue.empty? + node = queue.shift + + # If we're found a method definition that starts on the given line, then + # we're going to add the disasm code action for that node. + if [Def, DefEndless, Defs].include?(node.class) && node.location.start_line == line + code_actions.disasm(node) + end + + # Check if the node covers the given line. If it doesn't, then we just + # bail out and go to the next node in the queue. + next unless node.location.lines.cover?(line) + + # Get a list of child nodes and binary search over them to find the + # first child node that covers the given line. It's possible that there + # are multiple, but at least we'll have a handle on one of them. + child_nodes = node.child_nodes.compact + index = + child_nodes.bsearch_index do |child| + if child.location.lines.cover?(line) + 0 + else + line - child.location.start_line + end + end + + # If no valid child was found, then just continue on to the next node in + # the queue. + next unless index + + # First, we're going to go backward in our list until we find the start + # of the subset of child nodes that cover the given line. We're going to + # add each one to the queue. + current = index - 1 + while current > 0 && child_nodes[current].location.lines.cover?(line) + queue << child_nodes[current] + current -= 1 + end + + # Next, we're going to go forward in our list until we find the end of + # the subset of child nodes that cover the given line. We'll add each of + # those to the queue as well. + current = index + while current < child_nodes.length && child_nodes[current].location.lines.cover?(line) + queue << child_nodes[current] + current += 1 + end + end + + code_actions + end + end +end diff --git a/lib/syntax_tree/language_server.rb b/lib/syntax_tree/language_server.rb index 54abf5c8..b9dd4bcb 100644 --- a/lib/syntax_tree/language_server.rb +++ b/lib/syntax_tree/language_server.rb @@ -4,6 +4,7 @@ require "json" require "uri" +require_relative "code_actions" require_relative "implicits" class SyntaxTree @@ -22,8 +23,8 @@ def run end while headers = input.gets("\r\n\r\n") - length = input.read(headers[/Content-Length: (\d+)/i, 1].to_i) - request = JSON.parse(length, symbolize_names: true) + source = input.read(headers[/Content-Length: (\d+)/i, 1].to_i) + request = JSON.parse(source, symbolize_names: true) case request in { method: "initialize", id: } @@ -34,6 +35,8 @@ def run in { method: "shutdown" } store.clear return + in { method: "textDocument/codeAction", id:, params: { textDocument: { uri: }, range: { start: { line: } } } } + write(id: id, result: code_actions(store[uri], line + 1)) in { method: "textDocument/didChange", params: { textDocument: { uri: }, contentChanges: [{ text: }, *] } } store[uri] = text in { method: "textDocument/didOpen", params: { textDocument: { uri:, text: } } } @@ -42,6 +45,8 @@ def run store.delete(uri) in { method: "textDocument/formatting", id:, params: { textDocument: { uri: } } } write(id: id, result: [format(store[uri])]) + in { method: "syntaxTree/disasm", id:, params: { textDocument: { uri:, query: { line:, name: } } } } + write(id: id, result: disasm(store[uri], line.to_i, name)) in { method: "syntaxTree/implicits", id:, params: { textDocument: { uri: } } } write(id: id, result: implicits(store[uri])) in { method: "syntaxTree/visualizing", id:, params: { textDocument: { uri: } } } @@ -60,11 +65,43 @@ def run def capabilities { + codeActionProvider: { codeActionsKinds: ["disasm"] }, documentFormattingProvider: true, - textDocumentSync: { change: 1, openClose: true }, + textDocumentSync: { change: 1, openClose: true } } end + def code_actions(source, line) + actions = CodeActions.find(SyntaxTree.parse(source), line).actions + log("Found #{actions.length} actions on line #{line}") + + actions.map(&:as_json) + end + + def disasm(source, line, name) + actions = CodeActions.find(SyntaxTree.parse(source), line).actions + log("Disassembling #{name.inspect} on line #{line.inspect}") + + matched = actions.detect { |action| action.is_a?(CodeActions::DisasmAction) && action.node.name.value == name } + return "Unable to find method: #{name}" unless matched + + # First, get an instruction sequence that encompasses the method that + # we're going to disassemble. It will include the method declaration, + # which will be the top instruction sequence. + location = matched.node.location + iseq = RubyVM::InstructionSequence.new(source[location.start_char...location.end_char]) + + # Next, get the first child. We do this because the parent instruction + # sequence is the method declaration, whereas the first child is the body + # of the method, which is what we're interested in. + method = nil + iseq.each_child { |child| method = child } + + # Finally, return the disassembly as a string to the server, which will + # serialize it to JSON and return it back to the client. + method.disasm + end + def format(source) { range: { @@ -75,6 +112,10 @@ def format(source) } end + def log(message) + write(method: "window/logMessage", params: { type: 4, message: message }) + end + def implicits(source) implicits = Implicits.find(SyntaxTree.parse(source)) serialize = ->(position, text) { { position: position, text: text } } diff --git a/vscode/package.json b/vscode/package.json index a82c0857..87323ec7 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -22,7 +22,8 @@ "onCommand:syntaxTree.stop", "onCommand:syntaxTree.restart", "onCommand:syntaxTree.showOutputChannel", - "onCommand:syntaxTree.visualize" + "onCommand:syntaxTree.visualize", + "onCommand:syntaxTree.disasm" ], "main": "./out/extension", "contributes": { @@ -46,6 +47,10 @@ { "command": "syntaxTree.visualize", "title": "Syntax Tree: Visualize" + }, + { + "command": "syntaxTree.disasm", + "title": "Syntax Tree: Disassemble" } ], "colors": [ diff --git a/vscode/src/Disasm.ts b/vscode/src/Disasm.ts new file mode 100644 index 00000000..743eb700 --- /dev/null +++ b/vscode/src/Disasm.ts @@ -0,0 +1,55 @@ +import { commands, Disposable, languages, OutputChannel, ProviderResult, TextDocumentContentProvider, Uri, ViewColumn, window, workspace } from "vscode"; +import { LanguageClient } from "vscode-languageclient/node"; + +class Disasm implements Disposable, TextDocumentContentProvider { + // The client used to communicate with the language server. + private readonly languageClient: LanguageClient; + + // The output channel used for logging for this class. It's given from the + // main file so that it uses the same as the rest of the extension. + private readonly outputChannel: OutputChannel; + + // The list of callbacks and objects that should be disposed when an instance + // of Visualize is being disposed. + private readonly disposables: Disposable[]; + + constructor(languageClient: LanguageClient, outputChannel: OutputChannel) { + this.languageClient = languageClient; + this.outputChannel = outputChannel; + this.disposables = [ + commands.registerCommand("syntaxTree.disasm", this.disasm), + workspace.registerTextDocumentContentProvider("syntaxTree.disasm", this) + ]; + } + + dispose() { + this.disposables.forEach((disposable) => disposable.dispose()); + } + + provideTextDocumentContent(uri: Uri): ProviderResult { + this.outputChannel.appendLine("Requesting disassembly"); + + const query: Record = {}; + uri.query.split("&").forEach((pair) => { + const [key, value] = pair.split("="); + query[key] = value; + }); + + return this.languageClient.sendRequest("syntaxTree/disasm", { textDocument: { uri: uri.path, query } }); + } + + async disasm(line: number, name: string) { + const document = window.activeTextEditor?.document; + + if (document && document.languageId === "ruby" && document.uri.scheme === "file") { + const uri = Uri.parse(`syntaxTree.disasm:${document.uri.toString()}?line=${line}&name=${name}`); + + const doc = await workspace.openTextDocument(uri); + languages.setTextDocumentLanguage(doc, "plaintext"); + + await window.showTextDocument(doc, ViewColumn.Beside, true); + } + } +} + +export default Disasm; diff --git a/vscode/src/Visualize.ts b/vscode/src/Visualize.ts index baf1996e..d57b1ddf 100644 --- a/vscode/src/Visualize.ts +++ b/vscode/src/Visualize.ts @@ -18,7 +18,7 @@ class Visualize implements Disposable, TextDocumentContentProvider { this.outputChannel = outputChannel; this.disposables = [ commands.registerCommand("syntaxTree.visualize", this.visualize), - workspace.registerTextDocumentContentProvider("syntaxTree", this) + workspace.registerTextDocumentContentProvider("syntaxTree.visualize", this) ]; } @@ -35,7 +35,7 @@ class Visualize implements Disposable, TextDocumentContentProvider { const document = window.activeTextEditor?.document; if (document && document.languageId === "ruby" && document.uri.scheme === "file") { - const uri = Uri.parse(`syntaxTree:${document.uri.toString()}`); + const uri = Uri.parse(`syntaxTree.visualize:${document.uri.toString()}`); const doc = await workspace.openTextDocument(uri); languages.setTextDocumentLanguage(doc, "plaintext"); diff --git a/vscode/src/extension.ts b/vscode/src/extension.ts index ef6491cd..c60fb8cf 100644 --- a/vscode/src/extension.ts +++ b/vscode/src/extension.ts @@ -3,6 +3,7 @@ import { ExtensionContext, commands, window } from "vscode"; import { LanguageClient } from "vscode-languageclient/node"; +import Disasm from "./Disasm"; import Implicits from "./Implicits"; import startLanguageClient from "./startLanguageClient"; import Visualize from "./Visualize"; @@ -30,6 +31,7 @@ export function activate(context: ExtensionContext) { await languageClient.onReady(); context.subscriptions.push( + new Disasm(languageClient, outputChannel), new Implicits(languageClient, outputChannel), new Visualize(languageClient, outputChannel) ); From 87aaa63ac37759eabc36f09efe5019baacf317c2 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Fri, 11 Feb 2022 09:58:45 -0500 Subject: [PATCH 09/25] Move VSCode plugin to its own repository --- vscode/.gitignore | 3 - vscode/.vscode/launch.json | 21 ---- vscode/.vscode/settings.json | 11 -- vscode/.vscode/tasks.json | 20 ---- vscode/.vscodeignore | 11 -- vscode/package.json | 83 ------------- vscode/src/Disasm.ts | 55 --------- vscode/src/Implicits.ts | 190 ------------------------------ vscode/src/Visualize.ts | 48 -------- vscode/src/extension.ts | 53 --------- vscode/src/startLanguageClient.ts | 57 --------- vscode/tsconfig.json | 16 --- vscode/yarn.lock | 89 -------------- 13 files changed, 657 deletions(-) delete mode 100644 vscode/.gitignore delete mode 100644 vscode/.vscode/launch.json delete mode 100644 vscode/.vscode/settings.json delete mode 100644 vscode/.vscode/tasks.json delete mode 100644 vscode/.vscodeignore delete mode 100644 vscode/package.json delete mode 100644 vscode/src/Disasm.ts delete mode 100644 vscode/src/Implicits.ts delete mode 100644 vscode/src/Visualize.ts delete mode 100644 vscode/src/extension.ts delete mode 100644 vscode/src/startLanguageClient.ts delete mode 100644 vscode/tsconfig.json delete mode 100644 vscode/yarn.lock diff --git a/vscode/.gitignore b/vscode/.gitignore deleted file mode 100644 index 9adedec5..00000000 --- a/vscode/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -*.vsix -out diff --git a/vscode/.vscode/launch.json b/vscode/.vscode/launch.json deleted file mode 100644 index 42f351ce..00000000 --- a/vscode/.vscode/launch.json +++ /dev/null @@ -1,21 +0,0 @@ -// A launch configuration that compiles the extension and then opens it inside a new window -// Use IntelliSense to learn about possible attributes. -// Hover to view descriptions of existing attributes. -// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 -{ - "version": "0.2.0", - "configurations": [ - { - "name": "Run Extension", - "type": "extensionHost", - "request": "launch", - "args": [ - "--extensionDevelopmentPath=${workspaceFolder}" - ], - "outFiles": [ - "${workspaceFolder}/out/**/*.js" - ], - "preLaunchTask": "${defaultBuildTask}" - }, - ] -} diff --git a/vscode/.vscode/settings.json b/vscode/.vscode/settings.json deleted file mode 100644 index ebfbdcd6..00000000 --- a/vscode/.vscode/settings.json +++ /dev/null @@ -1,11 +0,0 @@ -// Place your settings in this file to overwrite default and user settings. -{ - "files.exclude": { - "out": false // set this to true to hide the "out" folder with the compiled JS files - }, - "search.exclude": { - "out": true // set this to false to include "out" folder in search results - }, - // Turn off tsc task auto detection since we have the necessary tasks as npm scripts - "typescript.tsc.autoDetect": "off" -} diff --git a/vscode/.vscode/tasks.json b/vscode/.vscode/tasks.json deleted file mode 100644 index 3b17e53b..00000000 --- a/vscode/.vscode/tasks.json +++ /dev/null @@ -1,20 +0,0 @@ -// See https://go.microsoft.com/fwlink/?LinkId=733558 -// for the documentation about the tasks.json format -{ - "version": "2.0.0", - "tasks": [ - { - "type": "npm", - "script": "watch", - "problemMatcher": "$tsc-watch", - "isBackground": true, - "presentation": { - "reveal": "never" - }, - "group": { - "kind": "build", - "isDefault": true - } - } - ] -} diff --git a/vscode/.vscodeignore b/vscode/.vscodeignore deleted file mode 100644 index 0a5a2bfe..00000000 --- a/vscode/.vscodeignore +++ /dev/null @@ -1,11 +0,0 @@ -.vscode/** -.vscode-test/** -out/test/** -src/** -.gitignore -.yarnrc -vsc-extension-quickstart.md -**/tsconfig.json -**/.eslintrc.json -**/*.map -**/*.ts diff --git a/vscode/package.json b/vscode/package.json deleted file mode 100644 index 87323ec7..00000000 --- a/vscode/package.json +++ /dev/null @@ -1,83 +0,0 @@ -{ - "name": "syntax_tree", - "displayName": "Syntax Tree", - "description": "VSCode integration for syntax tree", - "version": "0.1.0", - "publisher": "kddnewton", - "repository": { - "type": "git", - "url": "https://github.com/kddnewton/syntax_tree.git" - }, - "license": "MIT", - "bugs": { - "url": "https://github.com/kddnewton/syntax_tree/issues" - }, - "engines": { - "vscode": "^1.46.0" - }, - "activationEvents": [ - "onLanguage:ruby", - "workspaceContains:Gemfile.lock", - "onCommand:syntaxTree.start", - "onCommand:syntaxTree.stop", - "onCommand:syntaxTree.restart", - "onCommand:syntaxTree.showOutputChannel", - "onCommand:syntaxTree.visualize", - "onCommand:syntaxTree.disasm" - ], - "main": "./out/extension", - "contributes": { - "commands": [ - { - "command": "syntaxTree.start", - "title": "Syntax Tree: Start" - }, - { - "command": "syntaxTree.stop", - "title": "Syntax Tree: Stop" - }, - { - "command": "syntaxTree.restart", - "title": "Syntax Tree: Restart" - }, - { - "command": "syntaxTree.showOutputChannel", - "title": "Syntax Tree: Show Output Channel" - }, - { - "command": "syntaxTree.visualize", - "title": "Syntax Tree: Visualize" - }, - { - "command": "syntaxTree.disasm", - "title": "Syntax Tree: Disassemble" - } - ], - "colors": [ - { - "id": "syntaxTree.implicits", - "description": "Text color for the inserted implicits", - "defaults": { - "dark": "#707070", - "light": "#999999", - "highContrast": "foreground" - } - } - ] - }, - "scripts": { - "compile": "tsc -p ./", - "package": "vsce package --yarn --githubBranch main", - "publish": "vsce publish --yarn --githubBranch main", - "vscode:prepublish": "yarn compile", - "watch": "tsc --watch -p ./" - }, - "dependencies": { - "vscode-languageclient": "^7.0.0" - }, - "devDependencies": { - "@types/node": "^17.0.8", - "@types/vscode": "^1.46.0", - "typescript": "^4.2.2" - } -} diff --git a/vscode/src/Disasm.ts b/vscode/src/Disasm.ts deleted file mode 100644 index 743eb700..00000000 --- a/vscode/src/Disasm.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { commands, Disposable, languages, OutputChannel, ProviderResult, TextDocumentContentProvider, Uri, ViewColumn, window, workspace } from "vscode"; -import { LanguageClient } from "vscode-languageclient/node"; - -class Disasm implements Disposable, TextDocumentContentProvider { - // The client used to communicate with the language server. - private readonly languageClient: LanguageClient; - - // The output channel used for logging for this class. It's given from the - // main file so that it uses the same as the rest of the extension. - private readonly outputChannel: OutputChannel; - - // The list of callbacks and objects that should be disposed when an instance - // of Visualize is being disposed. - private readonly disposables: Disposable[]; - - constructor(languageClient: LanguageClient, outputChannel: OutputChannel) { - this.languageClient = languageClient; - this.outputChannel = outputChannel; - this.disposables = [ - commands.registerCommand("syntaxTree.disasm", this.disasm), - workspace.registerTextDocumentContentProvider("syntaxTree.disasm", this) - ]; - } - - dispose() { - this.disposables.forEach((disposable) => disposable.dispose()); - } - - provideTextDocumentContent(uri: Uri): ProviderResult { - this.outputChannel.appendLine("Requesting disassembly"); - - const query: Record = {}; - uri.query.split("&").forEach((pair) => { - const [key, value] = pair.split("="); - query[key] = value; - }); - - return this.languageClient.sendRequest("syntaxTree/disasm", { textDocument: { uri: uri.path, query } }); - } - - async disasm(line: number, name: string) { - const document = window.activeTextEditor?.document; - - if (document && document.languageId === "ruby" && document.uri.scheme === "file") { - const uri = Uri.parse(`syntaxTree.disasm:${document.uri.toString()}?line=${line}&name=${name}`); - - const doc = await workspace.openTextDocument(uri); - languages.setTextDocumentLanguage(doc, "plaintext"); - - await window.showTextDocument(doc, ViewColumn.Beside, true); - } - } -} - -export default Disasm; diff --git a/vscode/src/Implicits.ts b/vscode/src/Implicits.ts deleted file mode 100644 index 52b1b029..00000000 --- a/vscode/src/Implicits.ts +++ /dev/null @@ -1,190 +0,0 @@ -import { DecorationOptions, DecorationRangeBehavior, Disposable, OutputChannel, Range, TextDocument, TextDocumentChangeEvent, TextEditor, TextEditorDecorationType, ThemeColor, window, workspace } from "vscode"; -import { LanguageClient } from "vscode-languageclient/node"; - -type Implicit = { position: number, text: string }; -type ImplicitSet = { before: Implicit[], after: Implicit[] }; - -class Implicits implements Disposable { - // The client used to communicate with the language server. In the case of - // this class it's used to send a syntaxTree/implicits request. - private readonly languageClient: LanguageClient; - - // The output channel used for logging for this class. It's given from the - // main file so that it uses the same as the rest of the extension. - private readonly outputChannel: OutputChannel; - - private readonly decorationType: TextEditorDecorationType; - private readonly implicitsCache: WeakMap; - private readonly debouncedHandleTextDocumentChange: Debounced; - - private readonly disposables: Disposable[]; - - constructor(languageClient: LanguageClient, outputChannel: OutputChannel) { - this.languageClient = languageClient; - this.outputChannel = outputChannel; - - const color = new ThemeColor("syntaxTree.implicits"); - this.decorationType = window.createTextEditorDecorationType({ - before: { color, fontStyle: "normal" }, - after: { color, fontStyle: "normal" }, - rangeBehavior: DecorationRangeBehavior.ClosedClosed - }); - - this.implicitsCache = new WeakMap(); - this.setImplicitsForEditors(window.visibleTextEditors); - - // Here we're going to debounce the handleTextDocumentChange callback so - // that we're not reacting too quickly to user inputs and making it flash - // alll around the editor. - this.debouncedHandleTextDocumentChange = debounce(300, (editor: TextEditor) => { - this.outputChannel.appendLine("Handling text document changes (debounced)"); - this.implicitsCache.delete(editor.document); - this.setImplicitsForEditor(editor); - }); - - // Track all of the various callbacks and objects that implement Disposable - // so that when we need to dispose of the entire Implicits instance we can - // iterate through and dispose all of them. - this.disposables = [ - this.decorationType, - window.onDidChangeVisibleTextEditors(this.setImplicitsForEditors, this), - workspace.onDidChangeTextDocument(this.handleTextDocumentChange, this), - this.debouncedHandleTextDocumentChange - ]; - } - - dispose() { - this.disposables.forEach((disposable) => disposable.dispose()); - } - - handleTextDocumentChange(event: TextDocumentChangeEvent) { - const editor = window.activeTextEditor; - - if (editor !== undefined && event.document === editor.document) { - this.debouncedHandleTextDocumentChange(editor); - } else { - this.implicitsCache.delete(event.document); - } - } - - // Asynchronously get the implicits for a given text document, optionally - // using a cache if it has already been populated. - async getImplicitsForTextDocument(document: TextDocument): Promise { - if (document.languageId !== "ruby") { - // This editor may have previously been a Ruby file, but that has now - // changed. So we should delete the implicits that may be there. - if (this.implicitsCache.has(document)) { - this.implicitsCache.delete(document); - - // Return an empty set of implicits so that it gets properly cleared - // from the document. - return { before: [], after: [] }; - } - - // Otherwise, we're going to return undefined so that we don't bother with - // this file, as it's not a Ruby file. - return undefined; - } - - // Check the cache first to see if we have already computed the implicits - // for this document. Return them if we have them. - let implicits = this.implicitsCache.get(document); - if (implicits) { - this.outputChannel.appendLine("Loading implicits from cache"); - return implicits; - } - - // Otherwise, asynchronously request the implicits from the language server, - // cache the response, and return it. - this.outputChannel.appendLine("Requesting implicits"); - implicits = await this.languageClient.sendRequest("syntaxTree/implicits", { - textDocument: { uri: document.uri.toString() } - }); - - // In case of a syntax error, this is not going to return anything. In that - // case, we don't want to set the cache to anything, but we also don't want - // to clear the previous implicits either. So we're just going to return - // undefined - if (!implicits) { - return undefined; - } - - this.implicitsCache.set(document, implicits); - return implicits; - } - - async setImplicitsForEditor(editor: TextEditor) { - const implicits = await this.getImplicitsForTextDocument(editor.document); - if (!implicits) { - return; - } - - const decorations: DecorationOptions[] = [ - ...implicits.before.map(({ position, text: contentText }) => ({ - range: new Range(editor.document.positionAt(position), editor.document.positionAt(position)), - renderOptions: { before: { contentText } } - })), - ...implicits.after.map(({ position, text: contentText }) => ({ - range: new Range(editor.document.positionAt(position), editor.document.positionAt(position)), - renderOptions: { after: { contentText } } - })) - ]; - - this.outputChannel.appendLine("Settings implicits"); - editor.setDecorations(this.decorationType, decorations); - } - - setImplicitsForEditors(editors: readonly TextEditor[]) { - editors.forEach((editor) => this.setImplicitsForEditor(editor)); - } -} - -// The return value of the debounce function below. -type Debounced = Disposable & ((argument: T) => void); - -// This function will take a given callback and delay it by a given delay. If -// another call to the same function with the same argument comes in before the -// first one finishes, it will cancel the first invocation and start the delay -// again. -function debounce(delay: number, callback: (argument: T) => void): Debounced { - let allTimeouts = new WeakMap(); - const liveTimeouts = new Set(); - - const debounced = (argument: T) => { - // First, clear out the timeout for the previous call to this debounced - // callback. - const previousTimeout = allTimeouts.get(argument); - if (previousTimeout !== undefined) { - clearTimeout(previousTimeout); - liveTimeouts.delete(previousTimeout); - } - - // Next, create a new timeout for this call to the debounced callback. - // If it doesn't get cancelled by a subsequent call, it will be invoked with - // the given argument. - const timeout = setTimeout(() => { - allTimeouts.delete(argument); - liveTimeouts.delete(timeout); - callback(argument); - }, delay); - - // Finally, track the timeout that we just created in both a WeakMap - // that links the editor to the timeout and a set. We track both so that - // we can iterate through the timeouts when VSCode needs to dispose of - // this subscription. - allTimeouts.set(argument, timeout); - liveTimeouts.add(timeout); - }; - - // Define the necessary dispose function so that VSCode knows how to - // trigger disposal of this entire debounced function. - debounced.dispose = () => { - liveTimeouts.forEach((timeout) => clearTimeout(timeout)); - liveTimeouts.clear(); - allTimeouts = new WeakMap(); - }; - - return debounced; -} - -export default Implicits; diff --git a/vscode/src/Visualize.ts b/vscode/src/Visualize.ts deleted file mode 100644 index d57b1ddf..00000000 --- a/vscode/src/Visualize.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { commands, Disposable, languages, OutputChannel, ProviderResult, TextDocumentContentProvider, Uri, ViewColumn, window, workspace } from "vscode"; -import { LanguageClient } from "vscode-languageclient/node"; - -class Visualize implements Disposable, TextDocumentContentProvider { - // The client used to communicate with the language server. - private readonly languageClient: LanguageClient; - - // The output channel used for logging for this class. It's given from the - // main file so that it uses the same as the rest of the extension. - private readonly outputChannel: OutputChannel; - - // The list of callbacks and objects that should be disposed when an instance - // of Visualize is being disposed. - private readonly disposables: Disposable[]; - - constructor(languageClient: LanguageClient, outputChannel: OutputChannel) { - this.languageClient = languageClient; - this.outputChannel = outputChannel; - this.disposables = [ - commands.registerCommand("syntaxTree.visualize", this.visualize), - workspace.registerTextDocumentContentProvider("syntaxTree.visualize", this) - ]; - } - - dispose() { - this.disposables.forEach((disposable) => disposable.dispose()); - } - - provideTextDocumentContent(uri: Uri): ProviderResult { - this.outputChannel.appendLine("Requesting visualization"); - return this.languageClient.sendRequest("syntaxTree/visualizing", { textDocument: { uri: uri.path } }); - } - - async visualize() { - const document = window.activeTextEditor?.document; - - if (document && document.languageId === "ruby" && document.uri.scheme === "file") { - const uri = Uri.parse(`syntaxTree.visualize:${document.uri.toString()}`); - - const doc = await workspace.openTextDocument(uri); - languages.setTextDocumentLanguage(doc, "plaintext"); - - await window.showTextDocument(doc, ViewColumn.Beside, true); - } - } -} - -export default Visualize; diff --git a/vscode/src/extension.ts b/vscode/src/extension.ts deleted file mode 100644 index c60fb8cf..00000000 --- a/vscode/src/extension.ts +++ /dev/null @@ -1,53 +0,0 @@ -"use strict"; - -import { ExtensionContext, commands, window } from "vscode"; -import { LanguageClient } from "vscode-languageclient/node"; - -import Disasm from "./Disasm"; -import Implicits from "./Implicits"; -import startLanguageClient from "./startLanguageClient"; -import Visualize from "./Visualize"; - -export function activate(context: ExtensionContext) { - let languageClient: LanguageClient | null = null; - const outputChannel = window.createOutputChannel("Syntax Tree"); - - context.subscriptions.push( - commands.registerCommand("syntaxTree.start", startLanguageServer), - commands.registerCommand("syntaxTree.stop", stopLanguageServer), - commands.registerCommand("syntaxTree.restart", restartLanguageServer), - commands.registerCommand("syntaxTree.showOutputChannel", () => outputChannel.show()), - outputChannel - ); - - return startLanguageServer(); - - async function startLanguageServer() { - outputChannel.appendLine("Starting language server..."); - languageClient = await startLanguageClient(outputChannel); - - if (languageClient) { - context.subscriptions.push(languageClient.start()); - await languageClient.onReady(); - - context.subscriptions.push( - new Disasm(languageClient, outputChannel), - new Implicits(languageClient, outputChannel), - new Visualize(languageClient, outputChannel) - ); - } - } - - async function stopLanguageServer() { - if (languageClient) { - outputChannel.appendLine("Stopping language server..."); - await languageClient.stop(); - } - } - - async function restartLanguageServer() { - outputChannel.appendLine("Restarting language server..."); - await stopLanguageServer(); - await startLanguageServer(); - } -} diff --git a/vscode/src/startLanguageClient.ts b/vscode/src/startLanguageClient.ts deleted file mode 100644 index b1793d38..00000000 --- a/vscode/src/startLanguageClient.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { workspace, OutputChannel } from "vscode"; -import { LanguageClient } from "vscode-languageclient/node"; -import { promisify } from "util"; -import { exec } from "child_process"; - -const promiseExec = promisify(exec); - -async function startLocalClient(outputChannel: OutputChannel) { - const rootFolder = workspace.workspaceFolders![0]; - const cwd = rootFolder.uri.fsPath; - - try { - await promiseExec("bundle show syntax_tree", { cwd }); - } catch { - outputChannel.appendLine(""); - outputChannel.appendLine("Error: Cannot find `syntax_tree` in Gemfile."); - outputChannel.appendLine("Please add `syntax_tree` in your Gemfile and `bundle install`."); - outputChannel.appendLine(""); - return null; - } - - // In this case we're running inside a project, so we're going to attempt - // to run stree with bundler. - const run = { - command: "bundle", - args: ["exec", "stree", "lsp"], - options: { cwd } - }; - - return new LanguageClient("Syntax Tree", { run, debug: run }, { - documentSelector: [ - { scheme: "file", language: "ruby" }, - ], - outputChannel - }); -} - -function startGlobalClient(outputChannel: OutputChannel) { - const run = { command: "stree", args: ["lsp"] } - - return new LanguageClient("Syntax Tree", { run, debug: run }, { - documentSelector: [ - { scheme: "file", language: "ruby" }, - ], - outputChannel - }); -} - -async function startLanguageClient(outputChannel: OutputChannel) { - if (workspace.workspaceFolders) { - return await startLocalClient(outputChannel); - } else { - return startGlobalClient(outputChannel); - } -} - -export default startLanguageClient; diff --git a/vscode/tsconfig.json b/vscode/tsconfig.json deleted file mode 100644 index 53af4bb5..00000000 --- a/vscode/tsconfig.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "compilerOptions": { - "module": "commonjs", - "target": "es6", - "outDir": "out", - "lib": [ - "es6" - ], - "sourceMap": true, - "rootDir": "src", - "strict": true - }, - "exclude": [ - "node_modules" - ] -} diff --git a/vscode/yarn.lock b/vscode/yarn.lock deleted file mode 100644 index 986cb186..00000000 --- a/vscode/yarn.lock +++ /dev/null @@ -1,89 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@types/node@^17.0.8": - version "17.0.8" - resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.8.tgz#50d680c8a8a78fe30abe6906453b21ad8ab0ad7b" - integrity sha512-YofkM6fGv4gDJq78g4j0mMuGMkZVxZDgtU0JRdx6FgiJDG+0fY0GKVolOV8WqVmEhLCXkQRjwDdKyPxJp/uucg== - -"@types/vscode@^1.46.0": - version "1.63.1" - resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.63.1.tgz#b40f9f18055e2c9498ae543d18c59fbd6ef2e8a3" - integrity sha512-Z+ZqjRcnGfHP86dvx/BtSwWyZPKQ/LBdmAVImY82TphyjOw2KgTKcp7Nx92oNwCTsHzlshwexAG/WiY2JuUm3g== - -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= - -lru-cache@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" - integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== - dependencies: - yallist "^4.0.0" - -minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== - dependencies: - brace-expansion "^1.1.7" - -semver@^7.3.4: - version "7.3.5" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" - integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== - dependencies: - lru-cache "^6.0.0" - -typescript@^4.2.2: - version "4.5.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.4.tgz#a17d3a0263bf5c8723b9c52f43c5084edf13c2e8" - integrity sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg== - -vscode-jsonrpc@6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz#108bdb09b4400705176b957ceca9e0880e9b6d4e" - integrity sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg== - -vscode-languageclient@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-7.0.0.tgz#b505c22c21ffcf96e167799757fca07a6bad0fb2" - integrity sha512-P9AXdAPlsCgslpP9pRxYPqkNYV7Xq8300/aZDpO35j1fJm/ncize8iGswzYlcvFw5DQUx4eVk+KvfXdL0rehNg== - dependencies: - minimatch "^3.0.4" - semver "^7.3.4" - vscode-languageserver-protocol "3.16.0" - -vscode-languageserver-protocol@3.16.0: - version "3.16.0" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0.tgz#34135b61a9091db972188a07d337406a3cdbe821" - integrity sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A== - dependencies: - vscode-jsonrpc "6.0.0" - vscode-languageserver-types "3.16.0" - -vscode-languageserver-types@3.16.0: - version "3.16.0" - resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz#ecf393fc121ec6974b2da3efb3155644c514e247" - integrity sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA== - -yallist@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== From 909f77b4c82445a1ab819f49a6007d61b9d9f914 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Feb 2022 17:29:48 +0000 Subject: [PATCH 10/25] Bump benchmark-ips from 2.9.3 to 2.10.0 Bumps [benchmark-ips](https://github.com/evanphx/benchmark-ips) from 2.9.3 to 2.10.0. - [Release notes](https://github.com/evanphx/benchmark-ips/releases) - [Changelog](https://github.com/evanphx/benchmark-ips/blob/master/History.md) - [Commits](https://github.com/evanphx/benchmark-ips/compare/v2.9.3...v2.10.0) --- updated-dependencies: - dependency-name: benchmark-ips dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 1ae88ac5..160d41cc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,7 +7,7 @@ GEM remote: https://rubygems.org/ specs: ast (2.4.2) - benchmark-ips (2.9.3) + benchmark-ips (2.10.0) docile (1.4.0) minitest (5.15.0) parser (3.1.0.0) From f59e9241f3896788a9ca74f87c1cff72ffa327b9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Feb 2022 17:28:59 +0000 Subject: [PATCH 11/25] Bump parser from 3.1.0.0 to 3.1.1.0 Bumps [parser](https://github.com/whitequark/parser) from 3.1.0.0 to 3.1.1.0. - [Release notes](https://github.com/whitequark/parser/releases) - [Changelog](https://github.com/whitequark/parser/blob/master/CHANGELOG.md) - [Commits](https://github.com/whitequark/parser/compare/v3.1.0.0...v3.1.1.0) --- updated-dependencies: - dependency-name: parser dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 1ae88ac5..8efecfbb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -10,7 +10,7 @@ GEM benchmark-ips (2.9.3) docile (1.4.0) minitest (5.15.0) - parser (3.1.0.0) + parser (3.1.1.0) ast (~> 2.4.1) rake (13.0.6) ruby_parser (3.18.1) From 580e21cd8d47fe5b9d168455b19a9c75ded72e5b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 25 Feb 2022 17:29:35 +0000 Subject: [PATCH 12/25] Bump stackprof from 0.2.17 to 0.2.19 Bumps [stackprof](https://github.com/tmm1/stackprof) from 0.2.17 to 0.2.19. - [Release notes](https://github.com/tmm1/stackprof/releases) - [Changelog](https://github.com/tmm1/stackprof/blob/master/CHANGELOG.md) - [Commits](https://github.com/tmm1/stackprof/compare/v0.2.17...v0.2.19) --- updated-dependencies: - dependency-name: stackprof dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 1ae88ac5..a5663c77 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -22,7 +22,7 @@ GEM simplecov_json_formatter (~> 0.1) simplecov-html (0.12.3) simplecov_json_formatter (0.1.3) - stackprof (0.2.17) + stackprof (0.2.19) PLATFORMS x86_64-darwin-19 From 030be40438cbc2315b7693d1e8048af0a36a709a Mon Sep 17 00:00:00 2001 From: Maple Ong Date: Mon, 28 Mar 2022 16:51:54 -0400 Subject: [PATCH 13/25] Generate and deploy rdoc on GH pages using GH workflow --- .github/workflows/gh-pages.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/gh-pages.yml diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml new file mode 100644 index 00000000..887cd49b --- /dev/null +++ b/.github/workflows/gh-pages.yml @@ -0,0 +1,25 @@ +name: Github Pages (rdoc) +on: [push] +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout 🛎️ + uses: actions/checkout@master + + - name: Set up Ruby 💎 + uses: ruby/setup-ruby@v1 + with: + bundler-cache: true + ruby-version: '3.1' + + - name: Install rdoc and generate docs 🔧 + run: | + gem install rdoc + rdoc --op rdocs + + - name: Deploy 🚀 + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./rdocs From 5140d973601158cff119fcf95e4cec2d3df3ff5b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Mar 2022 17:29:49 +0000 Subject: [PATCH 14/25] Bump ruby_parser from 3.18.1 to 3.19.0 Bumps [ruby_parser](https://github.com/seattlerb/ruby_parser) from 3.18.1 to 3.19.0. - [Release notes](https://github.com/seattlerb/ruby_parser/releases) - [Changelog](https://github.com/seattlerb/ruby_parser/blob/master/History.rdoc) - [Commits](https://github.com/seattlerb/ruby_parser/compare/v3.18.1...v3.19.0) --- updated-dependencies: - dependency-name: ruby_parser dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index ed8e1cf3..d3e0d67c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -13,7 +13,7 @@ GEM parser (3.1.1.0) ast (~> 2.4.1) rake (13.0.6) - ruby_parser (3.18.1) + ruby_parser (3.19.0) sexp_processor (~> 4.16) sexp_processor (4.16.0) simplecov (0.21.2) From 762ae531958c5064f4a765ff91a3d2ea6b415595 Mon Sep 17 00:00:00 2001 From: Maple Ong Date: Wed, 30 Mar 2022 16:10:27 -0400 Subject: [PATCH 15/25] Exclude vendor directory when generating rdoc --- .github/workflows/gh-pages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index 887cd49b..583ade2a 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -16,7 +16,7 @@ jobs: - name: Install rdoc and generate docs 🔧 run: | gem install rdoc - rdoc --op rdocs + rdoc --op rdocs --exclude={Gemfile,Rakefile,"vendor/*","bin/*","test/*"} - name: Deploy 🚀 uses: peaceiris/actions-gh-pages@v3 From 590ccb53b20f723ea586532c95c6390e64fc028d Mon Sep 17 00:00:00 2001 From: Alexandre Terrasa Date: Wed, 30 Mar 2022 19:44:15 -0400 Subject: [PATCH 16/25] Trim trailing spaces Signed-off-by: Alexandre Terrasa --- lib/syntax_tree/implicits.rb | 10 +++++----- test/node_test.rb | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/syntax_tree/implicits.rb b/lib/syntax_tree/implicits.rb index 191f7a74..3f751e29 100644 --- a/lib/syntax_tree/implicits.rb +++ b/lib/syntax_tree/implicits.rb @@ -11,7 +11,7 @@ def initialize # Adds the implicitly rescued StandardError into a bare rescue clause. For # example, - # + # # begin # rescue # end @@ -29,7 +29,7 @@ def bare_rescue(location) # Adds the implicitly referenced value (local variable or method call) that # is added into a hash when the value of a key-value pair is omitted. For # example, - # + # # { value: } # # becomes @@ -57,14 +57,14 @@ def precedence_parentheses(location) def self.find(program) implicits = new queue = [[nil, program]] - + until queue.empty? parent_node, child_node = queue.shift - + child_node.child_nodes.each do |grand_child_node| queue << [child_node, grand_child_node] if grand_child_node end - + case [parent_node, child_node] in _, Rescue[exception: nil, location:] implicits.bare_rescue(location) diff --git a/test/node_test.rb b/test/node_test.rb index b5b064e6..0622397a 100644 --- a/test/node_test.rb +++ b/test/node_test.rb @@ -151,7 +151,7 @@ def test_assoc guard_version("3.1.0") do def test_assoc_no_value source = "{ key1:, key2: }" - + at = location(chars: 2..7) assert_node(Assoc, "assoc", source, at: at) { |node| node.assocs.first } end @@ -238,7 +238,7 @@ def test_blockarg guard_version("3.1.0") do def test_blockarg_anonymous source = "def method(&); end" - + at = location(chars: 11..12) assert_node(BlockArg, "blockarg", source, at: at) do |node| node.params.contents.block From e090e0afb2a06079c098ba438ff17c8258af78fd Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Wed, 30 Mar 2022 20:21:35 -0400 Subject: [PATCH 17/25] Fix up CI --- lib/syntax_tree.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/syntax_tree.rb b/lib/syntax_tree.rb index f1014b2f..d976d286 100644 --- a/lib/syntax_tree.rb +++ b/lib/syntax_tree.rb @@ -3102,14 +3102,12 @@ def to_json(*opts) # (nil | Ensure) ensure_clause # ) -> BodyStmt def on_bodystmt(statements, rescue_clause, else_clause, ensure_clause) - ending = [ensure_clause, else_clause, rescue_clause, statements].detect(&:itself) - BodyStmt.new( statements: statements, rescue_clause: rescue_clause, else_clause: else_clause, ensure_clause: ensure_clause, - location: statements.location.to(ending.location) + location: Location.fixed(line: lineno, char: char_pos) ) end From f27af1a4b9d1047e4a0e452dfc83a40aab202547 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Wed, 30 Mar 2022 21:24:18 -0400 Subject: [PATCH 18/25] More docs cleanup --- .github/workflows/gh-pages.yml | 3 ++- .gitignore | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index 583ade2a..8adc1b45 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -16,7 +16,8 @@ jobs: - name: Install rdoc and generate docs 🔧 run: | gem install rdoc - rdoc --op rdocs --exclude={Gemfile,Rakefile,"vendor/*","bin/*","test/*"} + rdoc --main README.md --op rdocs --exclude={Gemfile,Rakefile,"coverage/*","vendor/*","bin/*","test/*","tmp/*"} + cp -r doc rdocs/doc - name: Deploy 🚀 uses: peaceiris/actions-gh-pages@v3 diff --git a/.gitignore b/.gitignore index 4d0d86f3..24bbb786 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ /_yardoc/ /coverage/ /pkg/ +/rdocs/ /spec/reports/ /tmp/ From 3930a88753066881edfad8f65f83e57a448a46c9 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Wed, 30 Mar 2022 22:22:23 -0400 Subject: [PATCH 19/25] Move nodes into their own file --- lib/syntax_tree.rb | 12369 +------------------------------------- lib/syntax_tree/node.rb | 11931 ++++++++++++++++++++++++++++++++++++ 2 files changed, 12169 insertions(+), 12131 deletions(-) create mode 100644 lib/syntax_tree/node.rb diff --git a/lib/syntax_tree.rb b/lib/syntax_tree.rb index d976d286..b0236031 100644 --- a/lib/syntax_tree.rb +++ b/lib/syntax_tree.rb @@ -5,6 +5,7 @@ require "ripper" require "stringio" +require_relative "syntax_tree/node" require_relative "syntax_tree/version" # If PrettyPrint::Align isn't defined, then we haven't gotten the updated @@ -410,85 +411,6 @@ def find_next_statement_start(position) # The following methods all handle a dispatched ripper event. # ---------------------------------------------------------------------------- - # BEGINBlock represents the use of the +BEGIN+ keyword, which hooks into the - # lifecycle of the interpreter. Whatever is inside the block will get executed - # when the program starts. - # - # BEGIN { - # } - # - # Interestingly, the BEGIN keyword doesn't allow the do and end keywords for - # the block. Only braces are permitted. - class BEGINBlock - # [LBrace] the left brace that is seen after the keyword - attr_reader :lbrace - - # [Statements] the expressions to be executed - attr_reader :statements - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(lbrace:, statements:, location:, comments: []) - @lbrace = lbrace - @statements = statements - @location = location - @comments = comments - end - - def child_nodes - [lbrace, statements] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - lbrace: lbrace, - statements: statements, - location: location, - comments: comments - } - end - - def format(q) - q.group do - q.text("BEGIN ") - q.format(lbrace) - q.indent do - q.breakable - q.format(statements) - end - q.breakable - q.text("}") - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("BEGIN") - - q.breakable - q.pp(statements) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :BEGIN, - lbrace: lbrace, - stmts: statements, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_BEGIN: (Statements statements) -> BEGINBlock def on_BEGIN(statements) @@ -509,66 +431,6 @@ def on_BEGIN(statements) ) end - # CHAR irepresents a single codepoint in the script encoding. - # - # ?a - # - # In the example above, the CHAR node represents the string literal "a". You - # can use control characters with this as well, as in ?\C-a. - class CHAR - # [String] the value of the character literal - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(value:, location:, comments: []) - @value = value - @location = location - @comments = comments - end - - def child_nodes - [] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { value: value, location: location, comments: comments } - end - - def format(q) - if value.length != 2 - q.text(value) - else - q.text(q.quote) - q.text(value[1] == "\"" ? "\\\"" : value[1]) - q.text(q.quote) - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("CHAR") - - q.breakable - q.pp(value) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { type: :CHAR, value: value, loc: location, cmts: comments }.to_json( - *opts - ) - end - end - # :call-seq: # on_CHAR: (String value) -> CHAR def on_CHAR(value) @@ -578,85 +440,6 @@ def on_CHAR(value) ) end - # ENDBlock represents the use of the +END+ keyword, which hooks into the - # lifecycle of the interpreter. Whatever is inside the block will get executed - # when the program ends. - # - # END { - # } - # - # Interestingly, the END keyword doesn't allow the do and end keywords for the - # block. Only braces are permitted. - class ENDBlock - # [LBrace] the left brace that is seen after the keyword - attr_reader :lbrace - - # [Statements] the expressions to be executed - attr_reader :statements - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(lbrace:, statements:, location:, comments: []) - @lbrace = lbrace - @statements = statements - @location = location - @comments = comments - end - - def child_nodes - [lbrace, statements] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - lbrace: lbrace, - statements: statements, - location: location, - comments: comments - } - end - - def format(q) - q.group do - q.text("END ") - q.format(lbrace) - q.indent do - q.breakable - q.format(statements) - end - q.breakable - q.text("}") - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("END") - - q.breakable - q.pp(statements) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :END, - lbrace: lbrace, - stmts: statements, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_END: (Statements statements) -> ENDBlock def on_END(statements) @@ -677,67 +460,6 @@ def on_END(statements) ) end - # EndContent represents the use of __END__ syntax, which allows individual - # scripts to keep content after the main ruby code that can be read through - # the DATA constant. - # - # puts DATA.read - # - # __END__ - # some other content that is not executed by the program - # - class EndContent - # [String] the content after the script - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(value:, location:, comments: []) - @value = value - @location = location - @comments = comments - end - - def child_nodes - [] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { value: value, location: location, comments: comments } - end - - def format(q) - q.text("__END__") - q.breakable(force: true) - - separator = -> { q.breakable(indent: false, force: true) } - q.seplist(value.split(/\r?\n/, -1), separator) { |line| q.text(line) } - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("__end__") - - q.breakable - q.pp(value) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { type: :__end__, value: value, loc: location, cmts: comments }.to_json( - *opts - ) - end - end - # :call-seq: # on___end__: (String value) -> EndContent def on___end__(value) @@ -748,113 +470,6 @@ def on___end__(value) ) end - # Alias represents the use of the +alias+ keyword with regular arguments (not - # global variables). The +alias+ keyword is used to make a method respond to - # another name as well as the current one. - # - # alias aliased_name name - # - # For the example above, in the current context you can now call aliased_name - # and it will execute the name method. When you're aliasing two methods, you - # can either provide bare words (like the example above) or you can provide - # symbols (note that this includes dynamic symbols like - # :"left-#{middle}-right"). - class Alias - class AliasArgumentFormatter - # [DynaSymbol | SymbolLiteral] the argument being passed to alias - attr_reader :argument - - def initialize(argument) - @argument = argument - end - - def comments - if argument.is_a?(SymbolLiteral) - argument.comments + argument.value.comments - else - argument.comments - end - end - - def format(q) - if argument.is_a?(SymbolLiteral) - q.format(argument.value) - else - q.format(argument) - end - end - end - - # [DynaSymbol | SymbolLiteral] the new name of the method - attr_reader :left - - # [DynaSymbol | SymbolLiteral] the old name of the method - attr_reader :right - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(left:, right:, location:, comments: []) - @left = left - @right = right - @location = location - @comments = comments - end - - def child_nodes - [left, right] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { left: left, right: right, location: location, comments: comments } - end - - def format(q) - keyword = "alias " - left_argument = AliasArgumentFormatter.new(left) - - q.group do - q.text(keyword) - q.format(left_argument, stackable: false) - q.group do - q.nest(keyword.length) do - q.breakable(force: left_argument.comments.any?) - q.format(AliasArgumentFormatter.new(right), stackable: false) - end - end - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("alias") - - q.breakable - q.pp(left) - - q.breakable - q.pp(right) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :alias, - left: left, - right: right, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_alias: ( # (DynaSymbol | SymbolLiteral) left, @@ -870,96 +485,6 @@ def on_alias(left, right) ) end - # ARef represents when you're pulling a value out of a collection at a - # specific index. Put another way, it's any time you're calling the method - # #[]. - # - # collection[index] - # - # The nodes usually contains two children, the collection and the index. In - # some cases, you don't necessarily have the second child node, because you - # can call procs with a pretty esoteric syntax. In the following example, you - # wouldn't have a second child node: - # - # collection[] - # - class ARef - # [untyped] the value being indexed - attr_reader :collection - - # [nil | Args] the value being passed within the brackets - attr_reader :index - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(collection:, index:, location:, comments: []) - @collection = collection - @index = index - @location = location - @comments = comments - end - - def child_nodes - [collection, index] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - collection: collection, - index: index, - location: location, - comments: comments - } - end - - def format(q) - q.group do - q.format(collection) - q.text("[") - - if index - q.indent do - q.breakable("") - q.format(index) - end - q.breakable("") - end - - q.text("]") - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("aref") - - q.breakable - q.pp(collection) - - q.breakable - q.pp(index) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :aref, - collection: collection, - index: index, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_aref: (untyped collection, (nil | Args) index) -> ARef def on_aref(collection, index) @@ -973,90 +498,6 @@ def on_aref(collection, index) ) end - # ARefField represents assigning values into collections at specific indices. - # Put another way, it's any time you're calling the method #[]=. The - # ARefField node itself is just the left side of the assignment, and they're - # always wrapped in assign nodes. - # - # collection[index] = value - # - class ARefField - # [untyped] the value being indexed - attr_reader :collection - - # [nil | Args] the value being passed within the brackets - attr_reader :index - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(collection:, index:, location:, comments: []) - @collection = collection - @index = index - @location = location - @comments = comments - end - - def child_nodes - [collection, index] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - collection: collection, - index: index, - location: location, - comments: comments - } - end - - def format(q) - q.group do - q.format(collection) - q.text("[") - - if index - q.indent do - q.breakable("") - q.format(index) - end - q.breakable("") - end - - q.text("]") - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("aref_field") - - q.breakable - q.pp(collection) - - q.breakable - q.pp(index) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :aref_field, - collection: collection, - index: index, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_aref_field: ( # untyped collection, @@ -1077,80 +518,6 @@ def on_aref_field(collection, index) # value # end - # ArgParen represents wrapping arguments to a method inside a set of - # parentheses. - # - # method(argument) - # - # In the example above, there would be an ArgParen node around the Args node - # that represents the set of arguments being sent to the method method. The - # argument child node can be +nil+ if no arguments were passed, as in: - # - # method() - # - class ArgParen - # [nil | Args | ArgsForward] the arguments inside the - # parentheses - attr_reader :arguments - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(arguments:, location:, comments: []) - @arguments = arguments - @location = location - @comments = comments - end - - def child_nodes - [arguments] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { arguments: arguments, location: location, comments: comments } - end - - def format(q) - unless arguments - q.text("()") - return - end - - q.group(0, "(", ")") do - q.indent do - q.breakable("") - q.format(arguments) - end - q.breakable("") - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("arg_paren") - - q.breakable - q.pp(arguments) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :arg_paren, - args: arguments, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_arg_paren: ( # (nil | Args | ArgsForward) arguments @@ -1175,59 +542,6 @@ def on_arg_paren(arguments) ) end - # Args represents a list of arguments being passed to a method call or array - # literal. - # - # method(first, second, third) - # - class Args - # [Array[ untyped ]] the arguments that this node wraps - attr_reader :parts - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(parts:, location:, comments: []) - @parts = parts - @location = location - @comments = comments - end - - def child_nodes - parts - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { parts: parts, location: location, comments: comments } - end - - def format(q) - q.seplist(parts) { |part| q.format(part) } - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("args") - - q.breakable - q.group(2, "(", ")") { q.seplist(parts) { |part| q.pp(part) } } - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { type: :args, parts: parts, loc: location, cmts: comments }.to_json( - *opts - ) - end - end - # :call-seq: # on_args_add: (Args arguments, untyped argument) -> Args def on_args_add(arguments, argument) @@ -1246,68 +560,13 @@ def on_args_add(arguments, argument) end end - # ArgBlock represents using a block operator on an expression. - # - # method(&expression) - # - class ArgBlock - # [nil | untyped] the expression being turned into a block - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(value:, location:, comments: []) - @value = value - @location = location - @comments = comments - end - - def child_nodes - [value] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { value: value, location: location, comments: comments } - end - - def format(q) - q.text("&") - q.format(value) if value - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("arg_block") - - if value - q.breakable - q.pp(value) - end - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { type: :arg_block, value: value, loc: location, cmts: comments }.to_json( - *opts - ) - end - end - - # :call-seq: - # on_args_add_block: ( - # Args arguments, - # (false | untyped) block - # ) -> Args - def on_args_add_block(arguments, block) - operator = find_token(Op, "&", consume: false) + # :call-seq: + # on_args_add_block: ( + # Args arguments, + # (false | untyped) block + # ) -> Args + def on_args_add_block(arguments, block) + operator = find_token(Op, "&", consume: false) # If we can't find the & operator, then there's no block to add to the list, # so we're just going to return the arguments as-is. @@ -1338,59 +597,6 @@ def on_args_add_block(arguments, block) ) end - # Star represents using a splat operator on an expression. - # - # method(*arguments) - # - class ArgStar - # [nil | untyped] the expression being splatted - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(value:, location:, comments: []) - @value = value - @location = location - @comments = comments - end - - def child_nodes - [value] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { value: value, location: location, comments: comments } - end - - def format(q) - q.text("*") - q.format(value) if value - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("arg_star") - - q.breakable - q.pp(value) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { type: :arg_star, value: value, loc: location, cmts: comments }.to_json( - *opts - ) - end - end - # :call-seq: # on_args_add_star: (Args arguments, untyped star) -> Args def on_args_add_star(arguments, argument) @@ -1413,74 +619,6 @@ def on_args_add_star(arguments, argument) Args.new(parts: arguments.parts << arg_star, location: location) end - # ArgsForward represents forwarding all kinds of arguments onto another method - # call. - # - # def request(method, path, **headers, &block); end - # - # def get(...) - # request(:GET, ...) - # end - # - # def post(...) - # request(:POST, ...) - # end - # - # In the example above, both the get and post methods are forwarding all of - # their arguments (positional, keyword, and block) on to the request method. - # The ArgsForward node appears in both the caller (the request method calls) - # and the callee (the get and post definitions). - class ArgsForward - # [String] the value of the operator - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(value:, location:, comments: []) - @value = value - @location = location - @comments = comments - end - - def child_nodes - [] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { value: value, location: location, comments: comments } - end - - def format(q) - q.text(value) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("args_forward") - - q.breakable - q.pp(value) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :args_forward, - value: value, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_args_forward: () -> ArgsForward def on_args_forward @@ -1495,205 +633,6 @@ def on_args_new Args.new(parts: [], location: Location.fixed(line: lineno, char: char_pos)) end - # ArrayLiteral represents an array literal, which can optionally contain - # elements. - # - # [] - # [one, two, three] - # - class ArrayLiteral - class QWordsFormatter - # [Args] the contents of the array - attr_reader :contents - - def initialize(contents) - @contents = contents - end - - def format(q) - q.group(0, "%w[", "]") do - q.indent do - q.breakable("") - q.seplist(contents.parts, -> { q.breakable }) do |part| - if part.is_a?(StringLiteral) - q.format(part.parts.first) - else - q.text(part.value[1..-1]) - end - end - end - q.breakable("") - end - end - end - - class QSymbolsFormatter - # [Args] the contents of the array - attr_reader :contents - - def initialize(contents) - @contents = contents - end - - def format(q) - q.group(0, "%i[", "]") do - q.indent do - q.breakable("") - q.seplist(contents.parts, -> { q.breakable }) do |part| - q.format(part.value) - end - end - q.breakable("") - end - end - end - - class VarRefsFormatter - # [Args] the contents of the array - attr_reader :contents - - def initialize(contents) - @contents = contents - end - - def format(q) - q.group(0, "[", "]") do - q.indent do - q.breakable("") - - separator = -> do - q.text(",") - q.fill_breakable - end - - q.seplist(contents.parts, separator) { |part| q.format(part) } - end - q.breakable("") - end - end - end - - # [LBracket] the bracket that opens this array - attr_reader :lbracket - - # [nil | Args] the contents of the array - attr_reader :contents - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(lbracket:, contents:, location:, comments: []) - @lbracket = lbracket - @contents = contents - @location = location - @comments = comments - end - - def child_nodes - [lbracket, contents] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - lbracket: lbracket, - contents: contents, - location: location, - comments: comments - } - end - - def format(q) - if qwords? - QWordsFormatter.new(contents).format(q) - return - end - - if qsymbols? - QSymbolsFormatter.new(contents).format(q) - return - end - - if var_refs?(q) - VarRefsFormatter.new(contents).format(q) - return - end - - q.group do - q.format(lbracket) - - if contents - q.indent do - q.breakable("") - q.format(contents) - end - end - - q.breakable("") - q.text("]") - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("array") - - q.breakable - q.pp(contents) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { type: :array, cnts: contents, loc: location, cmts: comments }.to_json( - *opts - ) - end - - private - - def qwords? - lbracket.comments.empty? && contents && contents.comments.empty? && - contents.parts.length > 1 && - contents.parts.all? do |part| - case part - when StringLiteral - part.comments.empty? && part.parts.length == 1 && - part.parts.first.is_a?(TStringContent) && - !part.parts.first.value.match?(/[\s\[\]\\]/) - when CHAR - !part.value.match?(/[\[\]\\]/) - else - false - end - end - end - - def qsymbols? - lbracket.comments.empty? && contents && contents.comments.empty? && - contents.parts.length > 1 && - contents.parts.all? do |part| - part.is_a?(SymbolLiteral) && part.comments.empty? - end - end - - def var_refs?(q) - lbracket.comments.empty? && contents && contents.comments.empty? && - contents.parts.all? do |part| - part.is_a?(VarRef) && part.comments.empty? - end && - ( - contents.parts.sum { |part| part.value.value.length + 2 } > - q.maxwidth * 2 - ) - end - end - # :call-seq: # on_array: ((nil | Args) contents) -> # ArrayLiteral | QSymbols | QWords | Symbols | Words @@ -1719,165 +658,6 @@ def on_array(contents) end end - # AryPtn represents matching against an array pattern using the Ruby 2.7+ - # pattern matching syntax. It’s one of the more complicated nodes, because - # the four parameters that it accepts can almost all be nil. - # - # case [1, 2, 3] - # in [Integer, Integer] - # "matched" - # in Container[Integer, Integer] - # "matched" - # in [Integer, *, Integer] - # "matched" - # end - # - # An AryPtn node is created with four parameters: an optional constant - # wrapper, an array of positional matches, an optional splat with identifier, - # and an optional array of positional matches that occur after the splat. - # All of the in clauses above would create an AryPtn node. - class AryPtn - class RestFormatter - # [VarField] the identifier that represents the remaining positionals - attr_reader :value - - def initialize(value) - @value = value - end - - def comments - value.comments - end - - def format(q) - q.text("*") - q.format(value) - end - end - - # [nil | VarRef] the optional constant wrapper - attr_reader :constant - - # [Array[ untyped ]] the regular positional arguments that this array - # pattern is matching against - attr_reader :requireds - - # [nil | VarField] the optional starred identifier that grabs up a list of - # positional arguments - attr_reader :rest - - # [Array[ untyped ]] the list of positional arguments occurring after the - # optional star if there is one - attr_reader :posts - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize( - constant:, - requireds:, - rest:, - posts:, - location:, - comments: [] - ) - @constant = constant - @requireds = requireds - @rest = rest - @posts = posts - @location = location - @comments = comments - end - - def child_nodes - [constant, *requireds, rest, *posts] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - constant: constant, - requireds: requireds, - rest: rest, - posts: posts, - location: location, - comments: comments - } - end - - def format(q) - parts = [*requireds] - parts << RestFormatter.new(rest) if rest - parts += posts - - if constant - q.group do - q.format(constant) - q.text("[") - q.seplist(parts) { |part| q.format(part) } - q.text("]") - end - - return - end - - parent = q.parent - if parts.length == 1 || PATTERNS.include?(parent.class) - q.text("[") - q.seplist(parts) { |part| q.format(part) } - q.text("]") - else - q.group { q.seplist(parts) { |part| q.format(part) } } - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("aryptn") - - if constant - q.breakable - q.pp(constant) - end - - if requireds.any? - q.breakable - q.group(2, "(", ")") do - q.seplist(requireds) { |required| q.pp(required) } - end - end - - if rest - q.breakable - q.pp(rest) - end - - if posts.any? - q.breakable - q.group(2, "(", ")") { q.seplist(posts) { |post| q.pp(post) } } - end - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :aryptn, - constant: constant, - reqs: requireds, - rest: rest, - posts: posts, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_aryptn: ( # (nil | VarRef) constant, @@ -1897,109 +677,6 @@ def on_aryptn(constant, requireds, rest, posts) ) end - # Determins if the following value should be indented or not. - module AssignFormatting - def self.skip_indent?(value) - (value.is_a?(Call) && skip_indent?(value.receiver)) || - [ - ArrayLiteral, - HashLiteral, - Heredoc, - Lambda, - QSymbols, - QWords, - Symbols, - Words - ].include?(value.class) - end - end - - # Assign represents assigning something to a variable or constant. Generally, - # the left side of the assignment is going to be any node that ends with the - # name "Field". - # - # variable = value - # - class Assign - # [ARefField | ConstPathField | Field | TopConstField | VarField] the target - # to assign the result of the expression to - attr_reader :target - - # [untyped] the expression to be assigned - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(target:, value:, location:, comments: []) - @target = target - @value = value - @location = location - @comments = comments - end - - def child_nodes - [target, value] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { target: target, value: value, location: location, comments: comments } - end - - def format(q) - q.group do - q.format(target) - q.text(" =") - - if skip_indent? - q.text(" ") - q.format(value) - else - q.indent do - q.breakable - q.format(value) - end - end - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("assign") - - q.breakable - q.pp(target) - - q.breakable - q.pp(value) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :assign, - target: target, - value: value, - loc: location, - cmts: comments - }.to_json(*opts) - end - - private - - def skip_indent? - target.comments.empty? && - (target.is_a?(ARefField) || AssignFormatting.skip_indent?(value)) - end - end - # :call-seq: # on_assign: ( # (ARefField | ConstPathField | Field | TopConstField | VarField) target, @@ -2013,225 +690,27 @@ def on_assign(target, value) ) end - # Assoc represents a key-value pair within a hash. It is a child node of - # either an AssocListFromArgs or a BareAssocHash. - # - # { key1: value1, key2: value2 } - # - # In the above example, the would be two AssocNew nodes. - class Assoc - # [untyped] the key of this pair - attr_reader :key - - # [untyped] the value of this pair - attr_reader :value + # :call-seq: + # on_assoc_new: (untyped key, untyped value) -> Assoc + def on_assoc_new(key, value) + location = key.location + location = location.to(value.location) if value - # [Location] the location of this node - attr_reader :location + Assoc.new(key: key, value: value, location: location) + end - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments + # :call-seq: + # on_assoc_splat: (untyped value) -> AssocSplat + def on_assoc_splat(value) + operator = find_token(Op, "**") - def initialize(key:, value:, location:, comments: []) - @key = key - @value = value - @location = location - @comments = comments - end - - def child_nodes - [key, value] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { key: key, value: value, location: location, comments: comments } - end - - def format(q) - if value&.is_a?(HashLiteral) - format_contents(q) - else - q.group { format_contents(q) } - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("assoc") - - q.breakable - q.pp(key) - - if value - q.breakable - q.pp(value) - end - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :assoc, - key: key, - value: value, - loc: location, - cmts: comments - }.to_json(*opts) - end - - private - - def format_contents(q) - q.parent.format_key(q, key) - return unless value - - if key.comments.empty? && AssignFormatting.skip_indent?(value) - q.text(" ") - q.format(value) - else - q.indent do - q.breakable - q.format(value) - end - end - end - end - - # :call-seq: - # on_assoc_new: (untyped key, untyped value) -> Assoc - def on_assoc_new(key, value) - location = key.location - location = location.to(value.location) if value - - Assoc.new(key: key, value: value, location: location) - end - - # AssocSplat represents double-splatting a value into a hash (either a hash - # literal or a bare hash in a method call). - # - # { **pairs } - # - class AssocSplat - # [untyped] the expression that is being splatted - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(value:, location:, comments: []) - @value = value - @location = location - @comments = comments - end - - def child_nodes - [value] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { value: value, location: location, comments: comments } - end - - def format(q) - q.text("**") - q.format(value) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("assoc_splat") - - q.breakable - q.pp(value) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :assoc_splat, - value: value, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - - # :call-seq: - # on_assoc_splat: (untyped value) -> AssocSplat - def on_assoc_splat(value) - operator = find_token(Op, "**") - - AssocSplat.new(value: value, location: operator.location.to(value.location)) - end + AssocSplat.new(value: value, location: operator.location.to(value.location)) + end # def on_assoclist_from_args(assocs) # assocs # end - # Backref represents a global variable referencing a matched value. It comes - # in the form of a $ followed by a positive integer. - # - # $1 - # - class Backref - # [String] the name of the global backreference variable - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(value:, location:, comments: []) - @value = value - @location = location - @comments = comments - end - - def child_nodes - [] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { value: value, location: location, comments: comments } - end - - def format(q) - q.text(value) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("backref") - - q.breakable - q.pp(value) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { type: :backref, value: value, loc: location, cmts: comments }.to_json( - *opts - ) - end - end - # :call-seq: # on_backref: (String value) -> Backref def on_backref(value) @@ -2241,57 +720,6 @@ def on_backref(value) ) end - # Backtick represents the use of the ` operator. It's usually found being used - # for an XStringLiteral, but could also be found as the name of a method being - # defined. - class Backtick - # [String] the backtick in the string - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(value:, location:, comments: []) - @value = value - @location = location - @comments = comments - end - - def child_nodes - [] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { value: value, location: location, comments: comments } - end - - def format(q) - q.text(value) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("backtick") - - q.breakable - q.pp(value) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { type: :backtick, value: value, loc: location, cmts: comments }.to_json( - *opts - ) - end - end - # :call-seq: # on_backtick: (String value) -> Backtick def on_backtick(value) @@ -2305,130 +733,6 @@ def on_backtick(value) node end - # This module is responsible for formatting the assocs contained within a - # hash or bare hash. It first determines if every key in the hash can use - # labels. If it can, it uses labels. Otherwise it uses hash rockets. - module HashKeyFormatter - class Labels - def format_key(q, key) - case key - when Label - q.format(key) - when SymbolLiteral - q.format(key.value) - q.text(":") - when DynaSymbol - q.format(key) - q.text(":") - end - end - end - - class Rockets - def format_key(q, key) - case key - when Label - q.text(":") - q.text(key.value.chomp(":")) - when DynaSymbol - q.text(":") - q.format(key) - else - q.format(key) - end - - q.text(" =>") - end - end - - def self.for(container) - labels = - container.assocs.all? do |assoc| - next true if assoc.is_a?(AssocSplat) - - case assoc.key - when Label - true - when SymbolLiteral - # When attempting to convert a hash rocket into a hash label, - # you need to take care because only certain patterns are - # allowed. Ruby source says that they have to match keyword - # arguments to methods, but don't specify what that is. After - # some experimentation, it looks like it's: - value = assoc.key.value.value - value.match?(/^[_A-Za-z]/) && !value.end_with?("=") - when DynaSymbol - true - else - false - end - end - - (labels ? Labels : Rockets).new - end - end - - # BareAssocHash represents a hash of contents being passed as a method - # argument (and therefore has omitted braces). It's very similar to an - # AssocListFromArgs node. - # - # method(key1: value1, key2: value2) - # - class BareAssocHash - # [Array[ AssocNew | AssocSplat ]] - attr_reader :assocs - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(assocs:, location:, comments: []) - @assocs = assocs - @location = location - @comments = comments - end - - def child_nodes - assocs - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { assocs: assocs, location: location, comments: comments } - end - - def format(q) - q.seplist(assocs) { |assoc| q.format(assoc) } - end - - def format_key(q, key) - (@key_formatter ||= HashKeyFormatter.for(self)).format_key(q, key) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("bare_assoc_hash") - - q.breakable - q.group(2, "(", ")") { q.seplist(assocs) { |assoc| q.pp(assoc) } } - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :bare_assoc_hash, - assocs: assocs, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_bare_assoc_hash: (Array[AssocNew | AssocSplat] assocs) -> BareAssocHash def on_bare_assoc_hash(assocs) @@ -2438,140 +742,6 @@ def on_bare_assoc_hash(assocs) ) end - # Begin represents a begin..end chain. - # - # begin - # value - # end - # - class Begin - # [BodyStmt] the bodystmt that contains the contents of this begin block - attr_reader :bodystmt - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(bodystmt:, location:, comments: []) - @bodystmt = bodystmt - @location = location - @comments = comments - end - - def child_nodes - [bodystmt] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { bodystmt: bodystmt, location: location, comments: comments } - end - - def format(q) - q.text("begin") - - unless bodystmt.empty? - q.indent do - q.breakable(force: true) unless bodystmt.statements.empty? - q.format(bodystmt) - end - end - - q.breakable(force: true) - q.text("end") - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("begin") - - q.breakable - q.pp(bodystmt) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :begin, - bodystmt: bodystmt, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - - # PinnedBegin represents a pinning a nested statement within pattern matching. - # - # case value - # in ^(statement) - # end - # - class PinnedBegin - # [untyped] the expression being pinned - attr_reader :statement - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(statement:, location:, comments: []) - @statement = statement - @location = location - @comments = comments - end - - def child_nodes - [statement] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { statement: statement, location: location, comments: comments } - end - - def format(q) - q.group do - q.text("^(") - q.nest(1) do - q.indent do - q.breakable("") - q.format(statement) - end - q.breakable("") - q.text(")") - end - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("pinned_begin") - - q.breakable - q.pp(statement) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :pinned_begin, - stmt: statement, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_begin: (untyped bodystmt) -> Begin | PinnedBegin def on_begin(bodystmt) @@ -2602,103 +772,6 @@ def on_begin(bodystmt) end end - # Binary represents any expression that involves two sub-expressions with an - # operator in between. This can be something that looks like a mathematical - # operation: - # - # 1 + 1 - # - # but can also be something like pushing a value onto an array: - # - # array << value - # - class Binary - # [untyped] the left-hand side of the expression - attr_reader :left - - # [Symbol] the operator used between the two expressions - attr_reader :operator - - # [untyped] the right-hand side of the expression - attr_reader :right - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(left:, operator:, right:, location:, comments: []) - @left = left - @operator = operator - @right = right - @location = location - @comments = comments - end - - def child_nodes - [left, right] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - left: left, - operator: operator, - right: right, - location: location, - comments: comments - } - end - - def format(q) - power = operator == :** - - q.group do - q.group { q.format(left) } - q.text(" ") unless power - - q.group do - q.text(operator) - - q.indent do - q.breakable(power ? "" : " ") - q.format(right) - end - end - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("binary") - - q.breakable - q.pp(left) - - q.breakable - q.text(operator) - - q.breakable - q.pp(right) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :binary, - left: left, - op: operator, - right: right, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_binary: (untyped left, (Op | Symbol) operator, untyped right) -> Binary def on_binary(left, operator, right) @@ -2733,136 +806,14 @@ def on_binary(left, operator, right) ) end - # This module will remove any breakables from the list of contents so that no - # newlines are present in the output. - module RemoveBreaks - class << self - def call(doc) - marker = Object.new - stack = [doc] - - while stack.any? - doc = stack.pop - - if doc == marker - stack.pop - next - end - - stack += [doc, marker] - - case doc - when PrettyPrint::Align, PrettyPrint::Indent, PrettyPrint::Group - doc.contents.map! { |child| remove_breaks(child) } - stack += doc.contents.reverse - when PrettyPrint::IfBreak - doc.flat_contents.map! { |child| remove_breaks(child) } - stack += doc.flat_contents.reverse - end - end - end - - private - - def remove_breaks(doc) - case doc - when PrettyPrint::Breakable - text = PrettyPrint::Text.new - text.add(object: doc.force? ? "; " : doc.separator, width: doc.width) - text - when PrettyPrint::IfBreak - PrettyPrint::Align.new(indent: 0, contents: doc.flat_contents) - else - doc - end - end - end - end - - # BlockVar represents the parameters being declared for a block. Effectively - # this node is everything contained within the pipes. This includes all of the - # various parameter types, as well as block-local variable declarations. - # - # method do |positional, optional = value, keyword:, █ local| - # end - # - class BlockVar - # [Params] the parameters being declared with the block - attr_reader :params - - # [Array[ Ident ]] the list of block-local variable declarations - attr_reader :locals - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(params:, locals:, location:, comments: []) - @params = params - @locals = locals - @location = location - @comments = comments - end - - def child_nodes - [params, *locals] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { params: params, locals: locals, location: location, comments: comments } - end - - def format(q) - q.group(0, "|", "|") do - doc = q.format(params) - RemoveBreaks.call(doc) - - if locals.any? - q.text("; ") - q.seplist(locals, -> { q.text(", ") }) { |local| q.format(local) } - end - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("block_var") - - q.breakable - q.pp(params) - - if locals.any? - q.breakable - q.group(2, "(", ")") { q.seplist(locals) { |local| q.pp(local) } } - end - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :block_var, - params: params, - locals: locals, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - - # :call-seq: - # on_block_var: (Params params, (nil | Array[Ident]) locals) -> BlockVar - def on_block_var(params, locals) - index = - tokens.rindex do |node| - node.is_a?(Op) && %w[| ||].include?(node.value) && - node.location.start_char < params.location.start_char - end + # :call-seq: + # on_block_var: (Params params, (nil | Array[Ident]) locals) -> BlockVar + def on_block_var(params, locals) + index = + tokens.rindex do |node| + node.is_a?(Op) && %w[| ||].include?(node.value) && + node.location.start_char < params.location.start_char + end beginning = tokens[index] ending = tokens[-1] @@ -2874,61 +825,6 @@ def on_block_var(params, locals) ) end - # BlockArg represents declaring a block parameter on a method definition. - # - # def method(&block); end - # - class BlockArg - # [nil | Ident] the name of the block argument - attr_reader :name - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(name:, location:, comments: []) - @name = name - @location = location - @comments = comments - end - - def child_nodes - [name] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { name: name, location: location, comments: comments } - end - - def format(q) - q.text("&") - q.format(name) if name - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("blockarg") - - if name - q.breakable - q.pp(name) - end - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { type: :blockarg, name: name, loc: location, cmts: comments }.to_json( - *opts - ) - end - end - # :call-seq: # on_blockarg: (Ident name) -> BlockArg def on_blockarg(name) @@ -2940,160 +836,6 @@ def on_blockarg(name) BlockArg.new(name: name, location: location) end - # bodystmt can't actually determine its bounds appropriately because it - # doesn't necessarily know where it started. So the parent node needs to - # report back down into this one where it goes. - class BodyStmt - # [Statements] the list of statements inside the begin clause - attr_reader :statements - - # [nil | Rescue] the optional rescue chain attached to the begin clause - attr_reader :rescue_clause - - # [nil | Statements] the optional set of statements inside the else clause - attr_reader :else_clause - - # [nil | Ensure] the optional ensure clause - attr_reader :ensure_clause - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize( - statements:, - rescue_clause:, - else_clause:, - ensure_clause:, - location:, - comments: [] - ) - @statements = statements - @rescue_clause = rescue_clause - @else_clause = else_clause - @ensure_clause = ensure_clause - @location = location - @comments = comments - end - - def bind(start_char, end_char) - @location = - Location.new( - start_line: location.start_line, - start_char: start_char, - end_line: location.end_line, - end_char: end_char - ) - - parts = [rescue_clause, else_clause, ensure_clause] - - # Here we're going to determine the bounds for the statements - consequent = parts.compact.first - statements.bind( - start_char, - consequent ? consequent.location.start_char : end_char - ) - - # Next we're going to determine the rescue clause if there is one - if rescue_clause - consequent = parts.drop(1).compact.first - rescue_clause.bind_end( - consequent ? consequent.location.start_char : end_char - ) - end - end - - def empty? - statements.empty? && !rescue_clause && !else_clause && !ensure_clause - end - - def child_nodes - [statements, rescue_clause, else_clause, ensure_clause] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - statements: statements, - rescue_clause: rescue_clause, - else_clause: else_clause, - ensure_clause: ensure_clause, - location: location, - comments: comments - } - end - - def format(q) - q.group do - q.format(statements) unless statements.empty? - - if rescue_clause - q.nest(-2) do - q.breakable(force: true) - q.format(rescue_clause) - end - end - - if else_clause - q.nest(-2) do - q.breakable(force: true) - q.text("else") - end - q.breakable(force: true) - q.format(else_clause) - end - - if ensure_clause - q.nest(-2) do - q.breakable(force: true) - q.format(ensure_clause) - end - end - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("bodystmt") - - q.breakable - q.pp(statements) - - if rescue_clause - q.breakable - q.pp(rescue_clause) - end - - if else_clause - q.breakable - q.pp(else_clause) - end - - if ensure_clause - q.breakable - q.pp(ensure_clause) - end - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :bodystmt, - stmts: statements, - rsc: rescue_clause, - els: else_clause, - ens: ensure_clause, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_bodystmt: ( # Statements statements, @@ -3111,241 +853,6 @@ def on_bodystmt(statements, rescue_clause, else_clause, ensure_clause) ) end - # Responsible for formatting either a BraceBlock or a DoBlock. - class BlockFormatter - class BlockOpenFormatter - # [String] the actual output that should be printed - attr_reader :text - - # [LBrace | Keyword] the node that is being represented - attr_reader :node - - def initialize(text, node) - @text = text - @node = node - end - - def comments - node.comments - end - - def format(q) - q.text(text) - end - end - - # [BraceBlock | DoBlock] the block node to be formatted - attr_reader :node - - # [LBrace | Keyword] the node that opens the block - attr_reader :block_open - - # [String] the string that closes the block - attr_reader :block_close - - # [BodyStmt | Statements] the statements inside the block - attr_reader :statements - - def initialize(node, block_open, block_close, statements) - @node = node - @block_open = block_open - @block_close = block_close - @statements = statements - end - - def format(q) - # If this is nested anywhere inside of a Command or CommandCall node, then - # we can't change which operators we're using for the bounds of the block. - break_opening, break_closing, flat_opening, flat_closing = - if unchangeable_bounds?(q) - [block_open.value, block_close, block_open.value, block_close] - elsif forced_do_end_bounds?(q) - %w[do end do end] - elsif forced_brace_bounds?(q) - %w[{ } { }] - else - %w[do end { }] - end - - # If the receiver of this block a Command or CommandCall node, then there - # are no parentheses around the arguments to that command, so we need to - # break the block. - receiver = q.parent.call - if receiver.is_a?(Command) || receiver.is_a?(CommandCall) - q.break_parent - format_break(q, break_opening, break_closing) - return - end - - q.group do - q.if_break { format_break(q, break_opening, break_closing) }.if_flat do - format_flat(q, flat_opening, flat_closing) - end - end - end - - private - - # If this is nested anywhere inside certain nodes, then we can't change - # which operators/keywords we're using for the bounds of the block. - def unchangeable_bounds?(q) - q.parents.any? do |parent| - # If we hit a statements, then we're safe to use whatever since we - # know for certain we're going to get split over multiple lines - # anyway. - break false if parent.is_a?(Statements) - - [Command, CommandCall].include?(parent.class) - end - end - - # If we're a sibling of a control-flow keyword, then we're going to have to - # use the do..end bounds. - def forced_do_end_bounds?(q) - [Break, Next, Return, Super].include?(q.parent.call.class) - end - - # If we're the predicate of a loop or conditional, then we're going to have - # to go with the {..} bounds. - def forced_brace_bounds?(q) - parents = q.parents.to_a - parents.each_with_index.any? do |parent, index| - # If we hit certain breakpoints then we know we're safe. - break false if [Paren, Statements].include?(parent.class) - - [ - If, - IfMod, - IfOp, - Unless, - UnlessMod, - While, - WhileMod, - Until, - UntilMod - ].include?(parent.class) && parent.predicate == parents[index - 1] - end - end - - def format_break(q, opening, closing) - q.text(" ") - q.format(BlockOpenFormatter.new(opening, block_open), stackable: false) - - if node.block_var - q.text(" ") - q.format(node.block_var) - end - - unless statements.empty? - q.indent do - q.breakable - q.format(statements) - end - end - - q.breakable - q.text(closing) - end - - def format_flat(q, opening, closing) - q.text(" ") - q.format(BlockOpenFormatter.new(opening, block_open), stackable: false) - - if node.block_var - q.breakable - q.format(node.block_var) - q.breakable - end - - if statements.empty? - q.text(" ") if opening == "do" - else - q.breakable unless node.block_var - q.format(statements) - q.breakable - end - - q.text(closing) - end - end - - # BraceBlock represents passing a block to a method call using the { } - # operators. - # - # method { |variable| variable + 1 } - # - class BraceBlock - # [LBrace] the left brace that opens this block - attr_reader :lbrace - - # [nil | BlockVar] the optional set of parameters to the block - attr_reader :block_var - - # [Statements] the list of expressions to evaluate within the block - attr_reader :statements - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(lbrace:, block_var:, statements:, location:, comments: []) - @lbrace = lbrace - @block_var = block_var - @statements = statements - @location = location - @comments = comments - end - - def child_nodes - [lbrace, block_var, statements] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - lbrace: lbrace, - block_var: block_var, - statements: statements, - location: location, - comments: comments - } - end - - def format(q) - BlockFormatter.new(self, lbrace, "}", statements).format(q) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("brace_block") - - if block_var - q.breakable - q.pp(block_var) - end - - q.breakable - q.pp(statements) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :brace_block, - lbrace: lbrace, - block_var: block_var, - stmts: statements, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_brace_block: ( # (nil | BlockVar) block_var, @@ -3376,113 +883,6 @@ def on_brace_block(block_var, statements) ) end - # Formats either a Break or Next node. - class FlowControlFormatter - # [String] the keyword to print - attr_reader :keyword - - # [Break | Next] the node being formatted - attr_reader :node - - def initialize(keyword, node) - @keyword = keyword - @node = node - end - - def format(q) - arguments = node.arguments - - q.group do - q.text(keyword) - - if arguments.parts.any? - if arguments.parts.length == 1 - part = arguments.parts.first - - if part.is_a?(Paren) - q.format(arguments) - elsif part.is_a?(ArrayLiteral) - q.text(" ") - q.format(arguments) - else - format_arguments(q, "(", ")") - end - else - format_arguments(q, " [", "]") - end - end - end - end - - private - - def format_arguments(q, opening, closing) - q.if_break { q.text(opening) } - q.indent do - q.breakable(" ") - q.format(node.arguments) - end - q.breakable("") - q.if_break { q.text(closing) } - end - end - - # Break represents using the +break+ keyword. - # - # break - # - # It can also optionally accept arguments, as in: - # - # break 1 - # - class Break - # [Args] the arguments being sent to the keyword - attr_reader :arguments - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(arguments:, location:, comments: []) - @arguments = arguments - @location = location - @comments = comments - end - - def child_nodes - [arguments] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { arguments: arguments, location: location, comments: comments } - end - - def format(q) - FlowControlFormatter.new("break", self).format(q) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("break") - - q.breakable - q.pp(arguments) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { type: :break, args: arguments, loc: location, cmts: comments }.to_json( - *opts - ) - end - end - # :call-seq: # on_break: (Args arguments) -> Break def on_break(arguments) @@ -3494,153 +894,6 @@ def on_break(arguments) Break.new(arguments: arguments, location: location) end - # Wraps a call operator (which can be a string literal :: or an Op node or a - # Period node) and formats it when called. - class CallOperatorFormatter - # [:"::" | Op | Period] the operator being formatted - attr_reader :operator - - def initialize(operator) - @operator = operator - end - - def comments - operator == :"::" ? [] : operator.comments - end - - def format(q) - if operator == :"::" || (operator.is_a?(Op) && operator.value == "::") - q.text(".") - else - operator.format(q) - end - end - end - - # Call represents a method call. - # - # receiver.message - # - class Call - # [untyped] the receiver of the method call - attr_reader :receiver - - # [:"::" | Op | Period] the operator being used to send the message - attr_reader :operator - - # [:call | Backtick | Const | Ident | Op] the message being sent - attr_reader :message - - # [nil | ArgParen | Args] the arguments to the method call - attr_reader :arguments - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize( - receiver:, - operator:, - message:, - arguments:, - location:, - comments: [] - ) - @receiver = receiver - @operator = operator - @message = message - @arguments = arguments - @location = location - @comments = comments - end - - def child_nodes - [ - receiver, - (operator if operator != :"::"), - (message if message != :call), - arguments - ] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - receiver: receiver, - operator: operator, - message: message, - arguments: arguments, - location: location, - comments: comments - } - end - - def format(q) - call_operator = CallOperatorFormatter.new(operator) - - q.group do - q.format(receiver) - - # If there are trailing comments on the call operator, then we need to - # use the trailing form as opposed to the leading form. - q.format(call_operator) if call_operator.comments.any? - - q.group do - q.indent do - if receiver.comments.any? || call_operator.comments.any? - q.breakable(force: true) - end - - if call_operator.comments.empty? - q.format(call_operator, stackable: false) - end - - q.format(message) if message != :call - end - - q.format(arguments) if arguments - end - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("call") - - q.breakable - q.pp(receiver) - - q.breakable - q.pp(operator) - - q.breakable - q.pp(message) - - if arguments - q.breakable - q.pp(arguments) - end - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :call, - receiver: receiver, - op: operator, - message: message, - args: arguments, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_call: ( # untyped receiver, @@ -3660,193 +913,6 @@ def on_call(receiver, operator, message) ) end - # Case represents the beginning of a case chain. - # - # case value - # when 1 - # "one" - # when 2 - # "two" - # else - # "number" - # end - # - class Case - # [Kw] the keyword that opens this expression - attr_reader :keyword - - # [nil | untyped] optional value being switched on - attr_reader :value - - # [In | When] the next clause in the chain - attr_reader :consequent - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(keyword:, value:, consequent:, location:, comments: []) - @keyword = keyword - @value = value - @consequent = consequent - @location = location - @comments = comments - end - - def child_nodes - [keyword, value, consequent] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - keyword: keyword, - value: value, - consequent: consequent, - location: location, - comments: comments - } - end - - def format(q) - q.group do - q.format(keyword) - - if value - q.text(" ") - q.format(value) - end - - q.breakable(force: true) - q.format(consequent) - q.breakable(force: true) - - q.text("end") - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("case") - - q.breakable - q.pp(keyword) - - if value - q.breakable - q.pp(value) - end - - q.breakable - q.pp(consequent) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :case, - value: value, - cons: consequent, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - - # RAssign represents a single-line pattern match. - # - # value in pattern - # value => pattern - # - class RAssign - # [untyped] the left-hand expression - attr_reader :value - - # [Kw | Op] the operator being used to match against the pattern, which is - # either => or in - attr_reader :operator - - # [untyped] the pattern on the right-hand side of the expression - attr_reader :pattern - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(value:, operator:, pattern:, location:, comments: []) - @value = value - @operator = operator - @pattern = pattern - @location = location - @comments = comments - end - - def child_nodes - [value, operator, pattern] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - value: value, - operator: operator, - pattern: pattern, - location: location, - comments: comments - } - end - - def format(q) - q.group do - q.format(value) - q.text(" ") - q.format(operator) - q.group do - q.indent do - q.breakable - q.format(pattern) - end - end - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("rassign") - - q.breakable - q.pp(value) - - q.breakable - q.pp(operator) - - q.breakable - q.pp(pattern) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :rassign, - value: value, - op: operator, - pattern: pattern, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_case: (untyped value, untyped consequent) -> Case | RAssign def on_case(value, consequent) @@ -3871,144 +937,6 @@ def on_case(value, consequent) end end - # Class represents defining a class using the +class+ keyword. - # - # class Container - # end - # - # Classes can have path names as their class name in case it's being nested - # under a namespace, as in: - # - # class Namespace::Container - # end - # - # Classes can also be defined as a top-level path, in the case that it's - # already in a namespace but you want to define it at the top-level instead, - # as in: - # - # module OtherNamespace - # class ::Namespace::Container - # end - # end - # - # All of these declarations can also have an optional superclass reference, as - # in: - # - # class Child < Parent - # end - # - # That superclass can actually be any Ruby expression, it doesn't necessarily - # need to be a constant, as in: - # - # class Child < method - # end - # - class ClassDeclaration - # [ConstPathRef | ConstRef | TopConstRef] the name of the class being - # defined - attr_reader :constant - - # [nil | untyped] the optional superclass declaration - attr_reader :superclass - - # [BodyStmt] the expressions to execute within the context of the class - attr_reader :bodystmt - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(constant:, superclass:, bodystmt:, location:, comments: []) - @constant = constant - @superclass = superclass - @bodystmt = bodystmt - @location = location - @comments = comments - end - - def child_nodes - [constant, superclass, bodystmt] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - constant: constant, - superclass: superclass, - bodystmt: bodystmt, - location: location, - comments: comments - } - end - - def format(q) - declaration = -> do - q.group do - q.text("class ") - q.format(constant) - - if superclass - q.text(" < ") - q.format(superclass) - end - end - end - - if bodystmt.empty? - q.group do - declaration.call - q.breakable(force: true) - q.text("end") - end - else - q.group do - declaration.call - - q.indent do - q.breakable(force: true) - q.format(bodystmt) - end - - q.breakable(force: true) - q.text("end") - end - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("class") - - q.breakable - q.pp(constant) - - if superclass - q.breakable - q.pp(superclass) - end - - q.breakable - q.pp(bodystmt) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :class, - constant: constant, - superclass: superclass, - bodystmt: bodystmt, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_class: ( # (ConstPathRef | ConstRef | TopConstRef) constant, @@ -4032,20 +960,6 @@ def on_class(constant, superclass, bodystmt) ) end - # Comma represents the use of the , operator. - class Comma - # [String] the comma in the string - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - def initialize(value:, location:) - @value = value - @location = location - end - end - # :call-seq: # on_comma: (String value) -> Comma def on_comma(value) @@ -4059,80 +973,6 @@ def on_comma(value) node end - # Command represents a method call with arguments and no parentheses. Note - # that Command nodes only happen when there is no explicit receiver for this - # method. - # - # method argument - # - class Command - # [Const | Ident] the message being sent to the implicit receiver - attr_reader :message - - # [Args] the arguments being sent with the message - attr_reader :arguments - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(message:, arguments:, location:, comments: []) - @message = message - @arguments = arguments - @location = location - @comments = comments - end - - def child_nodes - [message, arguments] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - message: message, - arguments: arguments, - location: location, - comments: comments - } - end - - def format(q) - q.group do - q.format(message) - q.text(" ") - q.nest(message.value.length + 1) { q.format(arguments) } - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("command") - - q.breakable - q.pp(message) - - q.breakable - q.pp(arguments) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :command, - message: message, - args: arguments, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_command: ((Const | Ident) message, Args arguments) -> Command def on_command(message, arguments) @@ -4143,163 +983,6 @@ def on_command(message, arguments) ) end - # CommandCall represents a method call on an object with arguments and no - # parentheses. - # - # object.method argument - # - class CommandCall - # [untyped] the receiver of the message - attr_reader :receiver - - # [:"::" | Op | Period] the operator used to send the message - attr_reader :operator - - # [Const | Ident | Op] the message being send - attr_reader :message - - # [nil | Args] the arguments going along with the message - attr_reader :arguments - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize( - receiver:, - operator:, - message:, - arguments:, - location:, - comments: [] - ) - @receiver = receiver - @operator = operator - @message = message - @arguments = arguments - @location = location - @comments = comments - end - - def child_nodes - [receiver, message, arguments] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - receiver: receiver, - operator: operator, - message: message, - arguments: arguments, - location: location, - comments: comments - } - end - - def format(q) - q.group do - doc = - q.nest(0) do - q.format(receiver) - q.format(CallOperatorFormatter.new(operator), stackable: false) - q.format(message) - end - - if arguments - q.text(" ") - q.nest(argument_alignment(q, doc)) { q.format(arguments) } - end - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("command_call") - - q.breakable - q.pp(receiver) - - q.breakable - q.pp(operator) - - q.breakable - q.pp(message) - - if arguments - q.breakable - q.pp(arguments) - end - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :command_call, - receiver: receiver, - op: operator, - message: message, - args: arguments, - loc: location, - cmts: comments - }.to_json(*opts) - end - - private - - # This is a somewhat naive method that is attempting to sum up the width of - # the doc nodes that make up the given doc node. This is used to align - # content. - def doc_width(parent) - queue = [parent] - width = 0 - - until queue.empty? - doc = queue.shift - - case doc - when PrettyPrint::Text - width += doc.width - when PrettyPrint::Indent, PrettyPrint::Align, PrettyPrint::Group - queue = doc.contents + queue - when PrettyPrint::IfBreak - queue = doc.break_contents + queue - when PrettyPrint::Breakable - width = 0 - end - end - - width - end - - def argument_alignment(q, doc) - # Very special handling case for rspec matchers. In general with rspec - # matchers you expect to see something like: - # - # expect(foo).to receive(:bar).with( - # 'one', - # 'two', - # 'three', - # 'four', - # 'five' - # ) - # - # In this case the arguments are aligned to the left side as opposed to - # being aligned with the `receive` call. - if %w[to not_to to_not].include?(message.value) - 0 - else - width = doc_width(doc) + 1 - width > (q.maxwidth / 2) ? 0 : width - end - end - end - # :call-seq: # on_command_call: ( # untyped receiver, @@ -4319,104 +1002,6 @@ def on_command_call(receiver, operator, message, arguments) ) end - # Comment represents a comment in the source. - # - # # comment - # - class Comment - class List - # [Array[ Comment ]] the list of comments this list represents - attr_reader :comments - - def initialize(comments) - @comments = comments - end - - def pretty_print(q) - return if comments.empty? - - q.breakable - q.group(2, "(", ")") { q.seplist(comments) { |comment| q.pp(comment) } } - end - end - - # [String] the contents of the comment - attr_reader :value - - # [boolean] whether or not there is code on the same line as this comment. - # If there is, then inline will be true. - attr_reader :inline - alias inline? inline - - # [Location] the location of this node - attr_reader :location - - def initialize(value:, inline:, location:) - @value = value - @inline = inline - @location = location - - @leading = false - @trailing = false - end - - def leading! - @leading = true - end - - def leading? - @leading - end - - def trailing! - @trailing = true - end - - def trailing? - @trailing - end - - def ignore? - value[1..-1].strip == "stree-ignore" - end - - def comments - [] - end - - def child_nodes - [] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { value: value, inline: inline, location: location } - end - - def format(q) - q.text(value) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("comment") - - q.breakable - q.pp(value) - end - end - - def to_json(*opts) - { - type: :comment, - value: value.force_encoding("UTF-8"), - inline: inline, - loc: location - }.to_json(*opts) - end - end - # :call-seq: # on_comment: (String value) -> Comment def on_comment(value) @@ -4433,68 +1018,6 @@ def on_comment(value) comment end - # Const represents a literal value that _looks_ like a constant. This could - # actually be a reference to a constant: - # - # Constant - # - # It could also be something that looks like a constant in another context, as - # in a method call to a capitalized method: - # - # object.Constant - # - # or a symbol that starts with a capital letter: - # - # :Constant - # - class Const - # [String] the name of the constant - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(value:, location:, comments: []) - @value = value - @location = location - @comments = comments - end - - def child_nodes - [] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { value: value, location: location, comments: comments } - end - - def format(q) - q.text(value) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("const") - - q.breakable - q.pp(value) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { type: :const, value: value, loc: location, cmts: comments }.to_json( - *opts - ) - end - end - # :call-seq: # on_const: (String value) -> Const def on_const(value) @@ -4504,78 +1027,6 @@ def on_const(value) ) end - # ConstPathField represents the child node of some kind of assignment. It - # represents when you're assigning to a constant that is being referenced as - # a child of another variable. - # - # object::Const = value - # - class ConstPathField - # [untyped] the source of the constant - attr_reader :parent - - # [Const] the constant itself - attr_reader :constant - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(parent:, constant:, location:, comments: []) - @parent = parent - @constant = constant - @location = location - @comments = comments - end - - def child_nodes - [parent, constant] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - parent: parent, - constant: constant, - location: location, - comments: comments - } - end - - def format(q) - q.format(parent) - q.text("::") - q.format(constant) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("const_path_field") - - q.breakable - q.pp(parent) - - q.breakable - q.pp(constant) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :const_path_field, - parent: parent, - constant: constant, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_const_path_field: (untyped parent, Const constant) -> ConstPathField def on_const_path_field(parent, constant) @@ -4586,76 +1037,6 @@ def on_const_path_field(parent, constant) ) end - # ConstPathRef represents referencing a constant by a path. - # - # object::Const - # - class ConstPathRef - # [untyped] the source of the constant - attr_reader :parent - - # [Const] the constant itself - attr_reader :constant - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(parent:, constant:, location:, comments: []) - @parent = parent - @constant = constant - @location = location - @comments = comments - end - - def child_nodes - [parent, constant] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - parent: parent, - constant: constant, - location: location, - comments: comments - } - end - - def format(q) - q.format(parent) - q.text("::") - q.format(constant) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("const_path_ref") - - q.breakable - q.pp(parent) - - q.breakable - q.pp(constant) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :const_path_ref, - parent: parent, - constant: constant, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_const_path_ref: (untyped parent, Const constant) -> ConstPathRef def on_const_path_ref(parent, constant) @@ -4666,121 +1047,12 @@ def on_const_path_ref(parent, constant) ) end - # ConstRef represents the name of the constant being used in a class or module - # declaration. - # - # class Container - # end - # - class ConstRef - # [Const] the constant itself - attr_reader :constant - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(constant:, location:, comments: []) - @constant = constant - @location = location - @comments = comments - end - - def child_nodes - [constant] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { constant: constant, location: location, comments: comments } - end - - def format(q) - q.format(constant) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("const_ref") - - q.breakable - q.pp(constant) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :const_ref, - constant: constant, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_const_ref: (Const constant) -> ConstRef def on_const_ref(constant) ConstRef.new(constant: constant, location: constant.location) end - # CVar represents the use of a class variable. - # - # @@variable - # - class CVar - # [String] the name of the class variable - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(value:, location:, comments: []) - @value = value - @location = location - @comments = comments - end - - def child_nodes - [] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { value: value, location: location, comments: comments } - end - - def format(q) - q.text(value) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("cvar") - - q.breakable - q.pp(value) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { type: :cvar, value: value, loc: location, cmts: comments }.to_json( - *opts - ) - end - end - # :call-seq: # on_cvar: (String value) -> CVar def on_cvar(value) @@ -4790,227 +1062,6 @@ def on_cvar(value) ) end - # Def represents defining a regular method on the current self object. - # - # def method(param) result end - # - class Def - # [Backtick | Const | Ident | Kw | Op] the name of the method - attr_reader :name - - # [Params | Paren] the parameter declaration for the method - attr_reader :params - - # [BodyStmt] the expressions to be executed by the method - attr_reader :bodystmt - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(name:, params:, bodystmt:, location:, comments: []) - @name = name - @params = params - @bodystmt = bodystmt - @location = location - @comments = comments - end - - def child_nodes - [name, params, bodystmt] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - name: name, - params: params, - bodystmt: bodystmt, - location: location, - comments: comments - } - end - - def format(q) - q.group do - q.group do - q.text("def ") - q.format(name) - q.format(params) if !params.is_a?(Params) || !params.empty? - end - - unless bodystmt.empty? - q.indent do - q.breakable(force: true) - q.format(bodystmt) - end - end - - q.breakable(force: true) - q.text("end") - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("def") - - q.breakable - q.pp(name) - - q.breakable - q.pp(params) - - q.breakable - q.pp(bodystmt) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :def, - name: name, - params: params, - bodystmt: bodystmt, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - - # DefEndless represents defining a single-line method since Ruby 3.0+. - # - # def method = result - # - class DefEndless - # [untyped] the target where the method is being defined - attr_reader :target - - # [Op | Period] the operator being used to declare the method - attr_reader :operator - - # [Backtick | Const | Ident | Kw | Op] the name of the method - attr_reader :name - - # [nil | Params | Paren] the parameter declaration for the method - attr_reader :paren - - # [untyped] the expression to be executed by the method - attr_reader :statement - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize( - target:, - operator:, - name:, - paren:, - statement:, - location:, - comments: [] - ) - @target = target - @operator = operator - @name = name - @paren = paren - @statement = statement - @location = location - @comments = comments - end - - def child_nodes - [target, operator, name, paren, statement] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - target: target, - operator: operator, - name: name, - paren: paren, - statement: statement, - location: location, - comments: comments - } - end - - def format(q) - q.group do - q.text("def ") - - if target - q.format(target) - q.format(CallOperatorFormatter.new(operator), stackable: false) - end - - q.format(name) - - if paren - params = paren - params = params.contents if params.is_a?(Paren) - q.format(paren) unless params.empty? - end - - q.text(" =") - q.group do - q.indent do - q.breakable - q.format(statement) - end - end - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("def_endless") - - if target - q.breakable - q.pp(target) - - q.breakable - q.pp(operator) - end - - q.breakable - q.pp(name) - - if paren - q.breakable - q.pp(paren) - end - - q.breakable - q.pp(statement) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :def_endless, - name: name, - paren: paren, - stmt: statement, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_def: ( # (Backtick | Const | Ident | Kw | Op) name, @@ -5072,65 +1123,6 @@ def on_def(name, params, bodystmt) end end - # Defined represents the use of the +defined?+ operator. It can be used with - # and without parentheses. - # - # defined?(variable) - # - class Defined - # [untyped] the value being sent to the keyword - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(value:, location:, comments: []) - @value = value - @location = location - @comments = comments - end - - def child_nodes - [value] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { value: value, location: location, comments: comments } - end - - def format(q) - q.group(0, "defined?(", ")") do - q.indent do - q.breakable("") - q.format(value) - end - q.breakable("") - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("defined") - - q.breakable - q.pp(value) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { type: :defined, value: value, loc: location, cmts: comments }.to_json( - *opts - ) - end - end - # :call-seq: # on_defined: (untyped value) -> Defined def on_defined(value) @@ -5146,127 +1138,6 @@ def on_defined(value) Defined.new(value: value, location: beginning.location.to(ending.location)) end - # Defs represents defining a singleton method on an object. - # - # def object.method(param) result end - # - class Defs - # [untyped] the target where the method is being defined - attr_reader :target - - # [Op | Period] the operator being used to declare the method - attr_reader :operator - - # [Backtick | Const | Ident | Kw | Op] the name of the method - attr_reader :name - - # [Params | Paren] the parameter declaration for the method - attr_reader :params - - # [BodyStmt] the expressions to be executed by the method - attr_reader :bodystmt - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize( - target:, - operator:, - name:, - params:, - bodystmt:, - location:, - comments: [] - ) - @target = target - @operator = operator - @name = name - @params = params - @bodystmt = bodystmt - @location = location - @comments = comments - end - - def child_nodes - [target, operator, name, params, bodystmt] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - target: target, - operator: operator, - name: name, - params: params, - bodystmt: bodystmt, - location: location, - comments: comments - } - end - - def format(q) - q.group do - q.group do - q.text("def ") - q.format(target) - q.format(CallOperatorFormatter.new(operator), stackable: false) - q.format(name) - q.format(params) if !params.is_a?(Params) || !params.empty? - end - - unless bodystmt.empty? - q.indent do - q.breakable(force: true) - q.format(bodystmt) - end - end - - q.breakable(force: true) - q.text("end") - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("defs") - - q.breakable - q.pp(target) - - q.breakable - q.pp(operator) - - q.breakable - q.pp(name) - - q.breakable - q.pp(params) - - q.breakable - q.pp(bodystmt) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :defs, - target: target, - op: operator, - name: name, - params: params, - bodystmt: bodystmt, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_defs: ( # untyped target, @@ -5330,84 +1201,6 @@ def on_defs(target, operator, name, params, bodystmt) end end - # DoBlock represents passing a block to a method call using the +do+ and +end+ - # keywords. - # - # method do |value| - # end - # - class DoBlock - # [Kw] the do keyword that opens this block - attr_reader :keyword - - # [nil | BlockVar] the optional variable declaration within this block - attr_reader :block_var - - # [BodyStmt] the expressions to be executed within this block - attr_reader :bodystmt - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(keyword:, block_var:, bodystmt:, location:, comments: []) - @keyword = keyword - @block_var = block_var - @bodystmt = bodystmt - @location = location - @comments = comments - end - - def child_nodes - [keyword, block_var, bodystmt] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - keyword: keyword, - block_var: block_var, - bodystmt: bodystmt, - location: location, - comments: comments - } - end - - def format(q) - BlockFormatter.new(self, keyword, "end", bodystmt).format(q) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("do_block") - - if block_var - q.breakable - q.pp(block_var) - end - - q.breakable - q.pp(bodystmt) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :do_block, - keyword: keyword, - block_var: block_var, - bodystmt: bodystmt, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_do_block: (BlockVar block_var, BodyStmt bodystmt) -> DoBlock def on_do_block(block_var, bodystmt) @@ -5427,107 +1220,6 @@ def on_do_block(block_var, bodystmt) ) end - # Responsible for formatting Dot2 and Dot3 nodes. - class DotFormatter - # [String] the operator to display - attr_reader :operator - - # [Dot2 | Dot3] the node that is being formatter - attr_reader :node - - def initialize(operator, node) - @operator = operator - @node = node - end - - def format(q) - space = [If, IfMod, Unless, UnlessMod].include?(q.parent.class) - - left = node.left - right = node.right - - q.format(left) if left - q.text(" ") if space - q.text(operator) - q.text(" ") if space - q.format(right) if right - end - end - - # Dot2 represents using the .. operator between two expressions. Usually this - # is to create a range object. - # - # 1..2 - # - # Sometimes this operator is used to create a flip-flop. - # - # if value == 5 .. value == 10 - # end - # - # One of the sides of the expression may be nil, but not both. - class Dot2 - # [nil | untyped] the left side of the expression - attr_reader :left - - # [nil | untyped] the right side of the expression - attr_reader :right - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(left:, right:, location:, comments: []) - @left = left - @right = right - @location = location - @comments = comments - end - - def child_nodes - [left, right] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { left: left, right: right, location: location, comments: comments } - end - - def format(q) - DotFormatter.new("..", self).format(q) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("dot2") - - if left - q.breakable - q.pp(left) - end - - if right - q.breakable - q.pp(right) - end - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :dot2, - left: left, - right: right, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_dot2: ((nil | untyped) left, (nil | untyped) right) -> Dot2 def on_dot2(left, right) @@ -5543,81 +1235,6 @@ def on_dot2(left, right) ) end - # Dot3 represents using the ... operator between two expressions. Usually this - # is to create a range object. It's effectively the same event as the Dot2 - # node but with this operator you're asking Ruby to omit the final value. - # - # 1...2 - # - # Like Dot2 it can also be used to create a flip-flop. - # - # if value == 5 ... value == 10 - # end - # - # One of the sides of the expression may be nil, but not both. - class Dot3 - # [nil | untyped] the left side of the expression - attr_reader :left - - # [nil | untyped] the right side of the expression - attr_reader :right - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(left:, right:, location:, comments: []) - @left = left - @right = right - @location = location - @comments = comments - end - - def child_nodes - [left, right] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { left: left, right: right, location: location, comments: comments } - end - - def format(q) - DotFormatter.new("...", self).format(q) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("dot3") - - if left - q.breakable - q.pp(left) - end - - if right - q.breakable - q.pp(right) - end - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :dot3, - left: left, - right: right, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_dot3: ((nil | untyped) left, (nil | untyped) right) -> Dot3 def on_dot3(left, right) @@ -5633,166 +1250,6 @@ def on_dot3(left, right) ) end - # Responsible for providing information about quotes to be used for strings - # and dynamic symbols. - module Quotes - # The matching pairs of quotes that can be used with % literals. - PAIRS = { "(" => ")", "[" => "]", "{" => "}", "<" => ">" }.freeze - - # If there is some part of this string that matches an escape sequence or - # that contains the interpolation pattern ("#{"), then we are locked into - # whichever quote the user chose. (If they chose single quotes, then double - # quoting would activate the escape sequence, and if they chose double - # quotes, then single quotes would deactivate it.) - def self.locked?(node) - node.parts.any? do |part| - part.is_a?(TStringContent) && part.value.match?(/#[@${]|[\\]/) - end - end - - # Find the matching closing quote for the given opening quote. - def self.matching(quote) - PAIRS.fetch(quote) { quote } - end - - # Escape and unescape single and double quotes as needed to be able to - # enclose +content+ with +enclosing+. - def self.normalize(content, enclosing) - return content if enclosing != "\"" && enclosing != "'" - - content.gsub(/\\([\s\S])|(['"])/) do - _match, escaped, quote = Regexp.last_match.to_a - - if quote == enclosing - "\\#{quote}" - elsif quote - quote - else - "\\#{escaped}" - end - end - end - end - - # DynaSymbol represents a symbol literal that uses quotes to dynamically - # define its value. - # - # :"#{variable}" - # - # They can also be used as a special kind of dynamic hash key, as in: - # - # { "#{key}": value } - # - class DynaSymbol - # [Array[ StringDVar | StringEmbExpr | TStringContent ]] the parts of the - # dynamic symbol - attr_reader :parts - - # [String] the quote used to delimit the dynamic symbol - attr_reader :quote - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(parts:, quote:, location:, comments: []) - @parts = parts - @quote = quote - @location = location - @comments = comments - end - - def child_nodes - parts - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { parts: parts, quote: quote, location: location, comments: comments } - end - - def format(q) - opening_quote, closing_quote = quotes(q) - - q.group(0, opening_quote, closing_quote) do - parts.each do |part| - if part.is_a?(TStringContent) - value = Quotes.normalize(part.value, closing_quote) - separator = -> { q.breakable(force: true, indent: false) } - q.seplist(value.split(/\r?\n/, -1), separator) do |text| - q.text(text) - end - else - q.format(part) - end - end - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("dyna_symbol") - - q.breakable - q.group(2, "(", ")") { q.seplist(parts) { |part| q.pp(part) } } - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :dyna_symbol, - parts: parts, - quote: quote, - loc: location, - cmts: comments - }.to_json(*opts) - end - - private - - # Here we determine the quotes to use for a dynamic symbol. It's bound by a - # lot of rules because it could be in many different contexts with many - # different kinds of escaping. - def quotes(q) - # If we're inside of an assoc node as the key, then it will handle - # printing the : on its own since it could change sides. - parent = q.parent - hash_key = parent.is_a?(Assoc) && parent.key == self - - if quote.start_with?("%s") - # Here we're going to check if there is a closing character, a new line, - # or a quote in the content of the dyna symbol. If there is, then - # quoting could get weird, so just bail out and stick to the original - # quotes in the source. - matching = Quotes.matching(quote[2]) - pattern = /[\n#{Regexp.escape(matching)}'"]/ - - if parts.any? { |part| - part.is_a?(TStringContent) && part.value.match?(pattern) - } - [quote, matching] - elsif Quotes.locked?(self) - ["#{":" unless hash_key}'", "'"] - else - ["#{":" unless hash_key}#{q.quote}", q.quote] - end - elsif Quotes.locked?(self) - if quote.start_with?(":") - [hash_key ? quote[1..-1] : quote, quote[1..-1]] - else - [hash_key ? quote : ":#{quote}", quote] - end - else - [hash_key ? q.quote : ":#{q.quote}", q.quote] - end - end - end - # :call-seq: # on_dyna_symbol: (StringContent string_content) -> DynaSymbol def on_dyna_symbol(string_content) @@ -5819,69 +1276,6 @@ def on_dyna_symbol(string_content) end end - # Else represents the end of an +if+, +unless+, or +case+ chain. - # - # if variable - # else - # end - # - class Else - # [Statements] the expressions to be executed - attr_reader :statements - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(statements:, location:, comments: []) - @statements = statements - @location = location - @comments = comments - end - - def child_nodes - [statements] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { statements: statements, location: location, comments: comments } - end - - def format(q) - q.group do - q.text("else") - - unless statements.empty? - q.indent do - q.breakable(force: true) - q.format(statements) - end - end - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("else") - - q.breakable - q.pp(statements) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { type: :else, stmts: statements, loc: location, cmts: comments }.to_json( - *opts - ) - end - end - # :call-seq: # on_else: (Statements statements) -> Else def on_else(statements) @@ -5907,112 +1301,6 @@ def on_else(statements) ) end - # Elsif represents another clause in an +if+ or +unless+ chain. - # - # if variable - # elsif other_variable - # end - # - class Elsif - # [untyped] the expression to be checked - attr_reader :predicate - - # [Statements] the expressions to be executed - attr_reader :statements - - # [nil | Elsif | Else] the next clause in the chain - attr_reader :consequent - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize( - predicate:, - statements:, - consequent:, - location:, - comments: [] - ) - @predicate = predicate - @statements = statements - @consequent = consequent - @location = location - @comments = comments - end - - def child_nodes - [predicate, statements, consequent] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - predicate: predicate, - statements: statements, - consequent: consequent, - location: location, - comments: comments - } - end - - def format(q) - q.group do - q.group do - q.text("elsif ") - q.nest("elsif".length - 1) { q.format(predicate) } - end - - unless statements.empty? - q.indent do - q.breakable(force: true) - q.format(statements) - end - end - - if consequent - q.group do - q.breakable(force: true) - q.format(consequent) - end - end - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("elsif") - - q.breakable - q.pp(predicate) - - q.breakable - q.pp(statements) - - if consequent - q.breakable - q.pp(consequent) - end - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :elsif, - pred: predicate, - stmts: statements, - cons: consequent, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_elsif: ( # untyped predicate, @@ -6033,66 +1321,6 @@ def on_elsif(predicate, statements, consequent) ) end - # EmbDoc represents a multi-line comment. - # - # =begin - # first line - # second line - # =end - # - class EmbDoc - # [String] the contents of the comment - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - def initialize(value:, location:) - @value = value - @location = location - end - - def inline? - false - end - - def ignore? - false - end - - def comments - [] - end - - def child_nodes - [] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { value: value, location: location } - end - - def format(q) - q.trim - q.text(value) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("embdoc") - - q.breakable - q.pp(value) - end - end - - def to_json(*opts) - { type: :embdoc, value: value, loc: location }.to_json(*opts) - end - end - # :call-seq: # on_embdoc: (String value) -> EmbDoc def on_embdoc(value) @@ -6132,25 +1360,6 @@ def on_embdoc_end(value) embdoc end - # EmbExprBeg represents the beginning token for using interpolation inside of - # a parent node that accepts string content (like a string or regular - # expression). - # - # "Hello, #{person}!" - # - class EmbExprBeg - # [String] the #{ used in the string - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - def initialize(value:, location:) - @value = value - @location = location - end - end - # :call-seq: # on_embexpr_beg: (String value) -> EmbExprBeg def on_embexpr_beg(value) @@ -6164,25 +1373,6 @@ def on_embexpr_beg(value) node end - # EmbExprEnd represents the ending token for using interpolation inside of a - # parent node that accepts string content (like a string or regular - # expression). - # - # "Hello, #{person}!" - # - class EmbExprEnd - # [String] the } used in the string - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - def initialize(value:, location:) - @value = value - @location = location - end - end - # :call-seq: # on_embexpr_end: (String value) -> EmbExprEnd def on_embexpr_end(value) @@ -6196,27 +1386,6 @@ def on_embexpr_end(value) node end - # EmbVar represents the use of shorthand interpolation for an instance, class, - # or global variable into a parent node that accepts string content (like a - # string or regular expression). - # - # "#@variable" - # - # In the example above, an EmbVar node represents the # because it forces - # @variable to be interpolated. - class EmbVar - # [String] the # used in the string - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - def initialize(value:, location:) - @value = value - @location = location - end - end - # :call-seq: # on_embvar: (String value) -> EmbVar def on_embvar(value) @@ -6230,81 +1399,6 @@ def on_embvar(value) node end - # Ensure represents the use of the +ensure+ keyword and its subsequent - # statements. - # - # begin - # ensure - # end - # - class Ensure - # [Kw] the ensure keyword that began this node - attr_reader :keyword - - # [Statements] the expressions to be executed - attr_reader :statements - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(keyword:, statements:, location:, comments: []) - @keyword = keyword - @statements = statements - @location = location - @comments = comments - end - - def child_nodes - [keyword, statements] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - keyword: keyword, - statements: statements, - location: location, - comments: comments - } - end - - def format(q) - q.format(keyword) - - unless statements.empty? - q.indent do - q.breakable(force: true) - q.format(statements) - end - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("ensure") - - q.breakable - q.pp(statements) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :ensure, - keyword: keyword, - stmts: statements, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_ensure: (Statements statements) -> Ensure def on_ensure(statements) @@ -6325,67 +1419,6 @@ def on_ensure(statements) ) end - # ExcessedComma represents a trailing comma in a list of block parameters. It - # changes the block parameters such that they will destructure. - # - # [[1, 2, 3], [2, 3, 4]].each do |first, second,| - # end - # - # In the above example, an ExcessedComma node would appear in the third - # position of the Params node that is used to declare that block. The third - # position typically represents a rest-type parameter, but in this case is - # used to indicate that a trailing comma was used. - class ExcessedComma - # [String] the comma - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(value:, location:, comments: []) - @value = value - @location = location - @comments = comments - end - - def child_nodes - [] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { value: value, location: location, comments: comments } - end - - def format(q) - q.text(value) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("excessed_comma") - - q.breakable - q.pp(value) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :excessed_comma, - value: value, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # The handler for this event accepts no parameters (though in previous # versions of Ruby it accepted a string literal with a value of ","). # @@ -6397,168 +1430,12 @@ def on_excessed_comma(*) ExcessedComma.new(value: comma.value, location: comma.location) end - # FCall represents the piece of a method call that comes before any arguments - # (i.e., just the name of the method). It is used in places where the parser - # is sure that it is a method call and not potentially a local variable. - # - # method(argument) - # - # In the above example, it's referring to the +method+ segment. - class FCall - # [Const | Ident] the name of the method - attr_reader :value - - # [nil | ArgParen | Args] the arguments to the method call - attr_reader :arguments - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(value:, arguments:, location:, comments: []) - @value = value - @arguments = arguments - @location = location - @comments = comments - end - - def child_nodes - [value, arguments] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - value: value, - arguments: arguments, - location: location, - comments: comments - } - end - - def format(q) - q.format(value) - q.format(arguments) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("fcall") - - q.breakable - q.pp(value) - - if arguments - q.breakable - q.pp(arguments) - end - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :fcall, - value: value, - args: arguments, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_fcall: ((Const | Ident) value) -> FCall def on_fcall(value) FCall.new(value: value, arguments: nil, location: value.location) end - # Field is always the child of an assignment. It represents assigning to a - # “field” on an object. - # - # object.variable = value - # - class Field - # [untyped] the parent object that owns the field being assigned - attr_reader :parent - - # [:"::" | Op | Period] the operator being used for the assignment - attr_reader :operator - - # [Const | Ident] the name of the field being assigned - attr_reader :name - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(parent:, operator:, name:, location:, comments: []) - @parent = parent - @operator = operator - @name = name - @location = location - @comments = comments - end - - def child_nodes - [parent, (operator if operator != :"::"), name] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - parent: parent, - operator: operator, - name: name, - location: location, - comments: comments - } - end - - def format(q) - q.group do - q.format(parent) - q.format(CallOperatorFormatter.new(operator), stackable: false) - q.format(name) - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("field") - - q.breakable - q.pp(parent) - - q.breakable - q.pp(operator) - - q.breakable - q.pp(name) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :field, - parent: parent, - op: operator, - name: name, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_field: ( # untyped parent, @@ -6574,58 +1451,6 @@ def on_field(parent, operator, name) ) end - # FloatLiteral represents a floating point number literal. - # - # 1.0 - # - class FloatLiteral - # [String] the value of the floating point number literal - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(value:, location:, comments: []) - @value = value - @location = location - @comments = comments - end - - def child_nodes - [] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { value: value, location: location, comments: comments } - end - - def format(q) - q.text(value) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("float") - - q.breakable - q.pp(value) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { type: :float, value: value, loc: location, cmts: comments }.to_json( - *opts - ) - end - end - # :call-seq: # on_float: (String value) -> FloatLiteral def on_float(value) @@ -6635,109 +1460,6 @@ def on_float(value) ) end - # FndPtn represents matching against a pattern where you find a pattern in an - # array using the Ruby 3.0+ pattern matching syntax. - # - # case value - # in [*, 7, *] - # end - # - class FndPtn - # [nil | untyped] the optional constant wrapper - attr_reader :constant - - # [VarField] the splat on the left-hand side - attr_reader :left - - # [Array[ untyped ]] the list of positional expressions in the pattern that - # are being matched - attr_reader :values - - # [VarField] the splat on the right-hand side - attr_reader :right - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(constant:, left:, values:, right:, location:, comments: []) - @constant = constant - @left = left - @values = values - @right = right - @location = location - @comments = comments - end - - def child_nodes - [constant, left, *values, right] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - constant: constant, - left: left, - values: values, - right: right, - location: location, - comments: comments - } - end - - def format(q) - q.format(constant) if constant - q.group(0, "[", "]") do - q.text("*") - q.format(left) - q.comma_breakable - - q.seplist(values) { |value| q.format(value) } - q.comma_breakable - - q.text("*") - q.format(right) - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("fndptn") - - if constant - q.breakable - q.pp(constant) - end - - q.breakable - q.pp(left) - - q.breakable - q.group(2, "(", ")") { q.seplist(values) { |value| q.pp(value) } } - - q.breakable - q.pp(right) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :fndptn, - constant: constant, - left: left, - values: values, - right: right, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_fndptn: ( # (nil | untyped) constant, @@ -6758,100 +1480,6 @@ def on_fndptn(constant, left, values, right) ) end - # For represents using a +for+ loop. - # - # for value in list do - # end - # - class For - # [MLHS | VarField] the variable declaration being used to - # pull values out of the object being enumerated - attr_reader :index - - # [untyped] the object being enumerated in the loop - attr_reader :collection - - # [Statements] the statements to be executed - attr_reader :statements - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(index:, collection:, statements:, location:, comments: []) - @index = index - @collection = collection - @statements = statements - @location = location - @comments = comments - end - - def child_nodes - [index, collection, statements] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - index: index, - collection: collection, - statements: statements, - location: location, - comments: comments - } - end - - def format(q) - q.group do - q.text("for ") - q.group { q.format(index) } - q.text(" in ") - q.format(collection) - - unless statements.empty? - q.indent do - q.breakable(force: true) - q.format(statements) - end - end - - q.breakable(force: true) - q.text("end") - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("for") - - q.breakable - q.pp(index) - - q.breakable - q.pp(collection) - - q.breakable - q.pp(statements) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :for, - index: index, - collection: collection, - stmts: statements, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_for: ( # (MLHS | VarField) value, @@ -6889,58 +1517,6 @@ def on_for(index, collection, statements) ) end - # GVar represents a global variable literal. - # - # $variable - # - class GVar - # [String] the name of the global variable - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(value:, location:, comments: []) - @value = value - @location = location - @comments = comments - end - - def child_nodes - [] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { value: value, location: location, comments: comments } - end - - def format(q) - q.text(value) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("gvar") - - q.breakable - q.pp(value) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { type: :gvar, value: value, loc: location, cmts: comments }.to_json( - *opts - ) - end - end - # :call-seq: # on_gvar: (String value) -> GVar def on_gvar(value) @@ -6950,90 +1526,6 @@ def on_gvar(value) ) end - # HashLiteral represents a hash literal. - # - # { key => value } - # - class HashLiteral - # [LBrace] the left brace that opens this hash - attr_reader :lbrace - - # [Array[ AssocNew | AssocSplat ]] the optional contents of the hash - attr_reader :assocs - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(lbrace:, assocs:, location:, comments: []) - @lbrace = lbrace - @assocs = assocs - @location = location - @comments = comments - end - - def child_nodes - [lbrace] + assocs - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { lbrace: lbrace, assocs: assocs, location: location, comments: comments } - end - - def format(q) - if q.parent.is_a?(Assoc) - format_contents(q) - else - q.group { format_contents(q) } - end - end - - def format_key(q, key) - (@key_formatter ||= HashKeyFormatter.for(self)).format_key(q, key) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("hash") - - if assocs.any? - q.breakable - q.group(2, "(", ")") { q.seplist(assocs) { |assoc| q.pp(assoc) } } - end - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { type: :hash, assocs: assocs, loc: location, cmts: comments }.to_json( - *opts - ) - end - - private - - def format_contents(q) - q.format(lbrace) - - if assocs.empty? - q.breakable("") - else - q.indent do - q.breakable - q.seplist(assocs) { |assoc| q.format(assoc) } - end - q.breakable - end - - q.text("}") - end - end - # :call-seq: # on_hash: ((nil | Array[AssocNew | AssocSplat]) assocs) -> HashLiteral def on_hash(assocs) @@ -7047,165 +1539,6 @@ def on_hash(assocs) ) end - # Heredoc represents a heredoc string literal. - # - # <<~DOC - # contents - # DOC - # - class Heredoc - # [HeredocBeg] the opening of the heredoc - attr_reader :beginning - - # [String] the ending of the heredoc - attr_reader :ending - - # [Array[ StringEmbExpr | StringDVar | TStringContent ]] the parts of the - # heredoc string literal - attr_reader :parts - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(beginning:, ending: nil, parts: [], location:, comments: []) - @beginning = beginning - @ending = ending - @parts = parts - @location = location - @comments = comments - end - - def child_nodes - [beginning, *parts] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - beginning: beginning, - location: location, - ending: ending, - parts: parts, - comments: comments - } - end - - def format(q) - # This is a very specific behavior that should probably be included in the - # prettyprint module. It's when you want to force a newline, but don't - # want to force the break parent. - breakable = -> do - q.target << - PrettyPrint::Breakable.new(" ", 1, indent: false, force: true) - end - - q.group do - q.format(beginning) - - q.line_suffix(priority: Formatter::HEREDOC_PRIORITY) do - q.group do - breakable.call - - parts.each do |part| - if part.is_a?(TStringContent) - texts = part.value.split(/\r?\n/, -1) - q.seplist(texts, breakable) { |text| q.text(text) } - else - q.format(part) - end - end - - q.text(ending) - end - end - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("heredoc") - - q.breakable - q.group(2, "(", ")") { q.seplist(parts) { |part| q.pp(part) } } - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :heredoc, - beging: beginning, - ending: ending, - parts: parts, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - - # HeredocBeg represents the beginning declaration of a heredoc. - # - # <<~DOC - # contents - # DOC - # - # In the example above the HeredocBeg node represents <<~DOC. - class HeredocBeg - # [String] the opening declaration of the heredoc - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(value:, location:, comments: []) - @value = value - @location = location - @comments = comments - end - - def child_nodes - [] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { value: value, location: location, comments: comments } - end - - def format(q) - q.text(value) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("heredoc_beg") - - q.breakable - q.pp(value) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :heredoc_beg, - value: value, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_heredoc_beg: (String value) -> HeredocBeg def on_heredoc_beg(value) @@ -7252,171 +1585,6 @@ def on_heredoc_end(value) ) end - # HshPtn represents matching against a hash pattern using the Ruby 2.7+ - # pattern matching syntax. - # - # case value - # in { key: } - # end - # - class HshPtn - class KeywordFormatter - # [Label] the keyword being used - attr_reader :key - - # [untyped] the optional value for the keyword - attr_reader :value - - def initialize(key, value) - @key = key - @value = value - end - - def comments - [] - end - - def format(q) - q.format(key) - - if value - q.text(" ") - q.format(value) - end - end - end - - class KeywordRestFormatter - # [VarField] the parameter that matches the remaining keywords - attr_reader :keyword_rest - - def initialize(keyword_rest) - @keyword_rest = keyword_rest - end - - def comments - [] - end - - def format(q) - q.text("**") - q.format(keyword_rest) - end - end - - # [nil | untyped] the optional constant wrapper - attr_reader :constant - - # [Array[ [Label, untyped] ]] the set of tuples representing the keywords - # that should be matched against in the pattern - attr_reader :keywords - - # [nil | VarField] an optional parameter to gather up all remaining keywords - attr_reader :keyword_rest - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(constant:, keywords:, keyword_rest:, location:, comments: []) - @constant = constant - @keywords = keywords - @keyword_rest = keyword_rest - @location = location - @comments = comments - end - - def child_nodes - [constant, *keywords.flatten(1), keyword_rest] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - constant: constant, - keywords: keywords, - keyword_rest: keyword_rest, - location: location, - comments: comments - } - end - - def format(q) - parts = keywords.map { |(key, value)| KeywordFormatter.new(key, value) } - parts << KeywordRestFormatter.new(keyword_rest) if keyword_rest - contents = -> do - q.seplist(parts) { |part| q.format(part, stackable: false) } - end - - if constant - q.format(constant) - q.group(0, "[", "]", &contents) - return - end - - parent = q.parent - if PATTERNS.include?(parent.class) - q.text("{ ") - contents.call - q.text(" }") - else - contents.call - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("hshptn") - - if constant - q.breakable - q.pp(constant) - end - - if keywords.any? - q.breakable - q.group(2, "(", ")") do - q.seplist(keywords) do |(key, value)| - q.group(2, "(", ")") do - q.pp(key) - - if value - q.breakable - q.pp(value) - end - end - end - end - end - - if keyword_rest - q.breakable - q.pp(keyword_rest) - end - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :hshptn, - constant: constant, - keywords: keywords, - kwrest: keyword_rest, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - - # The list of nodes that represent patterns inside of pattern matching so that - # when a pattern is being printed it knows if it's nested. - PATTERNS = [AryPtn, Binary, FndPtn, HshPtn, RAssign] - # :call-seq: # on_hshptn: ( # (nil | untyped) constant, @@ -7434,62 +1602,6 @@ def on_hshptn(constant, keywords, keyword_rest) ) end - # Ident represents an identifier anywhere in code. It can represent a very - # large number of things, depending on where it is in the syntax tree. - # - # value - # - class Ident - # [String] the value of the identifier - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(value:, location:, comments: []) - @value = value - @location = location - @comments = comments - end - - def child_nodes - [] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { value: value, location: location, comments: comments } - end - - def format(q) - q.text(value) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("ident") - - q.breakable - q.pp(value) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :ident, - value: value.force_encoding("UTF-8"), - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_ident: (String value) -> Ident def on_ident(value) @@ -7499,442 +1611,41 @@ def on_ident(value) ) end - # If the predicate of a conditional or loop contains an assignment (in which - # case we can't know for certain that that assignment doesn't impact the - # statements inside the conditional) then we can't use the modifier form - # and we must use the block form. - module ContainsAssignment - def self.call(parent) - queue = [parent] + # :call-seq: + # on_if: ( + # untyped predicate, + # Statements statements, + # (nil | Elsif | Else) consequent + # ) -> If + def on_if(predicate, statements, consequent) + beginning = find_token(Kw, "if") + ending = consequent || find_token(Kw, "end") - while node = queue.shift - return true if [Assign, MAssign, OpAssign].include?(node.class) - queue += node.child_nodes - end + statements.bind(predicate.location.end_char, ending.location.start_char) - false - end + If.new( + predicate: predicate, + statements: statements, + consequent: consequent, + location: beginning.location.to(ending.location) + ) end - # Formats an If or Unless node. - class ConditionalFormatter - # [String] the keyword associated with this conditional - attr_reader :keyword + # :call-seq: + # on_ifop: (untyped predicate, untyped truthy, untyped falsy) -> IfOp + def on_ifop(predicate, truthy, falsy) + IfOp.new( + predicate: predicate, + truthy: truthy, + falsy: falsy, + location: predicate.location.to(falsy.location) + ) + end - # [If | Unless] the node that is being formatted - attr_reader :node - - def initialize(keyword, node) - @keyword = keyword - @node = node - end - - def format(q) - # If the predicate of the conditional contains an assignment (in which - # case we can't know for certain that that assignment doesn't impact the - # statements inside the conditional) then we can't use the modifier form - # and we must use the block form. - if ContainsAssignment.call(node.predicate) - format_break(q, force: true) - return - end - - if node.consequent || node.statements.empty? - q.group { format_break(q, force: true) } - else - q.group do - q.if_break { format_break(q, force: false) }.if_flat do - Parentheses.flat(q) do - q.format(node.statements) - q.text(" #{keyword} ") - q.format(node.predicate) - end - end - end - end - end - - private - - def format_break(q, force:) - q.text("#{keyword} ") - q.nest(keyword.length + 1) { q.format(node.predicate) } - - unless node.statements.empty? - q.indent do - q.breakable(force: force) - q.format(node.statements) - end - end - - if node.consequent - q.breakable(force: force) - q.format(node.consequent) - end - - q.breakable(force: force) - q.text("end") - end - end - - # If represents the first clause in an +if+ chain. - # - # if predicate - # end - # - class If - # [untyped] the expression to be checked - attr_reader :predicate - - # [Statements] the expressions to be executed - attr_reader :statements - - # [nil, Elsif, Else] the next clause in the chain - attr_reader :consequent - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize( - predicate:, - statements:, - consequent:, - location:, - comments: [] - ) - @predicate = predicate - @statements = statements - @consequent = consequent - @location = location - @comments = comments - end - - def child_nodes - [predicate, statements, consequent] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - predicate: predicate, - statements: statements, - consequent: consequent, - location: location, - comments: comments - } - end - - def format(q) - ConditionalFormatter.new("if", self).format(q) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("if") - - q.breakable - q.pp(predicate) - - q.breakable - q.pp(statements) - - if consequent - q.breakable - q.pp(consequent) - end - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :if, - pred: predicate, - stmts: statements, - cons: consequent, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - - # :call-seq: - # on_if: ( - # untyped predicate, - # Statements statements, - # (nil | Elsif | Else) consequent - # ) -> If - def on_if(predicate, statements, consequent) - beginning = find_token(Kw, "if") - ending = consequent || find_token(Kw, "end") - - statements.bind(predicate.location.end_char, ending.location.start_char) - - If.new( - predicate: predicate, - statements: statements, - consequent: consequent, - location: beginning.location.to(ending.location) - ) - end - - # IfOp represents a ternary clause. - # - # predicate ? truthy : falsy - # - class IfOp - # [untyped] the expression to be checked - attr_reader :predicate - - # [untyped] the expression to be executed if the predicate is truthy - attr_reader :truthy - - # [untyped] the expression to be executed if the predicate is falsy - attr_reader :falsy - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(predicate:, truthy:, falsy:, location:, comments: []) - @predicate = predicate - @truthy = truthy - @falsy = falsy - @location = location - @comments = comments - end - - def child_nodes - [predicate, truthy, falsy] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - predicate: predicate, - truthy: truthy, - falsy: falsy, - location: location, - comments: comments - } - end - - def format(q) - force_flat = [ - Alias, Assign, Break, Command, CommandCall, Heredoc, If, IfMod, IfOp, - Lambda, MAssign, Next, OpAssign, RescueMod, Return, Return0, Super, - Undef, Unless, UnlessMod, UntilMod, VarAlias, VoidStmt, WhileMod, Yield, - Yield0, ZSuper - ] - - if force_flat.include?(truthy.class) || force_flat.include?(falsy.class) - q.group { format_flat(q) } - return - end - - q.group { q.if_break { format_break(q) }.if_flat { format_flat(q) } } - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("ifop") - - q.breakable - q.pp(predicate) - - q.breakable - q.pp(truthy) - - q.breakable - q.pp(falsy) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :ifop, - pred: predicate, - tthy: truthy, - flsy: falsy, - loc: location, - cmts: comments - }.to_json(*opts) - end - - private - - def format_break(q) - Parentheses.break(q) do - q.text("if ") - q.nest("if ".length) { q.format(predicate) } - - q.indent do - q.breakable - q.format(truthy) - end - - q.breakable - q.text("else") - - q.indent do - q.breakable - q.format(falsy) - end - - q.breakable - q.text("end") - end - end - - def format_flat(q) - q.format(predicate) - q.text(" ?") - - q.breakable - q.format(truthy) - q.text(" :") - - q.breakable - q.format(falsy) - end - end - - # :call-seq: - # on_ifop: (untyped predicate, untyped truthy, untyped falsy) -> IfOp - def on_ifop(predicate, truthy, falsy) - IfOp.new( - predicate: predicate, - truthy: truthy, - falsy: falsy, - location: predicate.location.to(falsy.location) - ) - end - - # Formats an IfMod or UnlessMod node. - class ConditionalModFormatter - # [String] the keyword associated with this conditional - attr_reader :keyword - - # [IfMod | UnlessMod] the node that is being formatted - attr_reader :node - - def initialize(keyword, node) - @keyword = keyword - @node = node - end - - def format(q) - if ContainsAssignment.call(node.statement) || q.parent.is_a?(In) - q.group { format_flat(q) } - else - q.group { q.if_break { format_break(q) }.if_flat { format_flat(q) } } - end - end - - private - - def format_break(q) - q.text("#{keyword} ") - q.nest(keyword.length + 1) { q.format(node.predicate) } - q.indent do - q.breakable - q.format(node.statement) - end - q.breakable - q.text("end") - end - - def format_flat(q) - Parentheses.flat(q) do - q.format(node.statement) - q.text(" #{keyword} ") - q.format(node.predicate) - end - end - end - - # IfMod represents the modifier form of an +if+ statement. - # - # expression if predicate - # - class IfMod - # [untyped] the expression to be executed - attr_reader :statement - - # [untyped] the expression to be checked - attr_reader :predicate - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(statement:, predicate:, location:, comments: []) - @statement = statement - @predicate = predicate - @location = location - @comments = comments - end - - def child_nodes - [statement, predicate] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - statement: statement, - predicate: predicate, - location: location, - comments: comments - } - end - - def format(q) - ConditionalModFormatter.new("if", self).format(q) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("if_mod") - - q.breakable - q.pp(statement) - - q.breakable - q.pp(predicate) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :if_mod, - stmt: statement, - pred: predicate, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - - # :call-seq: - # on_if_mod: (untyped predicate, untyped statement) -> IfMod - def on_if_mod(predicate, statement) - find_token(Kw, "if") + # :call-seq: + # on_if_mod: (untyped predicate, untyped statement) -> IfMod + def on_if_mod(predicate, statement) + find_token(Kw, "if") IfMod.new( statement: statement, @@ -7951,58 +1662,6 @@ def on_if_mod(predicate, statement) # value # end - # Imaginary represents an imaginary number literal. - # - # 1i - # - class Imaginary - # [String] the value of the imaginary number literal - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(value:, location:, comments: []) - @value = value - @location = location - @comments = comments - end - - def child_nodes - [] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { value: value, location: location, comments: comments } - end - - def format(q) - q.text(value) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("imaginary") - - q.breakable - q.pp(value) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { type: :imaginary, value: value, loc: location, cmts: comments }.to_json( - *opts - ) - end - end - # :call-seq: # on_imaginary: (String value) -> Imaginary def on_imaginary(value) @@ -8012,105 +1671,6 @@ def on_imaginary(value) ) end - # In represents using the +in+ keyword within the Ruby 2.7+ pattern matching - # syntax. - # - # case value - # in pattern - # end - # - class In - # [untyped] the pattern to check against - attr_reader :pattern - - # [Statements] the expressions to execute if the pattern matched - attr_reader :statements - - # [nil | In | Else] the next clause in the chain - attr_reader :consequent - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(pattern:, statements:, consequent:, location:, comments: []) - @pattern = pattern - @statements = statements - @consequent = consequent - @location = location - @comments = comments - end - - def child_nodes - [pattern, statements, consequent] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - pattern: pattern, - statements: statements, - consequent: consequent, - location: location, - comments: comments - } - end - - def format(q) - keyword = "in " - - q.group do - q.text(keyword) - q.nest(keyword.length) { q.format(pattern) } - - unless statements.empty? - q.indent do - q.breakable(force: true) - q.format(statements) - end - end - - if consequent - q.breakable(force: true) - q.format(consequent) - end - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("in") - - q.breakable - q.pp(pattern) - - q.breakable - q.pp(statements) - - if consequent - q.breakable - q.pp(consequent) - end - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :in, - pattern: pattern, - stmts: statements, - cons: consequent, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_in: (RAssign pattern, nil statements, nil consequent) -> RAssign # | ( @@ -8144,192 +1704,23 @@ def on_in(pattern, statements, consequent) ) end - # Int represents an integer number literal. - # - # 1 - # - class Int - # [String] the value of the integer - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(value:, location:, comments: []) - @value = value - @location = location - @comments = comments - end + # :call-seq: + # on_int: (String value) -> Int + def on_int(value) + Int.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) + end - def child_nodes - [] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { value: value, location: location, comments: comments } - end - - def format(q) - if !value.start_with?(/\+?0/) && value.length >= 5 && !value.include?("_") - # If it's a plain integer and it doesn't have any underscores separating - # the values, then we're going to insert them every 3 characters - # starting from the right. - index = (value.length + 2) % 3 - q.text(" #{value}"[index..-1].scan(/.../).join("_").strip) - else - q.text(value) - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("int") - - q.breakable - q.pp(value) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { type: :int, value: value, loc: location, cmts: comments }.to_json(*opts) - end - end - - # :call-seq: - # on_int: (String value) -> Int - def on_int(value) - Int.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - end - - # IVar represents an instance variable literal. - # - # @variable - # - class IVar - # [String] the name of the instance variable - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(value:, location:, comments: []) - @value = value - @location = location - @comments = comments - end - - def child_nodes - [] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { value: value, location: location, comments: comments } - end - - def format(q) - q.text(value) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("ivar") - - q.breakable - q.pp(value) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { type: :ivar, value: value, loc: location, cmts: comments }.to_json( - *opts - ) - end - end - - # :call-seq: - # on_ivar: (String value) -> IVar - def on_ivar(value) - IVar.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - end - - # Kw represents the use of a keyword. It can be almost anywhere in the syntax - # tree, so you end up seeing it quite a lot. - # - # if value - # end - # - # In the above example, there would be two Kw nodes: one for the if and one - # for the end. Note that anything that matches the list of keywords in Ruby - # will use a Kw, so if you use a keyword in a symbol literal for instance: - # - # :if - # - # then the contents of the symbol node will contain a Kw node. - class Kw - # [String] the value of the keyword - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(value:, location:, comments: []) - @value = value - @location = location - @comments = comments - end - - def child_nodes - [] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { value: value, location: location, comments: comments } - end - - def format(q) - q.text(value) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("kw") - - q.breakable - q.pp(value) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { type: :kw, value: value, loc: location, cmts: comments }.to_json(*opts) - end - end + # :call-seq: + # on_ivar: (String value) -> IVar + def on_ivar(value) + IVar.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) + end # :call-seq: # on_kw: (String value) -> Kw @@ -8344,63 +1735,6 @@ def on_kw(value) node end - # KwRestParam represents defining a parameter in a method definition that - # accepts all remaining keyword parameters. - # - # def method(**kwargs) end - # - class KwRestParam - # [nil | Ident] the name of the parameter - attr_reader :name - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(name:, location:, comments: []) - @name = name - @location = location - @comments = comments - end - - def child_nodes - [name] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { name: name, location: location, comments: comments } - end - - def format(q) - q.text("**") - q.format(name) if name - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("kwrest_param") - - q.breakable - q.pp(name) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :kwrest_param, - name: name, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_kwrest_param: ((nil | Ident) name) -> KwRestParam def on_kwrest_param(name) @@ -8410,68 +1744,6 @@ def on_kwrest_param(name) KwRestParam.new(name: name, location: location) end - # Label represents the use of an identifier to associate with an object. You - # can find it in a hash key, as in: - # - # { key: value } - # - # In this case "key:" would be the body of the label. You can also find it in - # pattern matching, as in: - # - # case value - # in key: - # end - # - # In this case "key:" would be the body of the label. - class Label - # [String] the value of the label - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(value:, location:, comments: []) - @value = value - @location = location - @comments = comments - end - - def child_nodes - [] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { value: value, location: location, comments: comments } - end - - def format(q) - q.text(value) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("label") - - q.breakable - q.text(":") - q.text(value[0...-1]) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { type: :label, value: value, loc: location, cmts: comments }.to_json( - *opts - ) - end - end - # :call-seq: # on_label: (String value) -> Label def on_label(value) @@ -8481,26 +1753,6 @@ def on_label(value) ) end - # LabelEnd represents the end of a dynamic symbol. - # - # { "key": value } - # - # In the example above, LabelEnd represents the "\":" token at the end of the - # hash key. This node is important for determining the type of quote being - # used by the label. - class LabelEnd - # [String] the end of the label - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - def initialize(value:, location:) - @value = value - @location = location - end - end - # :call-seq: # on_label_end: (String value) -> LabelEnd def on_label_end(value) @@ -8514,105 +1766,6 @@ def on_label_end(value) node end - # Lambda represents using a lambda literal (not the lambda method call). - # - # ->(value) { value * 2 } - # - class Lambda - # [Params | Paren] the parameter declaration for this lambda - attr_reader :params - - # [BodyStmt | Statements] the expressions to be executed in this lambda - attr_reader :statements - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(params:, statements:, location:, comments: []) - @params = params - @statements = statements - @location = location - @comments = comments - end - - def child_nodes - [params, statements] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - params: params, - statements: statements, - location: location, - comments: comments - } - end - - def format(q) - q.group(0, "->") do - if params.is_a?(Paren) - q.format(params) unless params.contents.empty? - elsif !params.empty? - q.group do - q.text("(") - q.format(params) - q.text(")") - end - end - - q.text(" ") - q.if_break do - force_parens = - q.parents.any? do |node| - node.is_a?(Command) || node.is_a?(CommandCall) - end - - q.text(force_parens ? "{" : "do") - q.indent do - q.breakable - q.format(statements) - end - - q.breakable - q.text(force_parens ? "}" : "end") - end.if_flat do - q.text("{ ") - q.format(statements) - q.text(" }") - end - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("lambda") - - q.breakable - q.pp(params) - - q.breakable - q.pp(statements) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :lambda, - params: params, - stmts: statements, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_lambda: ( # (Params | Paren) params, @@ -8641,55 +1794,6 @@ def on_lambda(params, statements) ) end - # LBrace represents the use of a left brace, i.e., {. - class LBrace - # [String] the left brace - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(value:, location:, comments: []) - @value = value - @location = location - @comments = comments - end - - def child_nodes - [] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { value: value, location: location, comments: comments } - end - - def format(q) - q.text(value) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("lbrace") - - q.breakable - q.pp(value) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { type: :lbrace, value: value, loc: location, cmts: comments }.to_json( - *opts - ) - end - end - # :call-seq: # on_lbrace: (String value) -> LBrace def on_lbrace(value) @@ -8703,55 +1807,6 @@ def on_lbrace(value) node end - # LBracket represents the use of a left bracket, i.e., [. - class LBracket - # [String] the left bracket - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(value:, location:, comments: []) - @value = value - @location = location - @comments = comments - end - - def child_nodes - [] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { value: value, location: location, comments: comments } - end - - def format(q) - q.text(value) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("lbracket") - - q.breakable - q.pp(value) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { type: :lbracket, value: value, loc: location, cmts: comments }.to_json( - *opts - ) - end - end - # :call-seq: # on_lbracket: (String value) -> LBracket def on_lbracket(value) @@ -8765,55 +1820,6 @@ def on_lbracket(value) node end - # LParen represents the use of a left parenthesis, i.e., (. - class LParen - # [String] the left parenthesis - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(value:, location:, comments: []) - @value = value - @location = location - @comments = comments - end - - def child_nodes - [] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { value: value, location: location, comments: comments } - end - - def format(q) - q.text(value) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("lparen") - - q.breakable - q.pp(value) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { type: :lparen, value: value, loc: location, cmts: comments }.to_json( - *opts - ) - end - end - # :call-seq: # on_lparen: (String value) -> LParen def on_lparen(value) @@ -8831,91 +1837,11 @@ def on_lparen(value) # [key, value] # end - # MAssign is a parent node of any kind of multiple assignment. This includes - # splitting out variables on the left like: - # - # first, second, third = value - # - # as well as splitting out variables on the right, as in: - # - # value = first, second, third - # - # Both sides support splats, as well as variables following them. There's also - # destructuring behavior that you can achieve with the following: - # - # first, = value - # - class MAssign - # [MLHS | MLHSParen] the target of the multiple assignment - attr_reader :target - - # [untyped] the value being assigned - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(target:, value:, location:, comments: []) - @target = target - @value = value - @location = location - @comments = comments - end - - def child_nodes - [target, value] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { target: target, value: value, location: location, comments: comments } - end - - def format(q) - q.group do - q.group { q.format(target) } - q.text(" =") - q.indent do - q.breakable - q.format(value) - end - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("massign") - - q.breakable - q.pp(target) - - q.breakable - q.pp(value) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :massign, - target: target, - value: value, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - - # :call-seq: - # on_massign: ((MLHS | MLHSParen) target, untyped value) -> MAssign - def on_massign(target, value) - comma_range = target.location.end_char...value.location.start_char - target.comma = true if source[comma_range].strip.start_with?(",") + # :call-seq: + # on_massign: ((MLHS | MLHSParen) target, untyped value) -> MAssign + def on_massign(target, value) + comma_range = target.location.end_char...value.location.start_char + target.comma = true if source[comma_range].strip.start_with?(",") MAssign.new( target: target, @@ -8946,70 +1872,6 @@ def on_method_add_arg(call, arguments) end end - # MethodAddBlock represents a method call with a block argument. - # - # method {} - # - class MethodAddBlock - # [Call | Command | CommandCall | FCall] the method call - attr_reader :call - - # [BraceBlock | DoBlock] the block being sent with the method call - attr_reader :block - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(call:, block:, location:, comments: []) - @call = call - @block = block - @location = location - @comments = comments - end - - def child_nodes - [call, block] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { call: call, block: block, location: location, comments: comments } - end - - def format(q) - q.format(call) - q.format(block) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("method_add_block") - - q.breakable - q.pp(call) - - q.breakable - q.pp(block) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :method_add_block, - call: call, - block: block, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_method_add_block: ( # (Call | Command | CommandCall | FCall) call, @@ -9023,71 +1885,6 @@ def on_method_add_block(call, block) ) end - # MLHS represents a list of values being destructured on the left-hand side - # of a multiple assignment. - # - # first, second, third = value - # - class MLHS - # Array[ARefField | ArgStar | Field | Ident | MLHSParen | VarField] the - # parts of the left-hand side of a multiple assignment - attr_reader :parts - - # [boolean] whether or not there is a trailing comma at the end of this - # list, which impacts destructuring. It's an attr_accessor so that while - # the syntax tree is being built it can be set by its parent node - attr_accessor :comma - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(parts:, comma: false, location:, comments: []) - @parts = parts - @comma = comma - @location = location - @comments = comments - end - - def child_nodes - parts - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { parts: parts, location: location, comma: comma, comments: comments } - end - - def format(q) - q.seplist(parts) { |part| q.format(part) } - q.text(",") if comma - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("mlhs") - - q.breakable - q.group(2, "(", ")") { q.seplist(parts) { |part| q.pp(part) } } - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :mlhs, - parts: parts, - comma: comma, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_mlhs_add: ( # MLHS mlhs, @@ -9131,75 +1928,6 @@ def on_mlhs_new MLHS.new(parts: [], location: Location.fixed(line: lineno, char: char_pos)) end - # MLHSParen represents parentheses being used to destruct values in a multiple - # assignment on the left hand side. - # - # (left, right) = value - # - class MLHSParen - # [MLHS | MLHSParen] the contents inside of the parentheses - attr_reader :contents - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(contents:, location:, comments: []) - @contents = contents - @location = location - @comments = comments - end - - def child_nodes - [contents] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { contents: contents, location: location, comments: comments } - end - - def format(q) - parent = q.parent - - if parent.is_a?(MAssign) || parent.is_a?(MLHSParen) - q.format(contents) - else - q.group(0, "(", ")") do - q.indent do - q.breakable("") - q.format(contents) - end - - q.breakable("") - end - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("mlhs_paren") - - q.breakable - q.pp(contents) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :mlhs_paren, - cnts: contents, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_mlhs_paren: ((MLHS | MLHSParen) contents) -> MLHSParen def on_mlhs_paren(contents) @@ -9215,100 +1943,6 @@ def on_mlhs_paren(contents) ) end - # ModuleDeclaration represents defining a module using the +module+ keyword. - # - # module Namespace - # end - # - class ModuleDeclaration - # [ConstPathRef | ConstRef | TopConstRef] the name of the module - attr_reader :constant - - # [BodyStmt] the expressions to be executed in the context of the module - attr_reader :bodystmt - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(constant:, bodystmt:, location:, comments: []) - @constant = constant - @bodystmt = bodystmt - @location = location - @comments = comments - end - - def child_nodes - [constant, bodystmt] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - constant: constant, - bodystmt: bodystmt, - location: location, - comments: comments - } - end - - def format(q) - declaration = -> do - q.group do - q.text("module ") - q.format(constant) - end - end - - if bodystmt.empty? - q.group do - declaration.call - q.breakable(force: true) - q.text("end") - end - else - q.group do - declaration.call - - q.indent do - q.breakable(force: true) - q.format(bodystmt) - end - - q.breakable(force: true) - q.text("end") - end - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("module") - - q.breakable - q.pp(constant) - - q.breakable - q.pp(bodystmt) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :module, - constant: constant, - bodystmt: bodystmt, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_module: ( # (ConstPathRef | ConstRef | TopConstRef) constant, @@ -9330,59 +1964,6 @@ def on_module(constant, bodystmt) ) end - # MRHS represents the values that are being assigned on the right-hand side of - # a multiple assignment. - # - # values = first, second, third - # - class MRHS - # Array[untyped] the parts that are being assigned - attr_reader :parts - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(parts:, location:, comments: []) - @parts = parts - @location = location - @comments = comments - end - - def child_nodes - parts - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { parts: parts, location: location, comments: comments } - end - - def format(q) - q.seplist(parts) { |part| q.format(part) } - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("mrhs") - - q.breakable - q.group(2, "(", ")") { q.seplist(parts) { |part| q.pp(part) } } - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { type: :mrhs, parts: parts, loc: location, cmts: comments }.to_json( - *opts - ) - end - end - # :call-seq: # on_mrhs_new: () -> MRHS def on_mrhs_new @@ -9405,619 +1986,82 @@ def on_mrhs_add(mrhs, part) # :call-seq: # on_mrhs_add_star: (MRHS mrhs, untyped value) -> MRHS def on_mrhs_add_star(mrhs, value) - beginning = find_token(Op, "*") - ending = value || beginning - - arg_star = - ArgStar.new( - value: value, - location: beginning.location.to(ending.location) - ) - - location = - if mrhs.parts.empty? - arg_star.location - else - mrhs.location.to(arg_star.location) - end - - MRHS.new(parts: mrhs.parts << arg_star, location: location) - end - - # :call-seq: - # on_mrhs_new_from_args: (Args arguments) -> MRHS - def on_mrhs_new_from_args(arguments) - MRHS.new(parts: arguments.parts, location: arguments.location) - end - - # Next represents using the +next+ keyword. - # - # next - # - # The +next+ keyword can also optionally be called with an argument: - # - # next value - # - # +next+ can even be called with multiple arguments, but only if parentheses - # are omitted, as in: - # - # next first, second, third - # - # If a single value is being given, parentheses can be used, as in: - # - # next(value) - # - class Next - # [Args] the arguments passed to the next keyword - attr_reader :arguments - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(arguments:, location:, comments: []) - @arguments = arguments - @location = location - @comments = comments - end - - def child_nodes - [arguments] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { arguments: arguments, location: location, comments: comments } - end - - def format(q) - FlowControlFormatter.new("next", self).format(q) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("next") - - q.breakable - q.pp(arguments) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { type: :next, args: arguments, loc: location, cmts: comments }.to_json( - *opts - ) - end - end - - # :call-seq: - # on_next: (Args arguments) -> Next - def on_next(arguments) - keyword = find_token(Kw, "next") - - location = keyword.location - location = location.to(arguments.location) if arguments.parts.any? - - Next.new(arguments: arguments, location: location) - end - - # def on_nl(value) - # value - # end - - # def on_nokw_param(value) - # value - # end - - # Op represents an operator literal in the source. - # - # 1 + 2 - # - # In the example above, the Op node represents the + operator. - class Op - # [String] the operator - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(value:, location:, comments: []) - @value = value - @location = location - @comments = comments - end - - def child_nodes - [] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { value: value, location: location, comments: comments } - end - - def format(q) - q.text(value) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("op") - - q.breakable - q.pp(value) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { type: :op, value: value, loc: location, cmts: comments }.to_json(*opts) - end - end - - # :call-seq: - # on_op: (String value) -> Op - def on_op(value) - node = - Op.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - - tokens << node - node - end - - # OpAssign represents assigning a value to a variable or constant using an - # operator like += or ||=. - # - # variable += value - # - class OpAssign - # [ARefField | ConstPathField | Field | TopConstField | VarField] the target - # to assign the result of the expression to - attr_reader :target - - # [Op] the operator being used for the assignment - attr_reader :operator - - # [untyped] the expression to be assigned - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(target:, operator:, value:, location:, comments: []) - @target = target - @operator = operator - @value = value - @location = location - @comments = comments - end - - def child_nodes - [target, operator, value] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - target: target, - operator: operator, - value: value, - location: location, - comments: comments - } - end - - def format(q) - q.group do - q.format(target) - q.text(" ") - q.format(operator) - q.indent do - q.breakable - q.format(value) - end - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("opassign") - - q.breakable - q.pp(target) - - q.breakable - q.pp(operator) - - q.breakable - q.pp(value) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :opassign, - target: target, - op: operator, - value: value, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - - # :call-seq: - # on_opassign: ( - # (ARefField | ConstPathField | Field | TopConstField | VarField) target, - # Op operator, - # untyped value - # ) -> OpAssign - def on_opassign(target, operator, value) - OpAssign.new( - target: target, - operator: operator, - value: value, - location: target.location.to(value.location) - ) - end - - # If you have a modifier statement (for instance a modifier if statement or a - # modifier while loop) there are times when you need to wrap the entire - # statement in parentheses. This occurs when you have something like: - # - # foo[:foo] = - # if bar? - # baz - # end - # - # Normally we would shorten this to an inline version, which would result in: - # - # foo[:foo] = baz if bar? - # - # but this actually has different semantic meaning. The first example will - # result in a nil being inserted into the hash for the :foo key, whereas the - # second example will result in an empty hash because the if statement applies - # to the entire assignment. - # - # We can fix this in a couple of ways. We can use the then keyword, as in: - # - # foo[:foo] = if bar? then baz end - # - # But this isn't used very often. We can also just leave it as is with the - # multi-line version, but for a short predicate and short value it looks - # verbose. The last option and the one used here is to add parentheses on - # both sides of the expression, as in: - # - # foo[:foo] = (baz if bar?) - # - # This approach maintains the nice conciseness of the inline version, while - # keeping the correct semantic meaning. - module Parentheses - NODES = [Args, Assign, Assoc, Binary, Call, Defined, MAssign, OpAssign] - - def self.flat(q) - return yield unless NODES.include?(q.parent.class) - - q.text("(") - yield - q.text(")") - end - - def self.break(q) - return yield unless NODES.include?(q.parent.class) - - q.text("(") - q.indent do - q.breakable("") - yield - end - q.breakable("") - q.text(")") - end - end - - # def on_operator_ambiguous(value) - # value - # end - - # Params represents defining parameters on a method or lambda. - # - # def method(param) end - # - class Params - class OptionalFormatter - # [Ident] the name of the parameter - attr_reader :name - - # [untyped] the value of the parameter - attr_reader :value - - def initialize(name, value) - @name = name - @value = value - end - - def comments - [] - end - - def format(q) - q.format(name) - q.text(" = ") - q.format(value) - end - end - - class KeywordFormatter - # [Ident] the name of the parameter - attr_reader :name - - # [nil | untyped] the value of the parameter - attr_reader :value - - def initialize(name, value) - @name = name - @value = value - end - - def comments - [] - end - - def format(q) - q.format(name) - - if value - q.text(" ") - q.format(value) - end - end - end - - class KeywordRestFormatter - # [:nil | ArgsForward | KwRestParam] the value of the parameter - attr_reader :value - - def initialize(value) - @value = value - end - - def comments - [] - end - - def format(q) - if value == :nil - q.text("**nil") - else - q.format(value) - end - end - end - - # [Array[ Ident ]] any required parameters - attr_reader :requireds - - # [Array[ [ Ident, untyped ] ]] any optional parameters and their default - # values - attr_reader :optionals - - # [nil | ArgsForward | ExcessedComma | RestParam] the optional rest - # parameter - attr_reader :rest - - # [Array[ Ident ]] any positional parameters that exist after a rest - # parameter - attr_reader :posts - - # [Array[ [ Ident, nil | untyped ] ]] any keyword parameters and their - # optional default values - attr_reader :keywords - - # [nil | :nil | KwRestParam] the optional keyword rest parameter - attr_reader :keyword_rest - - # [nil | BlockArg] the optional block parameter - attr_reader :block - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize( - requireds: [], - optionals: [], - rest: nil, - posts: [], - keywords: [], - keyword_rest: nil, - block: nil, - location:, - comments: [] - ) - @requireds = requireds - @optionals = optionals - @rest = rest - @posts = posts - @keywords = keywords - @keyword_rest = keyword_rest - @block = block - @location = location - @comments = comments - end - - # Params nodes are the most complicated in the tree. Occasionally you want - # to know if they are "empty", which means not having any parameters - # declared. This logic accesses every kind of parameter and determines if - # it's missing. - def empty? - requireds.empty? && optionals.empty? && !rest && posts.empty? && - keywords.empty? && !keyword_rest && !block - end - - def child_nodes - [ - *requireds, - *optionals.flatten(1), - rest, - *posts, - *keywords.flatten(1), - (keyword_rest if keyword_rest != :nil), - block - ] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - location: location, - requireds: requireds, - optionals: optionals, - rest: rest, - posts: posts, - keywords: keywords, - keyword_rest: keyword_rest, - block: block, - comments: comments - } - end + beginning = find_token(Op, "*") + ending = value || beginning - def format(q) - parts = [ - *requireds, - *optionals.map { |(name, value)| OptionalFormatter.new(name, value) } - ] - - parts << rest if rest && !rest.is_a?(ExcessedComma) - parts += - [ - *posts, - *keywords.map { |(name, value)| KeywordFormatter.new(name, value) } - ] - - parts << KeywordRestFormatter.new(keyword_rest) if keyword_rest - parts << block if block - - contents = -> do - q.seplist(parts) { |part| q.format(part) } - q.format(rest) if rest && rest.is_a?(ExcessedComma) - end + arg_star = + ArgStar.new( + value: value, + location: beginning.location.to(ending.location) + ) - if [Def, Defs, DefEndless].include?(q.parent.class) - q.group(0, "(", ")") do - q.indent do - q.breakable("") - contents.call - end - q.breakable("") - end + location = + if mrhs.parts.empty? + arg_star.location else - q.nest(0, &contents) + mrhs.location.to(arg_star.location) end - end - def pretty_print(q) - q.group(2, "(", ")") do - q.text("params") + MRHS.new(parts: mrhs.parts << arg_star, location: location) + end - if requireds.any? - q.breakable - q.group(2, "(", ")") { q.seplist(requireds) { |name| q.pp(name) } } - end + # :call-seq: + # on_mrhs_new_from_args: (Args arguments) -> MRHS + def on_mrhs_new_from_args(arguments) + MRHS.new(parts: arguments.parts, location: arguments.location) + end - if optionals.any? - q.breakable - q.group(2, "(", ")") do - q.seplist(optionals) do |(name, default)| - q.pp(name) - q.text("=") - q.group(2) do - q.breakable("") - q.pp(default) - end - end - end - end + # :call-seq: + # on_next: (Args arguments) -> Next + def on_next(arguments) + keyword = find_token(Kw, "next") - if rest - q.breakable - q.pp(rest) - end + location = keyword.location + location = location.to(arguments.location) if arguments.parts.any? - if posts.any? - q.breakable - q.group(2, "(", ")") { q.seplist(posts) { |value| q.pp(value) } } - end + Next.new(arguments: arguments, location: location) + end - if keywords.any? - q.breakable - q.group(2, "(", ")") do - q.seplist(keywords) do |(name, default)| - q.pp(name) - - if default - q.text("=") - q.group(2) do - q.breakable("") - q.pp(default) - end - end - end - end - end + # def on_nl(value) + # value + # end - if keyword_rest - q.breakable - q.pp(keyword_rest) - end + # def on_nokw_param(value) + # value + # end - if block - q.breakable - q.pp(block) - end + # :call-seq: + # on_op: (String value) -> Op + def on_op(value) + node = + Op.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) - q.pp(Comment::List.new(comments)) - end - end + tokens << node + node + end - def to_json(*opts) - { - type: :params, - reqs: requireds, - opts: optionals, - rest: rest, - posts: posts, - keywords: keywords, - kwrest: keyword_rest, - block: block, - loc: location, - cmts: comments - }.to_json(*opts) - end + # :call-seq: + # on_opassign: ( + # (ARefField | ConstPathField | Field | TopConstField | VarField) target, + # Op operator, + # untyped value + # ) -> OpAssign + def on_opassign(target, operator, value) + OpAssign.new( + target: target, + operator: operator, + value: value, + location: target.location.to(value.location) + ) end + # def on_operator_ambiguous(value) + # value + # end + # :call-seq: # on_params: ( # (nil | Array[Ident]) requireds, @@ -10066,85 +2110,6 @@ def on_params( ) end - # Paren represents using balanced parentheses in a couple places in a Ruby - # program. In general parentheses can be used anywhere a Ruby expression can - # be used. - # - # (1 + 2) - # - class Paren - # [LParen] the left parenthesis that opened this statement - attr_reader :lparen - - # [nil | untyped] the expression inside the parentheses - attr_reader :contents - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(lparen:, contents:, location:, comments: []) - @lparen = lparen - @contents = contents - @location = location - @comments = comments - end - - def child_nodes - [lparen, contents] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - lparen: lparen, - contents: contents, - location: location, - comments: comments - } - end - - def format(q) - q.group do - q.format(lparen) - - if contents && (!contents.is_a?(Params) || !contents.empty?) - q.indent do - q.breakable("") - q.format(contents) - end - end - - q.breakable("") - q.text(")") - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("paren") - - q.breakable - q.pp(contents) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :paren, - lparen: lparen, - cnts: contents, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_paren: (untyped contents) -> Paren def on_paren(contents) @@ -10191,56 +2156,6 @@ def on_parse_error(error, *) alias on_class_name_error on_parse_error alias on_param_error on_parse_error - # Period represents the use of the +.+ operator. It is usually found in method - # calls. - class Period - # [String] the period - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(value:, location:, comments: []) - @value = value - @location = location - @comments = comments - end - - def child_nodes - [] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { value: value, location: location, comments: comments } - end - - def format(q) - q.text(value) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("period") - - q.breakable - q.pp(value) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { type: :period, value: value, loc: location, cmts: comments }.to_json( - *opts - ) - end - end - # :call-seq: # on_period: (String value) -> Period def on_period(value) @@ -10250,64 +2165,6 @@ def on_period(value) ) end - # Program represents the overall syntax tree. - class Program - # [Statements] the top-level expressions of the program - attr_reader :statements - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(statements:, location:, comments: []) - @statements = statements - @location = location - @comments = comments - end - - def child_nodes - [statements] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { statements: statements, location: location, comments: comments } - end - - def format(q) - q.format(statements) - - # We're going to put a newline on the end so that it always has one unless - # it ends with the special __END__ syntax. In that case we want to - # replicate the text exactly so we will just let it be. - q.breakable(force: true) unless statements.body.last.is_a?(EndContent) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("program") - - q.breakable - q.pp(statements) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :program, - stmts: statements, - comments: comments, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_program: (Statements statements) -> Program def on_program(statements) @@ -10417,85 +2274,6 @@ def nearest_nodes(node, comment) [preceding, node, following] end - # QSymbols represents a symbol literal array without interpolation. - # - # %i[one two three] - # - class QSymbols - # [QSymbolsBeg] the token that opens this array literal - attr_reader :beginning - - # [Array[ TStringContent ]] the elements of the array - attr_reader :elements - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(beginning:, elements:, location:, comments: []) - @beginning = beginning - @elements = elements - @location = location - @comments = comments - end - - def child_nodes - [] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - beginning: beginning, - elements: elements, - location: location, - comments: comments - } - end - - def format(q) - opening, closing = "%i[", "]" - - if elements.any? { |element| element.match?(/[\[\]]/) } - opening = beginning.value - closing = Quotes.matching(opening[2]) - end - - q.group(0, opening, closing) do - q.indent do - q.breakable("") - q.seplist(elements, -> { q.breakable }) do |element| - q.format(element) - end - end - q.breakable("") - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("qsymbols") - - q.breakable - q.group(2, "(", ")") { q.seplist(elements) { |element| q.pp(element) } } - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :qsymbols, - elems: elements, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_qsymbols_add: (QSymbols qsymbols, TStringContent element) -> QSymbols def on_qsymbols_add(qsymbols, element) @@ -10506,26 +2284,6 @@ def on_qsymbols_add(qsymbols, element) ) end - # QSymbolsBeg represents the beginning of a symbol literal array. - # - # %i[one two three] - # - # In the snippet above, QSymbolsBeg represents the "%i[" token. Note that - # these kinds of arrays can start with a lot of different delimiter types - # (e.g., %i| or %i<). - class QSymbolsBeg - # [String] the beginning of the array literal - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - def initialize(value:, location:) - @value = value - @location = location - end - end - # :call-seq: # on_qsymbols_beg: (String value) -> QSymbolsBeg def on_qsymbols_beg(value) @@ -10551,82 +2309,6 @@ def on_qsymbols_new ) end - # QWords represents a string literal array without interpolation. - # - # %w[one two three] - # - class QWords - # [QWordsBeg] the token that opens this array literal - attr_reader :beginning - - # [Array[ TStringContent ]] the elements of the array - attr_reader :elements - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(beginning:, elements:, location:, comments: []) - @beginning = beginning - @elements = elements - @location = location - @comments = comments - end - - def child_nodes - [] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - beginning: beginning, - elements: elements, - location: location, - comments: comments - } - end - - def format(q) - opening, closing = "%w[", "]" - - if elements.any? { |element| element.match?(/[\[\]]/) } - opening = beginning.value - closing = Quotes.matching(opening[2]) - end - - q.group(0, opening, closing) do - q.indent do - q.breakable("") - q.seplist(elements, -> { q.breakable }) do |element| - q.format(element) - end - end - q.breakable("") - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("qwords") - - q.breakable - q.group(2, "(", ")") { q.seplist(elements) { |element| q.pp(element) } } - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { type: :qwords, elems: elements, loc: location, cmts: comments }.to_json( - *opts - ) - end - end - # :call-seq: # on_qwords_add: (QWords qwords, TStringContent element) -> QWords def on_qwords_add(qwords, element) @@ -10637,97 +2319,25 @@ def on_qwords_add(qwords, element) ) end - # QWordsBeg represents the beginning of a string literal array. - # - # %w[one two three] - # - # In the snippet above, QWordsBeg represents the "%w[" token. Note that these - # kinds of arrays can start with a lot of different delimiter types (e.g., - # %w| or %w<). - class QWordsBeg - # [String] the beginning of the array literal - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - def initialize(value:, location:) - @value = value - @location = location - end - end - - # :call-seq: - # on_qwords_beg: (String value) -> QWordsBeg - def on_qwords_beg(value) - node = - QWordsBeg.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - - tokens << node - node - end - # :call-seq: - # on_qwords_new: () -> QWords - def on_qwords_new - beginning = find_token(QWordsBeg) - - QWords.new(beginning: beginning, elements: [], location: beginning.location) - end - - # RationalLiteral represents the use of a rational number literal. - # - # 1r - # - class RationalLiteral - # [String] the rational number literal - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(value:, location:, comments: []) - @value = value - @location = location - @comments = comments - end - - def child_nodes - [] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { value: value, location: location, comments: comments } - end - - def format(q) - q.text(value) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("rational") + # on_qwords_beg: (String value) -> QWordsBeg + def on_qwords_beg(value) + node = + QWordsBeg.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) - q.breakable - q.pp(value) + tokens << node + node + end - q.pp(Comment::List.new(comments)) - end - end + # :call-seq: + # on_qwords_new: () -> QWords + def on_qwords_new + beginning = find_token(QWordsBeg) - def to_json(*opts) - { type: :rational, value: value, loc: location, cmts: comments }.to_json( - *opts - ) - end + QWords.new(beginning: beginning, elements: [], location: beginning.location) end # :call-seq: @@ -10739,20 +2349,6 @@ def on_rational(value) ) end - # RBrace represents the use of a right brace, i.e., +++. - class RBrace - # [String] the right brace - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - def initialize(value:, location:) - @value = value - @location = location - end - end - # :call-seq: # on_rbrace: (String value) -> RBrace def on_rbrace(value) @@ -10766,20 +2362,6 @@ def on_rbrace(value) node end - # RBracket represents the use of a right bracket, i.e., +]+. - class RBracket - # [String] the right bracket - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - def initialize(value:, location:) - @value = value - @location = location - end - end - # :call-seq: # on_rbracket: (String value) -> RBracket def on_rbracket(value) @@ -10793,58 +2375,6 @@ def on_rbracket(value) node end - # Redo represents the use of the +redo+ keyword. - # - # redo - # - class Redo - # [String] the value of the keyword - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(value:, location:, comments: []) - @value = value - @location = location - @comments = comments - end - - def child_nodes - [] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { value: value, location: location, comments: comments } - end - - def format(q) - q.text(value) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("redo") - - q.breakable - q.pp(value) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { type: :redo, value: value, loc: location, cmts: comments }.to_json( - *opts - ) - end - end - # :call-seq: # on_redo: () -> Redo def on_redo @@ -10853,30 +2383,6 @@ def on_redo Redo.new(value: keyword.value, location: keyword.location) end - # RegexpContent represents the body of a regular expression. - # - # /.+ #{pattern} .+/ - # - # In the example above, a RegexpContent node represents everything contained - # within the forward slashes. - class RegexpContent - # [String] the opening of the regular expression - attr_reader :beginning - - # [Array[ StringDVar | StringEmbExpr | TStringContent ]] the parts of the - # regular expression - attr_reader :parts - - # [Location] the location of this node - attr_reader :location - - def initialize(beginning:, parts:, location:) - @beginning = beginning - @parts = parts - @location = location - end - end - # :call-seq: # on_regexp_add: ( # RegexpContent regexp_content, @@ -10890,28 +2396,6 @@ def on_regexp_add(regexp_content, part) ) end - # RegexpBeg represents the start of a regular expression literal. - # - # /.+/ - # - # In the example above, RegexpBeg represents the first / token. Regular - # expression literals can also be declared using the %r syntax, as in: - # - # %r{.+} - # - class RegexpBeg - # [String] the beginning of the regular expression - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - def initialize(value:, location:) - @value = value - @location = location - end - end - # :call-seq: # on_regexp_beg: (String value) -> RegexpBeg def on_regexp_beg(value) @@ -10925,29 +2409,6 @@ def on_regexp_beg(value) node end - # RegexpEnd represents the end of a regular expression literal. - # - # /.+/m - # - # In the example above, the RegexpEnd event represents the /m at the end of - # the regular expression literal. You can also declare regular expression - # literals using %r, as in: - # - # %r{.+}m - # - class RegexpEnd - # [String] the end of the regular expression - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - def initialize(value:, location:) - @value = value - @location = location - end - end - # :call-seq: # on_regexp_end: (String value) -> RegexpEnd def on_regexp_end(value) @@ -10957,134 +2418,6 @@ def on_regexp_end(value) ) end - # RegexpLiteral represents a regular expression literal. - # - # /.+/ - # - class RegexpLiteral - # [String] the beginning of the regular expression literal - attr_reader :beginning - - # [String] the ending of the regular expression literal - attr_reader :ending - - # [Array[ StringEmbExpr | StringDVar | TStringContent ]] the parts of the - # regular expression literal - attr_reader :parts - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(beginning:, ending:, parts:, location:, comments: []) - @beginning = beginning - @ending = ending - @parts = parts - @location = location - @comments = comments - end - - def child_nodes - parts - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - beginning: beginning, - ending: ending, - parts: parts, - location: location, - comments: comments - } - end - - def format(q) - braces = ambiguous?(q) || include?(%r{\/}) - - if braces && include?(/[{}]/) - q.group do - q.text(beginning) - q.format_each(parts) - q.text(ending) - end - elsif braces - q.group do - q.text("%r{") - - if beginning == "/" - # If we're changing from a forward slash to a %r{, then we can - # replace any escaped forward slashes with regular forward slashes. - parts.each do |part| - if part.is_a?(TStringContent) - q.text(part.value.gsub("\\/", "/")) - else - q.format(part) - end - end - else - q.format_each(parts) - end - - q.text("}") - q.text(ending[1..-1]) - end - else - q.group do - q.text("/") - q.format_each(parts) - q.text("/") - q.text(ending[1..-1]) - end - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("regexp_literal") - - q.breakable - q.group(2, "(", ")") { q.seplist(parts) { |part| q.pp(part) } } - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :regexp_literal, - beging: beginning, - ending: ending, - parts: parts, - loc: location, - cmts: comments - }.to_json(*opts) - end - - private - - def include?(pattern) - parts.any? do |part| - part.is_a?(TStringContent) && part.value.match?(pattern) - end - end - - # If the first part of this regex is plain string content, we have a space - # or an =, and we're contained within a command or command_call node, then - # we want to use braces because otherwise we could end up with an ambiguous - # operator, e.g. foo / bar/ or foo /=bar/ - def ambiguous?(q) - return false if parts.empty? - part = parts.first - - part.is_a?(TStringContent) && part.value.start_with?(" ", "=") && - q.parents.any? { |node| node.is_a?(Command) || node.is_a?(CommandCall) } - end - end - # :call-seq: # on_regexp_literal: ( # RegexpContent regexp_content, @@ -11111,213 +2444,6 @@ def on_regexp_new ) end - # RescueEx represents the list of exceptions being rescued in a rescue clause. - # - # begin - # rescue Exception => exception - # end - # - class RescueEx - # [untyped] the list of exceptions being rescued - attr_reader :exceptions - - # [nil | Field | VarField] the expression being used to capture the raised - # exception - attr_reader :variable - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(exceptions:, variable:, location:, comments: []) - @exceptions = exceptions - @variable = variable - @location = location - @comments = comments - end - - def child_nodes - [*exceptions, variable] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - exceptions: exceptions, - variable: variable, - location: location, - comments: comments - } - end - - def format(q) - q.group do - if exceptions - q.text(" ") - q.format(exceptions) - end - - if variable - q.text(" => ") - q.format(variable) - end - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("rescue_ex") - - q.breakable - q.pp(exceptions) - - q.breakable - q.pp(variable) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :rescue_ex, - extns: exceptions, - var: variable, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - - # Rescue represents the use of the rescue keyword inside of a BodyStmt node. - # - # begin - # rescue - # end - # - class Rescue - # [RescueEx] the exceptions being rescued - attr_reader :exception - - # [Statements] the expressions to evaluate when an error is rescued - attr_reader :statements - - # [nil | Rescue] the optional next clause in the chain - attr_reader :consequent - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize( - exception:, - statements:, - consequent:, - location:, - comments: [] - ) - @exception = exception - @statements = statements - @consequent = consequent - @location = location - @comments = comments - end - - def bind_end(end_char) - @location = - Location.new( - start_line: location.start_line, - start_char: location.start_char, - end_line: location.end_line, - end_char: end_char - ) - - if consequent - consequent.bind_end(end_char) - statements.bind_end(consequent.location.start_char) - else - statements.bind_end(end_char) - end - end - - def child_nodes - [exception, statements, consequent] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - exception: exception, - statements: statements, - consequent: consequent, - location: location, - comments: comments - } - end - - def format(q) - q.group do - q.text("rescue") - - if exception - q.nest("rescue ".length) { q.format(exception) } - else - q.text(" StandardError") - end - - unless statements.empty? - q.indent do - q.breakable(force: true) - q.format(statements) - end - end - - if consequent - q.breakable(force: true) - q.format(consequent) - end - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("rescue") - - if exception - q.breakable - q.pp(exception) - end - - q.breakable - q.pp(statements) - - if consequent - q.breakable - q.pp(consequent) - end - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :rescue, - extn: exception, - stmts: statements, - cons: consequent, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_rescue: ( # (nil | [untyped] | MRHS | MRHSAddStar) exceptions, @@ -11367,150 +2493,16 @@ def on_rescue(exceptions, variable, statements, consequent) ) end - # RescueMod represents the use of the modifier form of a +rescue+ clause. - # - # expression rescue value - # - class RescueMod - # [untyped] the expression to execute - attr_reader :statement - - # [untyped] the value to use if the executed expression raises an error - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(statement:, value:, location:, comments: []) - @statement = statement - @value = value - @location = location - @comments = comments - end - - def child_nodes - [statement, value] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - statement: statement, - value: value, - location: location, - comments: comments - } - end - - def format(q) - q.group(0, "begin", "end") do - q.indent do - q.breakable(force: true) - q.format(statement) - end - q.breakable(force: true) - q.text("rescue StandardError") - q.indent do - q.breakable(force: true) - q.format(value) - end - q.breakable(force: true) - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("rescue_mod") - - q.breakable - q.pp(statement) - - q.breakable - q.pp(value) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :rescue_mod, - stmt: statement, - value: value, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_rescue_mod: (untyped statement, untyped value) -> RescueMod def on_rescue_mod(statement, value) - find_token(Kw, "rescue") - - RescueMod.new( - statement: statement, - value: value, - location: statement.location.to(value.location) - ) - end - - # RestParam represents defining a parameter in a method definition that - # accepts all remaining positional parameters. - # - # def method(*rest) end - # - class RestParam - # [nil | Ident] the name of the parameter - attr_reader :name - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(name:, location:, comments: []) - @name = name - @location = location - @comments = comments - end - - def child_nodes - [name] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { name: name, location: location, comments: comments } - end - - def format(q) - q.text("*") - q.format(name) if name - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("rest_param") - - q.breakable - q.pp(name) - - q.pp(Comment::List.new(comments)) - end - end + find_token(Kw, "rescue") - def to_json(*opts) - { type: :rest_param, name: name, loc: location, cmts: comments }.to_json( - *opts - ) - end + RescueMod.new( + statement: statement, + value: value, + location: statement.location.to(value.location) + ) end # :call-seq: @@ -11522,58 +2514,6 @@ def on_rest_param(name) RestParam.new(name: name, location: location) end - # Retry represents the use of the +retry+ keyword. - # - # retry - # - class Retry - # [String] the value of the keyword - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(value:, location:, comments: []) - @value = value - @location = location - @comments = comments - end - - def child_nodes - [] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { value: value, location: location, comments: comments } - end - - def format(q) - q.text(value) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("retry") - - q.breakable - q.pp(value) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { type: :retry, value: value, loc: location, cmts: comments }.to_json( - *opts - ) - end - end - # :call-seq: # on_retry: () -> Retry def on_retry @@ -11582,58 +2522,6 @@ def on_retry Retry.new(value: keyword.value, location: keyword.location) end - # Return represents using the +return+ keyword with arguments. - # - # return value - # - class Return - # [Args] the arguments being passed to the keyword - attr_reader :arguments - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(arguments:, location:, comments: []) - @arguments = arguments - @location = location - @comments = comments - end - - def child_nodes - [arguments] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { arguments: arguments, location: location, comments: comments } - end - - def format(q) - FlowControlFormatter.new("return", self).format(q) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("return") - - q.breakable - q.pp(arguments) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { type: :return, args: arguments, loc: location, cmts: comments }.to_json( - *opts - ) - end - end - # :call-seq: # on_return: (Args arguments) -> Return def on_return(arguments) @@ -11645,58 +2533,6 @@ def on_return(arguments) ) end - # Return0 represents the bare +return+ keyword with no arguments. - # - # return - # - class Return0 - # [String] the value of the keyword - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(value:, location:, comments: []) - @value = value - @location = location - @comments = comments - end - - def child_nodes - [] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { value: value, location: location, comments: comments } - end - - def format(q) - q.text(value) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("return0") - - q.breakable - q.pp(value) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { type: :return0, value: value, loc: location, cmts: comments }.to_json( - *opts - ) - end - end - # :call-seq: # on_return0: () -> Return0 def on_return0 @@ -11705,20 +2541,6 @@ def on_return0 Return0.new(value: keyword.value, location: keyword.location) end - # RParen represents the use of a right parenthesis, i.e., +)+. - class RParen - # [String] the parenthesis - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - def initialize(value:, location:) - @value = value - @location = location - end - end - # :call-seq: # on_rparen: (String value) -> RParen def on_rparen(value) @@ -11732,84 +2554,6 @@ def on_rparen(value) node end - # SClass represents a block of statements that should be evaluated within the - # context of the singleton class of an object. It's frequently used to define - # singleton methods. - # - # class << self - # end - # - class SClass - # [untyped] the target of the singleton class to enter - attr_reader :target - - # [BodyStmt] the expressions to be executed - attr_reader :bodystmt - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(target:, bodystmt:, location:, comments: []) - @target = target - @bodystmt = bodystmt - @location = location - @comments = comments - end - - def child_nodes - [target, bodystmt] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - target: target, - bodystmt: bodystmt, - location: location, - comments: comments - } - end - - def format(q) - q.group(0, "class << ", "end") do - q.format(target) - q.indent do - q.breakable(force: true) - q.format(bodystmt) - end - q.breakable(force: true) - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("sclass") - - q.breakable - q.pp(target) - - q.breakable - q.pp(bodystmt) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :sclass, - target: target, - bodystmt: bodystmt, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_sclass: (untyped target, BodyStmt bodystmt) -> SClass def on_sclass(target, bodystmt) @@ -11836,187 +2580,6 @@ def on_sclass(target, bodystmt) # value # end - # Everything that has a block of code inside of it has a list of statements. - # Normally we would just track those as a node that has an array body, but we - # have some special handling in order to handle empty statement lists. They - # need to have the right location information, so all of the parent node of - # stmts nodes will report back down the location information. We then - # propagate that onto void_stmt nodes inside the stmts in order to make sure - # all comments get printed appropriately. - class Statements - # [SyntaxTree] the parser that is generating this node - attr_reader :parser - - # [Array[ untyped ]] the list of expressions contained within this node - attr_reader :body - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(parser, body:, location:, comments: []) - @parser = parser - @body = body - @location = location - @comments = comments - end - - def bind(start_char, end_char) - @location = - Location.new( - start_line: location.start_line, - start_char: start_char, - end_line: location.end_line, - end_char: end_char - ) - - if body[0].is_a?(VoidStmt) - location = body[0].location - location = - Location.new( - start_line: location.start_line, - start_char: start_char, - end_line: location.end_line, - end_char: start_char - ) - - body[0] = VoidStmt.new(location: location) - end - - attach_comments(start_char, end_char) - end - - def bind_end(end_char) - @location = - Location.new( - start_line: location.start_line, - start_char: location.start_char, - end_line: location.end_line, - end_char: end_char - ) - end - - def empty? - body.all? do |statement| - statement.is_a?(VoidStmt) && statement.comments.empty? - end - end - - def child_nodes - body - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { parser: parser, body: body, location: location, comments: comments } - end - - def format(q) - line = nil - - # This handles a special case where you've got a block of statements where - # the only value is a comment. In that case a lot of nodes like - # brace_block will attempt to format as a single line, but since that - # wouldn't work with a comment, we intentionally break the parent group. - if body.length == 2 - void_stmt, comment = body - - if void_stmt.is_a?(VoidStmt) && comment.is_a?(Comment) - q.format(comment) - q.break_parent - return - end - end - - access_controls = - Hash.new do |hash, node| - hash[node] = node.is_a?(VCall) && - %w[private protected public].include?(node.value.value) - end - - body.each_with_index do |statement, index| - next if statement.is_a?(VoidStmt) - - if line.nil? - q.format(statement) - elsif (statement.location.start_line - line) > 1 - q.breakable(force: true) - q.breakable(force: true) - q.format(statement) - elsif access_controls[statement] || access_controls[body[index - 1]] - q.breakable(force: true) - q.breakable(force: true) - q.format(statement) - elsif statement.location.start_line != line - q.breakable(force: true) - q.format(statement) - elsif !q.parent.is_a?(StringEmbExpr) - q.breakable(force: true) - q.format(statement) - else - q.text("; ") - q.format(statement) - end - - line = statement.location.end_line - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("statements") - - q.breakable - q.seplist(body) { |statement| q.pp(statement) } - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { type: :statements, body: body, loc: location, cmts: comments }.to_json( - *opts - ) - end - - private - - # As efficiently as possible, gather up all of the comments that have been - # found while this statements list was being parsed and add them into the - # body. - def attach_comments(start_char, end_char) - parser_comments = parser.comments - - comment_index = 0 - body_index = 0 - - while comment_index < parser_comments.size - comment = parser_comments[comment_index] - location = comment.location - - if !comment.inline? && (start_char <= location.start_char) && - (end_char >= location.end_char) && !comment.ignore? - parser_comments.delete_at(comment_index) - - while (node = body[body_index]) && - ( - node.is_a?(VoidStmt) || - node.location.start_char < location.start_char - ) - body_index += 1 - end - - body.insert(body_index, comment) - else - comment_index += 1 - end - end - end - end - # stmts_add is a parser event that represents a single statement inside a # list of statements within any lexical block. It accepts as arguments the # parent stmts node as well as an stmt which can be any expression in @@ -12042,24 +2605,6 @@ def on_stmts_new ) end - # StringContent represents the contents of a string-like value. - # - # "string" - # - class StringContent - # [Array[ StringEmbExpr | StringDVar | TStringContent ]] the parts of the - # string - attr_reader :parts - - # [Location] the location of this node - attr_reader :location - - def initialize(parts:, location:) - @parts = parts - @location = location - end - end - # :call-seq: # on_string_add: ( # String string, @@ -12072,157 +2617,26 @@ def on_string_add(string, part) StringContent.new(parts: string.parts << part, location: location) end - # StringConcat represents concatenating two strings together using a backward - # slash. - # - # "first" \ - # "second" - # - class StringConcat - # [StringConcat | StringLiteral] the left side of the concatenation - attr_reader :left - - # [StringLiteral] the right side of the concatenation - attr_reader :right - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(left:, right:, location:, comments: []) - @left = left - @right = right - @location = location - @comments = comments - end - - def child_nodes - [left, right] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { left: left, right: right, location: location, comments: comments } - end - - def format(q) - q.group do - q.format(left) - q.text(' \\') - q.indent do - q.breakable(force: true) - q.format(right) - end - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("string_concat") - - q.breakable - q.pp(left) - - q.breakable - q.pp(right) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :string_concat, - left: left, - right: right, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_string_concat: ( - # (StringConcat | StringLiteral) left, - # StringLiteral right - # ) -> StringConcat - def on_string_concat(left, right) - StringConcat.new( - left: left, - right: right, - location: left.location.to(right.location) - ) - end - - # :call-seq: - # on_string_content: () -> StringContent - def on_string_content - StringContent.new( - parts: [], - location: Location.fixed(line: lineno, char: char_pos) - ) - end - - # StringDVar represents shorthand interpolation of a variable into a string. - # It allows you to take an instance variable, class variable, or global - # variable and omit the braces when interpolating. - # - # "#@variable" - # - class StringDVar - # [Backref | VarRef] the variable being interpolated - attr_reader :variable - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(variable:, location:, comments: []) - @variable = variable - @location = location - @comments = comments - end - - def child_nodes - [variable] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { variable: variable, location: location, comments: comments } - end - - def format(q) - q.text('#{') - q.format(variable) - q.text("}") - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("string_dvar") - - q.breakable - q.pp(variable) - - q.pp(Comment::List.new(comments)) - end - end + # (StringConcat | StringLiteral) left, + # StringLiteral right + # ) -> StringConcat + def on_string_concat(left, right) + StringConcat.new( + left: left, + right: right, + location: left.location.to(right.location) + ) + end - def to_json(*opts) - { - type: :string_dvar, - var: variable, - loc: location, - cmts: comments - }.to_json(*opts) - end + # :call-seq: + # on_string_content: () -> StringContent + def on_string_content + StringContent.new( + parts: [], + location: Location.fixed(line: lineno, char: char_pos) + ) end # :call-seq: @@ -12236,80 +2650,6 @@ def on_string_dvar(variable) ) end - # StringEmbExpr represents interpolated content. It can be contained within a - # couple of different parent nodes, including regular expressions, strings, - # and dynamic symbols. - # - # "string #{expression}" - # - class StringEmbExpr - # [Statements] the expressions to be interpolated - attr_reader :statements - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(statements:, location:, comments: []) - @statements = statements - @location = location - @comments = comments - end - - def child_nodes - [statements] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { statements: statements, location: location, comments: comments } - end - - def format(q) - if location.start_line == location.end_line - # If the contents of this embedded expression were originally on the - # same line in the source, then we're going to leave them in place and - # assume that's the way the developer wanted this expression - # represented. - doc = q.group(0, '#{', "}") { q.format(statements) } - RemoveBreaks.call(doc) - else - q.group do - q.text('#{') - q.indent do - q.breakable("") - q.format(statements) - end - q.breakable("") - q.text("}") - end - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("string_embexpr") - - q.breakable - q.pp(statements) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :string_embexpr, - stmts: statements, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_string_embexpr: (Statements statements) -> StringEmbExpr def on_string_embexpr(statements) @@ -12335,93 +2675,6 @@ def on_string_embexpr(statements) StringEmbExpr.new(statements: statements, location: location) end - # StringLiteral represents a string literal. - # - # "string" - # - class StringLiteral - # [Array[ StringEmbExpr | StringDVar | TStringContent ]] the parts of the - # string literal - attr_reader :parts - - # [String] which quote was used by the string literal - attr_reader :quote - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(parts:, quote:, location:, comments: []) - @parts = parts - @quote = quote - @location = location - @comments = comments - end - - def child_nodes - parts - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { parts: parts, quote: quote, location: location, comments: comments } - end - - def format(q) - if parts.empty? - q.text("#{q.quote}#{q.quote}") - return - end - - opening_quote, closing_quote = - if !Quotes.locked?(self) - [q.quote, q.quote] - elsif quote.start_with?("%") - [quote, Quotes.matching(quote[/%[qQ]?(.)/, 1])] - else - [quote, quote] - end - - q.group(0, opening_quote, closing_quote) do - parts.each do |part| - if part.is_a?(TStringContent) - value = Quotes.normalize(part.value, closing_quote) - separator = -> { q.breakable(force: true, indent: false) } - q.seplist(value.split(/\r?\n/, -1), separator) do |text| - q.text(text) - end - else - q.format(part) - end - end - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("string_literal") - - q.breakable - q.group(2, "(", ")") { q.seplist(parts) { |part| q.pp(part) } } - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :string_literal, - parts: parts, - quote: quote, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_string_literal: (String string) -> Heredoc | StringLiteral def on_string_literal(string) @@ -12459,68 +2712,6 @@ def on_string_literal(string) end end - # Super represents using the +super+ keyword with arguments. It can optionally - # use parentheses. - # - # super(value) - # - class Super - # [ArgParen | Args] the arguments to the keyword - attr_reader :arguments - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(arguments:, location:, comments: []) - @arguments = arguments - @location = location - @comments = comments - end - - def child_nodes - [arguments] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { arguments: arguments, location: location, comments: comments } - end - - def format(q) - q.group do - q.text("super") - - if arguments.is_a?(ArgParen) - q.format(arguments) - else - q.text(" ") - q.nest("super ".length) { q.format(arguments) } - end - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("super") - - q.breakable - q.pp(arguments) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { type: :super, args: arguments, loc: location, cmts: comments }.to_json( - *opts - ) - end - end - # :call-seq: # on_super: ((ArgParen | Args) arguments) -> Super def on_super(arguments) @@ -12532,35 +2723,6 @@ def on_super(arguments) ) end - # SymBeg represents the beginning of a symbol literal. - # - # :symbol - # - # SymBeg is also used for dynamic symbols, as in: - # - # :"symbol" - # - # Finally, SymBeg is also used for symbols using the %s syntax, as in: - # - # %s[symbol] - # - # The value of this node is a string. In most cases (as in the first example - # above) it will contain just ":". In the case of dynamic symbols it will - # contain ":'" or ":\"". In the case of %s symbols, it will contain the start - # of the symbol including the %s and the delimiter. - class SymBeg - # [String] the beginning of the symbol - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - def initialize(value:, location:) - @value = value - @location = location - end - end - # symbeg is a token that represents the beginning of a symbol literal. # In most cases it will contain just ":" as in the value, but if its a dynamic # symbol being defined it will contain ":'" or ":\"". @@ -12575,25 +2737,6 @@ def on_symbeg(value) node end - # SymbolContent represents symbol contents and is always the child of a - # SymbolLiteral node. - # - # :symbol - # - class SymbolContent - # [Backtick | Const | CVar | GVar | Ident | IVar | Kw | Op] the value of the - # symbol - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - def initialize(value:, location:) - @value = value - @location = location - end - end - # :call-seq: # on_symbol: ( # (Backtick | Const | CVar | GVar | Ident | IVar | Kw | Op) value @@ -12604,64 +2747,6 @@ def on_symbol(value) SymbolContent.new(value: value, location: value.location) end - # SymbolLiteral represents a symbol in the system with no interpolation - # (as opposed to a DynaSymbol which has interpolation). - # - # :symbol - # - class SymbolLiteral - # [Backtick | Const | CVar | GVar | Ident | IVar | Kw | Op] the value of the - # symbol - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(value:, location:, comments: []) - @value = value - @location = location - @comments = comments - end - - def child_nodes - [value] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { value: value, location: location, comments: comments } - end - - def format(q) - q.text(":") - q.format(value) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("symbol_literal") - - q.breakable - q.pp(value) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :symbol_literal, - value: value, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_symbol_literal: ( # ( @@ -12683,85 +2768,6 @@ def on_symbol_literal(value) end end - # Symbols represents a symbol array literal with interpolation. - # - # %I[one two three] - # - class Symbols - # [SymbolsBeg] the token that opens this array literal - attr_reader :beginning - - # [Array[ Word ]] the words in the symbol array literal - attr_reader :elements - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(beginning:, elements:, location:, comments: []) - @beginning = beginning - @elements = elements - @location = location - @comments = comments - end - - def child_nodes - [] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - beginning: beginning, - elements: elements, - location: location, - comments: comments - } - end - - def format(q) - opening, closing = "%I[", "]" - - if elements.any? { |element| element.match?(/[\[\]]/) } - opening = beginning.value - closing = Quotes.matching(opening[2]) - end - - q.group(0, opening, closing) do - q.indent do - q.breakable("") - q.seplist(elements, -> { q.breakable }) do |element| - q.format(element) - end - end - q.breakable("") - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("symbols") - - q.breakable - q.group(2, "(", ")") { q.seplist(elements) { |element| q.pp(element) } } - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :symbols, - elems: elements, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_symbols_add: (Symbols symbols, Word word) -> Symbols def on_symbols_add(symbols, word) @@ -12772,27 +2778,6 @@ def on_symbols_add(symbols, word) ) end - # SymbolsBeg represents the start of a symbol array literal with - # interpolation. - # - # %I[one two three] - # - # In the snippet above, SymbolsBeg represents the "%I[" token. Note that these - # kinds of arrays can start with a lot of different delimiter types - # (e.g., %I| or %I<). - class SymbolsBeg - # [String] the beginning of the symbol literal array - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - def initialize(value:, location:) - @value = value - @location = location - end - end - # :call-seq: # on_symbols_beg: (String value) -> SymbolsBeg def on_symbols_beg(value) @@ -12818,24 +2803,6 @@ def on_symbols_new ) end - # TLambda represents the beginning of a lambda literal. - # - # -> { value } - # - # In the example above the TLambda represents the +->+ operator. - class TLambda - # [String] the beginning of the lambda literal - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - def initialize(value:, location:) - @value = value - @location = location - end - end - # :call-seq: # on_tlambda: (String value) -> TLambda def on_tlambda(value) @@ -12849,25 +2816,6 @@ def on_tlambda(value) node end - # TLamBeg represents the beginning of the body of a lambda literal using - # braces. - # - # -> { value } - # - # In the example above the TLamBeg represents the +{+ operator. - class TLamBeg - # [String] the beginning of the body of the lambda literal - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - def initialize(value:, location:) - @value = value - @location = location - end - end - # :call-seq: # on_tlambeg: (String value) -> TLamBeg def on_tlambeg(value) @@ -12881,64 +2829,6 @@ def on_tlambeg(value) node end - # TopConstField is always the child node of some kind of assignment. It - # represents when you're assigning to a constant that is being referenced at - # the top level. - # - # ::Constant = value - # - class TopConstField - # [Const] the constant being assigned - attr_reader :constant - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(constant:, location:, comments: []) - @constant = constant - @location = location - @comments = comments - end - - def child_nodes - [constant] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { constant: constant, location: location, comments: comments } - end - - def format(q) - q.text("::") - q.format(constant) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("top_const_field") - - q.breakable - q.pp(constant) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :top_const_field, - constant: constant, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_top_const_field: (Const constant) -> TopConstRef def on_top_const_field(constant) @@ -12950,95 +2840,15 @@ def on_top_const_field(constant) ) end - # TopConstRef is very similar to TopConstField except that it is not involved - # in an assignment. - # - # ::Constant - # - class TopConstRef - # [Const] the constant being referenced - attr_reader :constant - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(constant:, location:, comments: []) - @constant = constant - @location = location - @comments = comments - end - - def child_nodes - [constant] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { constant: constant, location: location, comments: comments } - end - - def format(q) - q.text("::") - q.format(constant) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("top_const_ref") - - q.breakable - q.pp(constant) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :top_const_ref, - constant: constant, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: - # on_top_const_ref: (Const constant) -> TopConstRef - def on_top_const_ref(constant) - operator = find_colon2_before(constant) - - TopConstRef.new( - constant: constant, - location: operator.location.to(constant.location) - ) - end - - # TStringBeg represents the beginning of a string literal. - # - # "string" - # - # In the example above, TStringBeg represents the first set of quotes. Strings - # can also use single quotes. They can also be declared using the +%q+ and - # +%Q+ syntax, as in: - # - # %q{string} - # - class TStringBeg - # [String] the beginning of the string - attr_reader :value - - # [Location] the location of this node - attr_reader :location + # on_top_const_ref: (Const constant) -> TopConstRef + def on_top_const_ref(constant) + operator = find_colon2_before(constant) - def initialize(value:, location:) - @value = value - @location = location - end + TopConstRef.new( + constant: constant, + location: operator.location.to(constant.location) + ) end # :call-seq: @@ -13054,69 +2864,6 @@ def on_tstring_beg(value) node end - # TStringContent represents plain characters inside of an entity that accepts - # string content like a string, heredoc, command string, or regular - # expression. - # - # "string" - # - # In the example above, TStringContent represents the +string+ token contained - # within the string. - class TStringContent - # [String] the content of the string - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(value:, location:, comments: []) - @value = value - @location = location - @comments = comments - end - - def match?(pattern) - value.match?(pattern) - end - - def child_nodes - [] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { value: value, location: location, comments: comments } - end - - def format(q) - q.text(value) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("tstring_content") - - q.breakable - q.pp(value) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :tstring_content, - value: value.force_encoding("UTF-8"), - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_tstring_content: (String value) -> TStringContent def on_tstring_content(value) @@ -13126,29 +2873,6 @@ def on_tstring_content(value) ) end - # TStringEnd represents the end of a string literal. - # - # "string" - # - # In the example above, TStringEnd represents the second set of quotes. - # Strings can also use single quotes. They can also be declared using the +%q+ - # and +%Q+ syntax, as in: - # - # %q{string} - # - class TStringEnd - # [String] the end of the string - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - def initialize(value:, location:) - @value = value - @location = location - end - end - # :call-seq: # on_tstring_end: (String value) -> TStringEnd def on_tstring_end(value) @@ -13162,143 +2886,6 @@ def on_tstring_end(value) node end - # Not represents the unary +not+ method being called on an expression. - # - # not value - # - class Not - # [untyped] the statement on which to operate - attr_reader :statement - - # [boolean] whether or not parentheses were used - attr_reader :parentheses - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(statement:, parentheses:, location:, comments: []) - @statement = statement - @parentheses = parentheses - @location = location - @comments = comments - end - - def child_nodes - [statement] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - statement: statement, - parentheses: parentheses, - location: location, - comments: comments - } - end - - def format(q) - q.text(parentheses ? "not(" : "not ") - q.format(statement) - q.text(")") if parentheses - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("not") - - q.breakable - q.pp(statement) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :not, - value: statement, - paren: parentheses, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - - # Unary represents a unary method being called on an expression, as in +!+ or - # +~+. - # - # !value - # - class Unary - # [String] the operator being used - attr_reader :operator - - # [untyped] the statement on which to operate - attr_reader :statement - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(operator:, statement:, location:, comments: []) - @operator = operator - @statement = statement - @location = location - @comments = comments - end - - def child_nodes - [statement] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - operator: operator, - statement: statement, - location: location, - comments: comments - } - end - - def format(q) - q.text(operator) - q.format(statement) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("unary") - - q.breakable - q.pp(operator) - - q.breakable - q.pp(statement) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :unary, - op: operator, - value: statement, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_unary: (:not operator, untyped statement) -> Not # | (Symbol operator, untyped statement) -> Unary @@ -13345,87 +2932,6 @@ def on_unary(operator, statement) end end - # Undef represents the use of the +undef+ keyword. - # - # undef method - # - class Undef - class UndefArgumentFormatter - # [DynaSymbol | SymbolLiteral] the symbol to undefine - attr_reader :node - - def initialize(node) - @node = node - end - - def comments - if node.is_a?(SymbolLiteral) - node.comments + node.value.comments - else - node.comments - end - end - - def format(q) - node.is_a?(SymbolLiteral) ? q.format(node.value) : q.format(node) - end - end - - # [Array[ DynaSymbol | SymbolLiteral ]] the symbols to undefine - attr_reader :symbols - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(symbols:, location:, comments: []) - @symbols = symbols - @location = location - @comments = comments - end - - def child_nodes - symbols - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { symbols: symbols, location: location, comments: comments } - end - - def format(q) - keyword = "undef " - formatters = symbols.map { |symbol| UndefArgumentFormatter.new(symbol) } - - q.group do - q.text(keyword) - q.nest(keyword.length) do - q.seplist(formatters) { |formatter| q.format(formatter) } - end - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("undef") - - q.breakable - q.group(2, "(", ")") { q.seplist(symbols) { |symbol| q.pp(symbol) } } - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { type: :undef, syms: symbols, loc: location, cmts: comments }.to_json( - *opts - ) - end - end - # :call-seq: # on_undef: (Array[DynaSymbol | SymbolLiteral] symbols) -> Undef def on_undef(symbols) @@ -13434,322 +2940,39 @@ def on_undef(symbols) Undef.new( symbols: symbols, location: keyword.location.to(symbols.last.location) - ) - end - - # Unless represents the first clause in an +unless+ chain. - # - # unless predicate - # end - # - class Unless - # [untyped] the expression to be checked - attr_reader :predicate - - # [Statements] the expressions to be executed - attr_reader :statements - - # [nil, Elsif, Else] the next clause in the chain - attr_reader :consequent - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize( - predicate:, - statements:, - consequent:, - location:, - comments: [] - ) - @predicate = predicate - @statements = statements - @consequent = consequent - @location = location - @comments = comments - end - - def child_nodes - [predicate, statements, consequent] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - predicate: predicate, - statements: statements, - consequent: consequent, - location: location, - comments: comments - } - end - - def format(q) - ConditionalFormatter.new("unless", self).format(q) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("unless") - - q.breakable - q.pp(predicate) - - q.breakable - q.pp(statements) - - if consequent - q.breakable - q.pp(consequent) - end - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :unless, - pred: predicate, - stmts: statements, - cons: consequent, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - - # :call-seq: - # on_unless: ( - # untyped predicate, - # Statements statements, - # ((nil | Elsif | Else) consequent) - # ) -> Unless - def on_unless(predicate, statements, consequent) - beginning = find_token(Kw, "unless") - ending = consequent || find_token(Kw, "end") - - statements.bind(predicate.location.end_char, ending.location.start_char) - - Unless.new( - predicate: predicate, - statements: statements, - consequent: consequent, - location: beginning.location.to(ending.location) - ) - end - - # UnlessMod represents the modifier form of an +unless+ statement. - # - # expression unless predicate - # - class UnlessMod - # [untyped] the expression to be executed - attr_reader :statement - - # [untyped] the expression to be checked - attr_reader :predicate - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(statement:, predicate:, location:, comments: []) - @statement = statement - @predicate = predicate - @location = location - @comments = comments - end - - def child_nodes - [statement, predicate] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - statement: statement, - predicate: predicate, - location: location, - comments: comments - } - end - - def format(q) - ConditionalModFormatter.new("unless", self).format(q) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("unless_mod") - - q.breakable - q.pp(statement) - - q.breakable - q.pp(predicate) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :unless_mod, - stmt: statement, - pred: predicate, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - - # :call-seq: - # on_unless_mod: (untyped predicate, untyped statement) -> UnlessMod - def on_unless_mod(predicate, statement) - find_token(Kw, "unless") - - UnlessMod.new( - statement: statement, - predicate: predicate, - location: statement.location.to(predicate.location) - ) - end - - # Formats an Until, UntilMod, While, or WhileMod node. - class LoopFormatter - # [String] the name of the keyword used for this loop - attr_reader :keyword - - # [Until | UntilMod | While | WhileMod] the node that is being formatted - attr_reader :node - - # [untyped] the statements associated with the node - attr_reader :statements - - def initialize(keyword, node, statements) - @keyword = keyword - @node = node - @statements = statements - end - - def format(q) - if ContainsAssignment.call(node.predicate) - format_break(q) - q.break_parent - return - end - - q.group do - q.if_break { format_break(q) }.if_flat do - Parentheses.flat(q) do - q.format(statements) - q.text(" #{keyword} ") - q.format(node.predicate) - end - end - end - end - - private - - def format_break(q) - q.text("#{keyword} ") - q.nest(keyword.length + 1) { q.format(node.predicate) } - q.indent do - q.breakable("") - q.format(statements) - end - q.breakable("") - q.text("end") - end - end - - # Until represents an +until+ loop. - # - # until predicate - # end - # - class Until - # [untyped] the expression to be checked - attr_reader :predicate - - # [Statements] the expressions to be executed - attr_reader :statements - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(predicate:, statements:, location:, comments: []) - @predicate = predicate - @statements = statements - @location = location - @comments = comments - end - - def child_nodes - [predicate, statements] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - predicate: predicate, - statements: statements, - location: location, - comments: comments - } - end - - def format(q) - if statements.empty? - keyword = "until " - - q.group do - q.text(keyword) - q.nest(keyword.length) { q.format(predicate) } - q.breakable(force: true) - q.text("end") - end - else - LoopFormatter.new("until", self, statements).format(q) - end - end + ) + end - def pretty_print(q) - q.group(2, "(", ")") do - q.text("until") + # :call-seq: + # on_unless: ( + # untyped predicate, + # Statements statements, + # ((nil | Elsif | Else) consequent) + # ) -> Unless + def on_unless(predicate, statements, consequent) + beginning = find_token(Kw, "unless") + ending = consequent || find_token(Kw, "end") - q.breakable - q.pp(predicate) + statements.bind(predicate.location.end_char, ending.location.start_char) - q.breakable - q.pp(statements) + Unless.new( + predicate: predicate, + statements: statements, + consequent: consequent, + location: beginning.location.to(ending.location) + ) + end - q.pp(Comment::List.new(comments)) - end - end + # :call-seq: + # on_unless_mod: (untyped predicate, untyped statement) -> UnlessMod + def on_unless_mod(predicate, statement) + find_token(Kw, "unless") - def to_json(*opts) - { - type: :until, - pred: predicate, - stmts: statements, - loc: location, - cmts: comments - }.to_json(*opts) - end + UnlessMod.new( + statement: statement, + predicate: predicate, + location: statement.location.to(predicate.location) + ) end # :call-seq: @@ -13776,94 +2999,6 @@ def on_until(predicate, statements) ) end - # UntilMod represents the modifier form of a +until+ loop. - # - # expression until predicate - # - class UntilMod - # [untyped] the expression to be executed - attr_reader :statement - - # [untyped] the expression to be checked - attr_reader :predicate - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(statement:, predicate:, location:, comments: []) - @statement = statement - @predicate = predicate - @location = location - @comments = comments - end - - def child_nodes - [statement, predicate] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - statement: statement, - predicate: predicate, - location: location, - comments: comments - } - end - - def format(q) - # If we're in the modifier form and we're modifying a `begin`, then this - # is a special case where we need to explicitly use the modifier form - # because otherwise the semantic meaning changes. This looks like: - # - # begin - # foo - # end until bar - # - # Also, if the statement of the modifier includes an assignment, then we - # can't know for certain that it won't impact the predicate, so we need to - # force it to stay as it is. This looks like: - # - # foo = bar until foo - # - if statement.is_a?(Begin) || ContainsAssignment.call(statement) - q.format(statement) - q.text(" until ") - q.format(predicate) - else - LoopFormatter.new("until", self, statement).format(q) - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("until_mod") - - q.breakable - q.pp(statement) - - q.breakable - q.pp(predicate) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :until_mod, - stmt: statement, - pred: predicate, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_until_mod: (untyped predicate, untyped statement) -> UntilMod def on_until_mod(predicate, statement) @@ -13876,75 +3011,6 @@ def on_until_mod(predicate, statement) ) end - # VarAlias represents when you're using the +alias+ keyword with global - # variable arguments. - # - # alias $new $old - # - class VarAlias - # [GVar] the new alias of the variable - attr_reader :left - - # [Backref | GVar] the current name of the variable to be aliased - attr_reader :right - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(left:, right:, location:, comments: []) - @left = left - @right = right - @location = location - @comments = comments - end - - def child_nodes - [left, right] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { left: left, right: right, location: location, comments: comments } - end - - def format(q) - keyword = "alias " - - q.text(keyword) - q.format(left) - q.text(" ") - q.format(right) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("var_alias") - - q.breakable - q.pp(left) - - q.breakable - q.pp(right) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :var_alias, - left: left, - right: right, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_var_alias: (GVar left, (Backref | GVar) right) -> VarAlias def on_var_alias(left, right) @@ -13957,60 +3023,6 @@ def on_var_alias(left, right) ) end - # VarField represents a variable that is being assigned a value. As such, it - # is always a child of an assignment type node. - # - # variable = value - # - # In the example above, the VarField node represents the +variable+ token. - class VarField - # [nil | Const | CVar | GVar | Ident | IVar] the target of this node - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(value:, location:, comments: []) - @value = value - @location = location - @comments = comments - end - - def child_nodes - [value] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { value: value, location: location, comments: comments } - end - - def format(q) - q.format(value) if value - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("var_field") - - q.breakable - q.pp(value) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { type: :var_field, value: value, loc: location, cmts: comments }.to_json( - *opts - ) - end - end - # :call-seq: # on_var_field: ( # (nil | Const | CVar | GVar | Ident | IVar) value @@ -14020,371 +3032,37 @@ def on_var_field(value) if value value.location else - # You can hit this pattern if you're assigning to a splat using pattern - # matching syntax in Ruby 2.7+ - Location.fixed(line: lineno, char: char_pos) - end - - VarField.new(value: value, location: location) - end - - # VarRef represents a variable reference. - # - # true - # - # This can be a plain local variable like the example above. It can also be a - # constant, a class variable, a global variable, an instance variable, a - # keyword (like +self+, +nil+, +true+, or +false+), or a numbered block - # variable. - class VarRef - # [Const | CVar | GVar | Ident | IVar | Kw] the value of this node - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(value:, location:, comments: []) - @value = value - @location = location - @comments = comments - end - - def child_nodes - [value] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { value: value, location: location, comments: comments } - end - - def format(q) - q.format(value) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("var_ref") - - q.breakable - q.pp(value) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { type: :var_ref, value: value, loc: location, cmts: comments }.to_json( - *opts - ) - end - end - - # PinnedVarRef represents a pinned variable reference within a pattern - # matching pattern. - # - # case value - # in ^variable - # end - # - # This can be a plain local variable like the example above. It can also be a - # a class variable, a global variable, or an instance variable. - class PinnedVarRef - # [VarRef] the value of this node - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(value:, location:, comments: []) - @value = value - @location = location - @comments = comments - end - - def child_nodes - [value] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { value: value, location: location, comments: comments } - end - - def format(q) - q.group do - q.text("^") - q.format(value) - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("pinned_var_ref") - - q.breakable - q.pp(value) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :pinned_var_ref, - value: value, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - - # :call-seq: - # on_var_ref: ((Const | CVar | GVar | Ident | IVar | Kw) value) -> VarRef - def on_var_ref(value) - pin = find_token(Op, "^", consume: false) - - if pin && pin.location.start_char == value.location.start_char - 1 - tokens.delete(pin) - PinnedVarRef.new(value: value, location: pin.location.to(value.location)) - else - VarRef.new(value: value, location: value.location) - end - end - - # VCall represent any plain named object with Ruby that could be either a - # local variable or a method call. - # - # variable - # - class VCall - # [Ident] the value of this expression - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(value:, location:, comments: []) - @value = value - @location = location - @comments = comments - end - - def child_nodes - [value] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { value: value, location: location, comments: comments } - end - - def format(q) - q.format(value) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("vcall") - - q.breakable - q.pp(value) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { type: :vcall, value: value, loc: location, cmts: comments }.to_json( - *opts - ) - end - end - - # :call-seq: - # on_vcall: (Ident ident) -> VCall - def on_vcall(ident) - VCall.new(value: ident, location: ident.location) - end - - # VoidStmt represents an empty lexical block of code. - # - # ;; - # - class VoidStmt - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(location:, comments: []) - @location = location - @comments = comments - end - - def child_nodes - [] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { location: location, comments: comments } - end - - def format(q) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("void_stmt") - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { type: :void_stmt, loc: location, cmts: comments }.to_json(*opts) - end - end - - # :call-seq: - # on_void_stmt: () -> VoidStmt - def on_void_stmt - VoidStmt.new(location: Location.fixed(line: lineno, char: char_pos)) - end - - # When represents a +when+ clause in a +case+ chain. - # - # case value - # when predicate - # end - # - class When - # [Args] the arguments to the when clause - attr_reader :arguments - - # [Statements] the expressions to be executed - attr_reader :statements - - # [nil | Else | When] the next clause in the chain - attr_reader :consequent - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize( - arguments:, - statements:, - consequent:, - location:, - comments: [] - ) - @arguments = arguments - @statements = statements - @consequent = consequent - @location = location - @comments = comments - end - - def child_nodes - [arguments, statements, consequent] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - arguments: arguments, - statements: statements, - consequent: consequent, - location: location, - comments: comments - } - end - - def format(q) - keyword = "when " - - q.group do - q.group do - q.text(keyword) - q.nest(keyword.length) do - if arguments.comments.any? - q.format(arguments) - else - separator = -> { q.group { q.comma_breakable } } - q.seplist(arguments.parts, separator) { |part| q.format(part) } - end - - # Very special case here. If you're inside of a when clause and the - # last argument to the predicate is and endless range, then you are - # forced to use the "then" keyword to make it parse properly. - last = arguments.parts.last - if (last.is_a?(Dot2) || last.is_a?(Dot3)) && !last.right - q.text(" then") - end - end - end - - unless statements.empty? - q.indent do - q.breakable(force: true) - q.format(statements) - end - end - - if consequent - q.breakable(force: true) - q.format(consequent) - end + # You can hit this pattern if you're assigning to a splat using pattern + # matching syntax in Ruby 2.7+ + Location.fixed(line: lineno, char: char_pos) end - end - def pretty_print(q) - q.group(2, "(", ")") do - q.text("when") - - q.breakable - q.pp(arguments) - - q.breakable - q.pp(statements) + VarField.new(value: value, location: location) + end - if consequent - q.breakable - q.pp(consequent) - end + # :call-seq: + # on_var_ref: ((Const | CVar | GVar | Ident | IVar | Kw) value) -> VarRef + def on_var_ref(value) + pin = find_token(Op, "^", consume: false) - q.pp(Comment::List.new(comments)) - end + if pin && pin.location.start_char == value.location.start_char - 1 + tokens.delete(pin) + PinnedVarRef.new(value: value, location: pin.location.to(value.location)) + else + VarRef.new(value: value, location: value.location) end + end - def to_json(*opts) - { - type: :when, - args: arguments, - stmts: statements, - cons: consequent, - loc: location, - cmts: comments - }.to_json(*opts) - end + # :call-seq: + # on_vcall: (Ident ident) -> VCall + def on_vcall(ident) + VCall.new(value: ident, location: ident.location) + end + + # :call-seq: + # on_void_stmt: () -> VoidStmt + def on_void_stmt + VoidStmt.new(location: Location.fixed(line: lineno, char: char_pos)) end # :call-seq: @@ -14416,86 +3094,6 @@ def on_when(arguments, statements, consequent) ) end - # While represents a +while+ loop. - # - # while predicate - # end - # - class While - # [untyped] the expression to be checked - attr_reader :predicate - - # [Statements] the expressions to be executed - attr_reader :statements - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(predicate:, statements:, location:, comments: []) - @predicate = predicate - @statements = statements - @location = location - @comments = comments - end - - def child_nodes - [predicate, statements] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - predicate: predicate, - statements: statements, - location: location, - comments: comments - } - end - - def format(q) - if statements.empty? - keyword = "while " - - q.group do - q.text(keyword) - q.nest(keyword.length) { q.format(predicate) } - q.breakable(force: true) - q.text("end") - end - else - LoopFormatter.new("while", self, statements).format(q) - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("while") - - q.breakable - q.pp(predicate) - - q.breakable - q.pp(statements) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :while, - pred: predicate, - stmts: statements, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_while: (untyped predicate, Statements statements) -> While def on_while(predicate, statements) @@ -14520,94 +3118,6 @@ def on_while(predicate, statements) ) end - # WhileMod represents the modifier form of a +while+ loop. - # - # expression while predicate - # - class WhileMod - # [untyped] the expression to be executed - attr_reader :statement - - # [untyped] the expression to be checked - attr_reader :predicate - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(statement:, predicate:, location:, comments: []) - @statement = statement - @predicate = predicate - @location = location - @comments = comments - end - - def child_nodes - [statement, predicate] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - statement: statement, - predicate: predicate, - location: location, - comments: comments - } - end - - def format(q) - # If we're in the modifier form and we're modifying a `begin`, then this - # is a special case where we need to explicitly use the modifier form - # because otherwise the semantic meaning changes. This looks like: - # - # begin - # foo - # end while bar - # - # Also, if the statement of the modifier includes an assignment, then we - # can't know for certain that it won't impact the predicate, so we need to - # force it to stay as it is. This looks like: - # - # foo = bar while foo - # - if statement.is_a?(Begin) || ContainsAssignment.call(statement) - q.format(statement) - q.text(" while ") - q.format(predicate) - else - LoopFormatter.new("while", self, statement).format(q) - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("while_mod") - - q.breakable - q.pp(statement) - - q.breakable - q.pp(predicate) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :while_mod, - stmt: statement, - pred: predicate, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_while_mod: (untyped predicate, untyped statement) -> WhileMod def on_while_mod(predicate, statement) @@ -14620,66 +3130,6 @@ def on_while_mod(predicate, statement) ) end - # Word represents an element within a special array literal that accepts - # interpolation. - # - # %W[a#{b}c xyz] - # - # In the example above, there would be two Word nodes within a parent Words - # node. - class Word - # [Array[ StringEmbExpr | StringDVar | TStringContent ]] the parts of the - # word - attr_reader :parts - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(parts:, location:, comments: []) - @parts = parts - @location = location - @comments = comments - end - - def match?(pattern) - parts.any? { |part| part.is_a?(TStringContent) && part.match?(pattern) } - end - - def child_nodes - parts - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { parts: parts, location: location, comments: comments } - end - - def format(q) - q.format_each(parts) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("word") - - q.breakable - q.group(2, "(", ")") { q.seplist(parts) { |part| q.pp(part) } } - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { type: :word, parts: parts, loc: location, cmts: comments }.to_json( - *opts - ) - end - end - # :call-seq: # on_word_add: ( # Word word, @@ -14698,82 +3148,6 @@ def on_word_new Word.new(parts: [], location: Location.fixed(line: lineno, char: char_pos)) end - # Words represents a string literal array with interpolation. - # - # %W[one two three] - # - class Words - # [WordsBeg] the token that opens this array literal - attr_reader :beginning - - # [Array[ Word ]] the elements of this array - attr_reader :elements - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(beginning:, elements:, location:, comments: []) - @beginning = beginning - @elements = elements - @location = location - @comments = comments - end - - def child_nodes - [] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { - beginning: beginning, - elements: elements, - location: location, - comments: comments - } - end - - def format(q) - opening, closing = "%W[", "]" - - if elements.any? { |element| element.match?(/[\[\]]/) } - opening = beginning.value - closing = Quotes.matching(opening[2]) - end - - q.group(0, opening, closing) do - q.indent do - q.breakable("") - q.seplist(elements, -> { q.breakable }) do |element| - q.format(element) - end - end - q.breakable("") - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("words") - - q.breakable - q.group(2, "(", ")") { q.seplist(elements) { |element| q.pp(element) } } - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { type: :words, elems: elements, loc: location, cmts: comments }.to_json( - *opts - ) - end - end - # :call-seq: # on_words_add: (Words words, Word word) -> Words def on_words_add(words, word) @@ -14784,27 +3158,6 @@ def on_words_add(words, word) ) end - # WordsBeg represents the beginning of a string literal array with - # interpolation. - # - # %W[one two three] - # - # In the snippet above, a WordsBeg would be created with the value of "%W[". - # Note that these kinds of arrays can start with a lot of different delimiter - # types (e.g., %W| or %W<). - class WordsBeg - # [String] the start of the word literal array - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - def initialize(value:, location:) - @value = value - @location = location - end - end - # :call-seq: # on_words_beg: (String value) -> WordsBeg def on_words_beg(value) @@ -14830,24 +3183,6 @@ def on_words_new # value # end - # XString represents the contents of an XStringLiteral. - # - # `ls` - # - class XString - # [Array[ StringEmbExpr | StringDVar | TStringContent ]] the parts of the - # xstring - attr_reader :parts - - # [Location] the location of this node - attr_reader :location - - def initialize(parts:, location:) - @parts = parts - @location = location - end - end - # :call-seq: # on_xstring_add: ( # XString xstring, @@ -14875,64 +3210,6 @@ def on_xstring_new XString.new(parts: [], location: location) end - # XStringLiteral represents a string that gets executed. - # - # `ls` - # - class XStringLiteral - # [Array[ StringEmbExpr | StringDVar | TStringContent ]] the parts of the - # xstring - attr_reader :parts - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(parts:, location:, comments: []) - @parts = parts - @location = location - @comments = comments - end - - def child_nodes - parts - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { parts: parts, location: location, comments: comments } - end - - def format(q) - q.text("`") - q.format_each(parts) - q.text("`") - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("xstring_literal") - - q.breakable - q.group(2, "(", ")") { q.seplist(parts) { |part| q.pp(part) } } - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { - type: :xstring_literal, - parts: parts, - loc: location, - cmts: comments - }.to_json(*opts) - end - end - # :call-seq: # on_xstring_literal: (XString xstring) -> Heredoc | XStringLiteral def on_xstring_literal(xstring) @@ -14955,72 +3232,6 @@ def on_xstring_literal(xstring) end end - # Yield represents using the +yield+ keyword with arguments. - # - # yield value - # - class Yield - # [Args | Paren] the arguments passed to the yield - attr_reader :arguments - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(arguments:, location:, comments: []) - @arguments = arguments - @location = location - @comments = comments - end - - def child_nodes - [arguments] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { arguments: arguments, location: location, comments: comments } - end - - def format(q) - q.group do - q.text("yield") - - if arguments.is_a?(Paren) - q.format(arguments) - else - q.if_break { q.text("(") }.if_flat { q.text(" ") } - q.indent do - q.breakable("") - q.format(arguments) - end - q.breakable("") - q.if_break { q.text(")") } - end - end - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("yield") - - q.breakable - q.pp(arguments) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { type: :yield, args: arguments, loc: location, cmts: comments }.to_json( - *opts - ) - end - end - # :call-seq: # on_yield: ((Args | Paren) arguments) -> Yield def on_yield(arguments) @@ -15032,58 +3243,6 @@ def on_yield(arguments) ) end - # Yield0 represents the bare +yield+ keyword with no arguments. - # - # yield - # - class Yield0 - # [String] the value of the keyword - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(value:, location:, comments: []) - @value = value - @location = location - @comments = comments - end - - def child_nodes - [] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { value: value, location: location, comments: comments } - end - - def format(q) - q.text(value) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("yield0") - - q.breakable - q.pp(value) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { type: :yield0, value: value, loc: location, cmts: comments }.to_json( - *opts - ) - end - end - # :call-seq: # on_yield0: () -> Yield0 def on_yield0 @@ -15092,58 +3251,6 @@ def on_yield0 Yield0.new(value: keyword.value, location: keyword.location) end - # ZSuper represents the bare +super+ keyword with no arguments. - # - # super - # - class ZSuper - # [String] the value of the keyword - attr_reader :value - - # [Location] the location of this node - attr_reader :location - - # [Array[ Comment | EmbDoc ]] the comments attached to this node - attr_reader :comments - - def initialize(value:, location:, comments: []) - @value = value - @location = location - @comments = comments - end - - def child_nodes - [] - end - - alias deconstruct child_nodes - - def deconstruct_keys(keys) - { value: value, location: location, comments: comments } - end - - def format(q) - q.text(value) - end - - def pretty_print(q) - q.group(2, "(", ")") do - q.text("zsuper") - - q.breakable - q.pp(value) - - q.pp(Comment::List.new(comments)) - end - end - - def to_json(*opts) - { type: :zsuper, value: value, loc: location, cmts: comments }.to_json( - *opts - ) - end - end - # :call-seq: # on_zsuper: () -> ZSuper def on_zsuper diff --git a/lib/syntax_tree/node.rb b/lib/syntax_tree/node.rb new file mode 100644 index 00000000..538c085e --- /dev/null +++ b/lib/syntax_tree/node.rb @@ -0,0 +1,11931 @@ +# frozen_string_literal: true + +class SyntaxTree < Ripper + # This is the parent node of all of the syntax tree nodes. It's pretty much + # exclusively here to make it easier to operate with the tree in cases where + # you're trying to monkey-patch or strictly type. + class Node + def child_nodes + raise NotImplementedError + end + + def deconstruct + raise NotImplementedError + end + + def deconstruct_keys(keys) + raise NotImplementedError + end + + def format(q) + raise NotImplementedError + end + + def pretty_print(q) + raise NotImplementedError + end + + def to_json(*opts) + raise NotImplementedError + end + end + + # BEGINBlock represents the use of the +BEGIN+ keyword, which hooks into the + # lifecycle of the interpreter. Whatever is inside the block will get executed + # when the program starts. + # + # BEGIN { + # } + # + # Interestingly, the BEGIN keyword doesn't allow the do and end keywords for + # the block. Only braces are permitted. + class BEGINBlock < Node + # [LBrace] the left brace that is seen after the keyword + attr_reader :lbrace + + # [Statements] the expressions to be executed + attr_reader :statements + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(lbrace:, statements:, location:, comments: []) + @lbrace = lbrace + @statements = statements + @location = location + @comments = comments + end + + def child_nodes + [lbrace, statements] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + lbrace: lbrace, + statements: statements, + location: location, + comments: comments + } + end + + def format(q) + q.group do + q.text("BEGIN ") + q.format(lbrace) + q.indent do + q.breakable + q.format(statements) + end + q.breakable + q.text("}") + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("BEGIN") + + q.breakable + q.pp(statements) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :BEGIN, + lbrace: lbrace, + stmts: statements, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # CHAR irepresents a single codepoint in the script encoding. + # + # ?a + # + # In the example above, the CHAR node represents the string literal "a". You + # can use control characters with this as well, as in ?\C-a. + class CHAR < Node + # [String] the value of the character literal + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) + @value = value + @location = location + @comments = comments + end + + def child_nodes + [] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + + def format(q) + if value.length != 2 + q.text(value) + else + q.text(q.quote) + q.text(value[1] == "\"" ? "\\\"" : value[1]) + q.text(q.quote) + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("CHAR") + + q.breakable + q.pp(value) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :CHAR, value: value, loc: location, cmts: comments }.to_json( + *opts + ) + end + end + + # ENDBlock represents the use of the +END+ keyword, which hooks into the + # lifecycle of the interpreter. Whatever is inside the block will get executed + # when the program ends. + # + # END { + # } + # + # Interestingly, the END keyword doesn't allow the do and end keywords for the + # block. Only braces are permitted. + class ENDBlock < Node + # [LBrace] the left brace that is seen after the keyword + attr_reader :lbrace + + # [Statements] the expressions to be executed + attr_reader :statements + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(lbrace:, statements:, location:, comments: []) + @lbrace = lbrace + @statements = statements + @location = location + @comments = comments + end + + def child_nodes + [lbrace, statements] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + lbrace: lbrace, + statements: statements, + location: location, + comments: comments + } + end + + def format(q) + q.group do + q.text("END ") + q.format(lbrace) + q.indent do + q.breakable + q.format(statements) + end + q.breakable + q.text("}") + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("END") + + q.breakable + q.pp(statements) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :END, + lbrace: lbrace, + stmts: statements, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # EndContent represents the use of __END__ syntax, which allows individual + # scripts to keep content after the main ruby code that can be read through + # the DATA constant. + # + # puts DATA.read + # + # __END__ + # some other content that is not executed by the program + # + class EndContent < Node + # [String] the content after the script + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) + @value = value + @location = location + @comments = comments + end + + def child_nodes + [] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + + def format(q) + q.text("__END__") + q.breakable(force: true) + + separator = -> { q.breakable(indent: false, force: true) } + q.seplist(value.split(/\r?\n/, -1), separator) { |line| q.text(line) } + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("__end__") + + q.breakable + q.pp(value) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :__end__, value: value, loc: location, cmts: comments }.to_json( + *opts + ) + end + end + + # Alias represents the use of the +alias+ keyword with regular arguments (not + # global variables). The +alias+ keyword is used to make a method respond to + # another name as well as the current one. + # + # alias aliased_name name + # + # For the example above, in the current context you can now call aliased_name + # and it will execute the name method. When you're aliasing two methods, you + # can either provide bare words (like the example above) or you can provide + # symbols (note that this includes dynamic symbols like + # :"left-#{middle}-right"). + class Alias < Node + class AliasArgumentFormatter + # [DynaSymbol | SymbolLiteral] the argument being passed to alias + attr_reader :argument + + def initialize(argument) + @argument = argument + end + + def comments + if argument.is_a?(SymbolLiteral) + argument.comments + argument.value.comments + else + argument.comments + end + end + + def format(q) + if argument.is_a?(SymbolLiteral) + q.format(argument.value) + else + q.format(argument) + end + end + end + + # [DynaSymbol | SymbolLiteral] the new name of the method + attr_reader :left + + # [DynaSymbol | SymbolLiteral] the old name of the method + attr_reader :right + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(left:, right:, location:, comments: []) + @left = left + @right = right + @location = location + @comments = comments + end + + def child_nodes + [left, right] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { left: left, right: right, location: location, comments: comments } + end + + def format(q) + keyword = "alias " + left_argument = AliasArgumentFormatter.new(left) + + q.group do + q.text(keyword) + q.format(left_argument, stackable: false) + q.group do + q.nest(keyword.length) do + q.breakable(force: left_argument.comments.any?) + q.format(AliasArgumentFormatter.new(right), stackable: false) + end + end + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("alias") + + q.breakable + q.pp(left) + + q.breakable + q.pp(right) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :alias, + left: left, + right: right, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # ARef represents when you're pulling a value out of a collection at a + # specific index. Put another way, it's any time you're calling the method + # #[]. + # + # collection[index] + # + # The nodes usually contains two children, the collection and the index. In + # some cases, you don't necessarily have the second child node, because you + # can call procs with a pretty esoteric syntax. In the following example, you + # wouldn't have a second child node: + # + # collection[] + # + class ARef < Node + # [untyped] the value being indexed + attr_reader :collection + + # [nil | Args] the value being passed within the brackets + attr_reader :index + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(collection:, index:, location:, comments: []) + @collection = collection + @index = index + @location = location + @comments = comments + end + + def child_nodes + [collection, index] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + collection: collection, + index: index, + location: location, + comments: comments + } + end + + def format(q) + q.group do + q.format(collection) + q.text("[") + + if index + q.indent do + q.breakable("") + q.format(index) + end + q.breakable("") + end + + q.text("]") + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("aref") + + q.breakable + q.pp(collection) + + q.breakable + q.pp(index) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :aref, + collection: collection, + index: index, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # ARefField represents assigning values into collections at specific indices. + # Put another way, it's any time you're calling the method #[]=. The + # ARefField node itself is just the left side of the assignment, and they're + # always wrapped in assign nodes. + # + # collection[index] = value + # + class ARefField < Node + # [untyped] the value being indexed + attr_reader :collection + + # [nil | Args] the value being passed within the brackets + attr_reader :index + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(collection:, index:, location:, comments: []) + @collection = collection + @index = index + @location = location + @comments = comments + end + + def child_nodes + [collection, index] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + collection: collection, + index: index, + location: location, + comments: comments + } + end + + def format(q) + q.group do + q.format(collection) + q.text("[") + + if index + q.indent do + q.breakable("") + q.format(index) + end + q.breakable("") + end + + q.text("]") + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("aref_field") + + q.breakable + q.pp(collection) + + q.breakable + q.pp(index) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :aref_field, + collection: collection, + index: index, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # ArgParen represents wrapping arguments to a method inside a set of + # parentheses. + # + # method(argument) + # + # In the example above, there would be an ArgParen node around the Args node + # that represents the set of arguments being sent to the method method. The + # argument child node can be +nil+ if no arguments were passed, as in: + # + # method() + # + class ArgParen < Node + # [nil | Args | ArgsForward] the arguments inside the + # parentheses + attr_reader :arguments + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(arguments:, location:, comments: []) + @arguments = arguments + @location = location + @comments = comments + end + + def child_nodes + [arguments] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { arguments: arguments, location: location, comments: comments } + end + + def format(q) + unless arguments + q.text("()") + return + end + + q.group(0, "(", ")") do + q.indent do + q.breakable("") + q.format(arguments) + end + q.breakable("") + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("arg_paren") + + q.breakable + q.pp(arguments) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :arg_paren, + args: arguments, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # Args represents a list of arguments being passed to a method call or array + # literal. + # + # method(first, second, third) + # + class Args < Node + # [Array[ untyped ]] the arguments that this node wraps + attr_reader :parts + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(parts:, location:, comments: []) + @parts = parts + @location = location + @comments = comments + end + + def child_nodes + parts + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { parts: parts, location: location, comments: comments } + end + + def format(q) + q.seplist(parts) { |part| q.format(part) } + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("args") + + q.breakable + q.group(2, "(", ")") { q.seplist(parts) { |part| q.pp(part) } } + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :args, parts: parts, loc: location, cmts: comments }.to_json( + *opts + ) + end + end + + # ArgBlock represents using a block operator on an expression. + # + # method(&expression) + # + class ArgBlock < Node + # [nil | untyped] the expression being turned into a block + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) + @value = value + @location = location + @comments = comments + end + + def child_nodes + [value] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + + def format(q) + q.text("&") + q.format(value) if value + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("arg_block") + + if value + q.breakable + q.pp(value) + end + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :arg_block, value: value, loc: location, cmts: comments }.to_json( + *opts + ) + end + end + + # Star represents using a splat operator on an expression. + # + # method(*arguments) + # + class ArgStar < Node + # [nil | untyped] the expression being splatted + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) + @value = value + @location = location + @comments = comments + end + + def child_nodes + [value] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + + def format(q) + q.text("*") + q.format(value) if value + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("arg_star") + + q.breakable + q.pp(value) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :arg_star, value: value, loc: location, cmts: comments }.to_json( + *opts + ) + end + end + + # ArgsForward represents forwarding all kinds of arguments onto another method + # call. + # + # def request(method, path, **headers, &block); end + # + # def get(...) + # request(:GET, ...) + # end + # + # def post(...) + # request(:POST, ...) + # end + # + # In the example above, both the get and post methods are forwarding all of + # their arguments (positional, keyword, and block) on to the request method. + # The ArgsForward node appears in both the caller (the request method calls) + # and the callee (the get and post definitions). + class ArgsForward < Node + # [String] the value of the operator + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) + @value = value + @location = location + @comments = comments + end + + def child_nodes + [] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + + def format(q) + q.text(value) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("args_forward") + + q.breakable + q.pp(value) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :args_forward, + value: value, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # ArrayLiteral represents an array literal, which can optionally contain + # elements. + # + # [] + # [one, two, three] + # + class ArrayLiteral < Node + class QWordsFormatter + # [Args] the contents of the array + attr_reader :contents + + def initialize(contents) + @contents = contents + end + + def format(q) + q.group(0, "%w[", "]") do + q.indent do + q.breakable("") + q.seplist(contents.parts, -> { q.breakable }) do |part| + if part.is_a?(StringLiteral) + q.format(part.parts.first) + else + q.text(part.value[1..-1]) + end + end + end + q.breakable("") + end + end + end + + class QSymbolsFormatter + # [Args] the contents of the array + attr_reader :contents + + def initialize(contents) + @contents = contents + end + + def format(q) + q.group(0, "%i[", "]") do + q.indent do + q.breakable("") + q.seplist(contents.parts, -> { q.breakable }) do |part| + q.format(part.value) + end + end + q.breakable("") + end + end + end + + class VarRefsFormatter + # [Args] the contents of the array + attr_reader :contents + + def initialize(contents) + @contents = contents + end + + def format(q) + q.group(0, "[", "]") do + q.indent do + q.breakable("") + + separator = -> do + q.text(",") + q.fill_breakable + end + + q.seplist(contents.parts, separator) { |part| q.format(part) } + end + q.breakable("") + end + end + end + + # [LBracket] the bracket that opens this array + attr_reader :lbracket + + # [nil | Args] the contents of the array + attr_reader :contents + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(lbracket:, contents:, location:, comments: []) + @lbracket = lbracket + @contents = contents + @location = location + @comments = comments + end + + def child_nodes + [lbracket, contents] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + lbracket: lbracket, + contents: contents, + location: location, + comments: comments + } + end + + def format(q) + if qwords? + QWordsFormatter.new(contents).format(q) + return + end + + if qsymbols? + QSymbolsFormatter.new(contents).format(q) + return + end + + if var_refs?(q) + VarRefsFormatter.new(contents).format(q) + return + end + + q.group do + q.format(lbracket) + + if contents + q.indent do + q.breakable("") + q.format(contents) + end + end + + q.breakable("") + q.text("]") + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("array") + + q.breakable + q.pp(contents) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :array, cnts: contents, loc: location, cmts: comments }.to_json( + *opts + ) + end + + private + + def qwords? + lbracket.comments.empty? && contents && contents.comments.empty? && + contents.parts.length > 1 && + contents.parts.all? do |part| + case part + when StringLiteral + part.comments.empty? && part.parts.length == 1 && + part.parts.first.is_a?(TStringContent) && + !part.parts.first.value.match?(/[\s\[\]\\]/) + when CHAR + !part.value.match?(/[\[\]\\]/) + else + false + end + end + end + + def qsymbols? + lbracket.comments.empty? && contents && contents.comments.empty? && + contents.parts.length > 1 && + contents.parts.all? do |part| + part.is_a?(SymbolLiteral) && part.comments.empty? + end + end + + def var_refs?(q) + lbracket.comments.empty? && contents && contents.comments.empty? && + contents.parts.all? do |part| + part.is_a?(VarRef) && part.comments.empty? + end && + ( + contents.parts.sum { |part| part.value.value.length + 2 } > + q.maxwidth * 2 + ) + end + end + + # AryPtn represents matching against an array pattern using the Ruby 2.7+ + # pattern matching syntax. It’s one of the more complicated nodes, because + # the four parameters that it accepts can almost all be nil. + # + # case [1, 2, 3] + # in [Integer, Integer] + # "matched" + # in Container[Integer, Integer] + # "matched" + # in [Integer, *, Integer] + # "matched" + # end + # + # An AryPtn node is created with four parameters: an optional constant + # wrapper, an array of positional matches, an optional splat with identifier, + # and an optional array of positional matches that occur after the splat. + # All of the in clauses above would create an AryPtn node. + class AryPtn < Node + class RestFormatter + # [VarField] the identifier that represents the remaining positionals + attr_reader :value + + def initialize(value) + @value = value + end + + def comments + value.comments + end + + def format(q) + q.text("*") + q.format(value) + end + end + + # [nil | VarRef] the optional constant wrapper + attr_reader :constant + + # [Array[ untyped ]] the regular positional arguments that this array + # pattern is matching against + attr_reader :requireds + + # [nil | VarField] the optional starred identifier that grabs up a list of + # positional arguments + attr_reader :rest + + # [Array[ untyped ]] the list of positional arguments occurring after the + # optional star if there is one + attr_reader :posts + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize( + constant:, + requireds:, + rest:, + posts:, + location:, + comments: [] + ) + @constant = constant + @requireds = requireds + @rest = rest + @posts = posts + @location = location + @comments = comments + end + + def child_nodes + [constant, *requireds, rest, *posts] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + constant: constant, + requireds: requireds, + rest: rest, + posts: posts, + location: location, + comments: comments + } + end + + def format(q) + parts = [*requireds] + parts << RestFormatter.new(rest) if rest + parts += posts + + if constant + q.group do + q.format(constant) + q.text("[") + q.seplist(parts) { |part| q.format(part) } + q.text("]") + end + + return + end + + parent = q.parent + if parts.length == 1 || PATTERNS.include?(parent.class) + q.text("[") + q.seplist(parts) { |part| q.format(part) } + q.text("]") + else + q.group { q.seplist(parts) { |part| q.format(part) } } + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("aryptn") + + if constant + q.breakable + q.pp(constant) + end + + if requireds.any? + q.breakable + q.group(2, "(", ")") do + q.seplist(requireds) { |required| q.pp(required) } + end + end + + if rest + q.breakable + q.pp(rest) + end + + if posts.any? + q.breakable + q.group(2, "(", ")") { q.seplist(posts) { |post| q.pp(post) } } + end + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :aryptn, + constant: constant, + reqs: requireds, + rest: rest, + posts: posts, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # Determins if the following value should be indented or not. + module AssignFormatting + def self.skip_indent?(value) + (value.is_a?(Call) && skip_indent?(value.receiver)) || + [ + ArrayLiteral, + HashLiteral, + Heredoc, + Lambda, + QSymbols, + QWords, + Symbols, + Words + ].include?(value.class) + end + end + + # Assign represents assigning something to a variable or constant. Generally, + # the left side of the assignment is going to be any node that ends with the + # name "Field". + # + # variable = value + # + class Assign < Node + # [ARefField | ConstPathField | Field | TopConstField | VarField] the target + # to assign the result of the expression to + attr_reader :target + + # [untyped] the expression to be assigned + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(target:, value:, location:, comments: []) + @target = target + @value = value + @location = location + @comments = comments + end + + def child_nodes + [target, value] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { target: target, value: value, location: location, comments: comments } + end + + def format(q) + q.group do + q.format(target) + q.text(" =") + + if skip_indent? + q.text(" ") + q.format(value) + else + q.indent do + q.breakable + q.format(value) + end + end + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("assign") + + q.breakable + q.pp(target) + + q.breakable + q.pp(value) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :assign, + target: target, + value: value, + loc: location, + cmts: comments + }.to_json(*opts) + end + + private + + def skip_indent? + target.comments.empty? && + (target.is_a?(ARefField) || AssignFormatting.skip_indent?(value)) + end + end + + # Assoc represents a key-value pair within a hash. It is a child node of + # either an AssocListFromArgs or a BareAssocHash. + # + # { key1: value1, key2: value2 } + # + # In the above example, the would be two AssocNew nodes. + class Assoc < Node + # [untyped] the key of this pair + attr_reader :key + + # [untyped] the value of this pair + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(key:, value:, location:, comments: []) + @key = key + @value = value + @location = location + @comments = comments + end + + def child_nodes + [key, value] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { key: key, value: value, location: location, comments: comments } + end + + def format(q) + if value&.is_a?(HashLiteral) + format_contents(q) + else + q.group { format_contents(q) } + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("assoc") + + q.breakable + q.pp(key) + + if value + q.breakable + q.pp(value) + end + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :assoc, + key: key, + value: value, + loc: location, + cmts: comments + }.to_json(*opts) + end + + private + + def format_contents(q) + q.parent.format_key(q, key) + return unless value + + if key.comments.empty? && AssignFormatting.skip_indent?(value) + q.text(" ") + q.format(value) + else + q.indent do + q.breakable + q.format(value) + end + end + end + end + + # AssocSplat represents double-splatting a value into a hash (either a hash + # literal or a bare hash in a method call). + # + # { **pairs } + # + class AssocSplat < Node + # [untyped] the expression that is being splatted + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) + @value = value + @location = location + @comments = comments + end + + def child_nodes + [value] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + + def format(q) + q.text("**") + q.format(value) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("assoc_splat") + + q.breakable + q.pp(value) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :assoc_splat, + value: value, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # Backref represents a global variable referencing a matched value. It comes + # in the form of a $ followed by a positive integer. + # + # $1 + # + class Backref < Node + # [String] the name of the global backreference variable + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) + @value = value + @location = location + @comments = comments + end + + def child_nodes + [] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + + def format(q) + q.text(value) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("backref") + + q.breakable + q.pp(value) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :backref, value: value, loc: location, cmts: comments }.to_json( + *opts + ) + end + end + + # Backtick represents the use of the ` operator. It's usually found being used + # for an XStringLiteral, but could also be found as the name of a method being + # defined. + class Backtick < Node + # [String] the backtick in the string + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) + @value = value + @location = location + @comments = comments + end + + def child_nodes + [] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + + def format(q) + q.text(value) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("backtick") + + q.breakable + q.pp(value) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :backtick, value: value, loc: location, cmts: comments }.to_json( + *opts + ) + end + end + + # This module is responsible for formatting the assocs contained within a + # hash or bare hash. It first determines if every key in the hash can use + # labels. If it can, it uses labels. Otherwise it uses hash rockets. + module HashKeyFormatter + class Labels + def format_key(q, key) + case key + when Label + q.format(key) + when SymbolLiteral + q.format(key.value) + q.text(":") + when DynaSymbol + q.format(key) + q.text(":") + end + end + end + + class Rockets + def format_key(q, key) + case key + when Label + q.text(":") + q.text(key.value.chomp(":")) + when DynaSymbol + q.text(":") + q.format(key) + else + q.format(key) + end + + q.text(" =>") + end + end + + def self.for(container) + labels = + container.assocs.all? do |assoc| + next true if assoc.is_a?(AssocSplat) + + case assoc.key + when Label + true + when SymbolLiteral + # When attempting to convert a hash rocket into a hash label, + # you need to take care because only certain patterns are + # allowed. Ruby source says that they have to match keyword + # arguments to methods, but don't specify what that is. After + # some experimentation, it looks like it's: + value = assoc.key.value.value + value.match?(/^[_A-Za-z]/) && !value.end_with?("=") + when DynaSymbol + true + else + false + end + end + + (labels ? Labels : Rockets).new + end + end + + # BareAssocHash represents a hash of contents being passed as a method + # argument (and therefore has omitted braces). It's very similar to an + # AssocListFromArgs node. + # + # method(key1: value1, key2: value2) + # + class BareAssocHash < Node + # [Array[ AssocNew | AssocSplat ]] + attr_reader :assocs + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(assocs:, location:, comments: []) + @assocs = assocs + @location = location + @comments = comments + end + + def child_nodes + assocs + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { assocs: assocs, location: location, comments: comments } + end + + def format(q) + q.seplist(assocs) { |assoc| q.format(assoc) } + end + + def format_key(q, key) + (@key_formatter ||= HashKeyFormatter.for(self)).format_key(q, key) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("bare_assoc_hash") + + q.breakable + q.group(2, "(", ")") { q.seplist(assocs) { |assoc| q.pp(assoc) } } + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :bare_assoc_hash, + assocs: assocs, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # Begin represents a begin..end chain. + # + # begin + # value + # end + # + class Begin < Node + # [BodyStmt] the bodystmt that contains the contents of this begin block + attr_reader :bodystmt + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(bodystmt:, location:, comments: []) + @bodystmt = bodystmt + @location = location + @comments = comments + end + + def child_nodes + [bodystmt] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { bodystmt: bodystmt, location: location, comments: comments } + end + + def format(q) + q.text("begin") + + unless bodystmt.empty? + q.indent do + q.breakable(force: true) unless bodystmt.statements.empty? + q.format(bodystmt) + end + end + + q.breakable(force: true) + q.text("end") + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("begin") + + q.breakable + q.pp(bodystmt) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :begin, + bodystmt: bodystmt, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # PinnedBegin represents a pinning a nested statement within pattern matching. + # + # case value + # in ^(statement) + # end + # + class PinnedBegin < Node + # [untyped] the expression being pinned + attr_reader :statement + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(statement:, location:, comments: []) + @statement = statement + @location = location + @comments = comments + end + + def child_nodes + [statement] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { statement: statement, location: location, comments: comments } + end + + def format(q) + q.group do + q.text("^(") + q.nest(1) do + q.indent do + q.breakable("") + q.format(statement) + end + q.breakable("") + q.text(")") + end + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("pinned_begin") + + q.breakable + q.pp(statement) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :pinned_begin, + stmt: statement, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + + # Binary represents any expression that involves two sub-expressions with an + # operator in between. This can be something that looks like a mathematical + # operation: + # + # 1 + 1 + # + # but can also be something like pushing a value onto an array: + # + # array << value + # + class Binary < Node + # [untyped] the left-hand side of the expression + attr_reader :left + + # [Symbol] the operator used between the two expressions + attr_reader :operator + + # [untyped] the right-hand side of the expression + attr_reader :right + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(left:, operator:, right:, location:, comments: []) + @left = left + @operator = operator + @right = right + @location = location + @comments = comments + end + + def child_nodes + [left, right] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + left: left, + operator: operator, + right: right, + location: location, + comments: comments + } + end + + def format(q) + power = operator == :** + + q.group do + q.group { q.format(left) } + q.text(" ") unless power + + q.group do + q.text(operator) + + q.indent do + q.breakable(power ? "" : " ") + q.format(right) + end + end + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("binary") + + q.breakable + q.pp(left) + + q.breakable + q.text(operator) + + q.breakable + q.pp(right) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :binary, + left: left, + op: operator, + right: right, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # This module will remove any breakables from the list of contents so that no + # newlines are present in the output. + module RemoveBreaks + class << self + def call(doc) + marker = Object.new + stack = [doc] + + while stack.any? + doc = stack.pop + + if doc == marker + stack.pop + next + end + + stack += [doc, marker] + + case doc + when PrettyPrint::Align, PrettyPrint::Indent, PrettyPrint::Group + doc.contents.map! { |child| remove_breaks(child) } + stack += doc.contents.reverse + when PrettyPrint::IfBreak + doc.flat_contents.map! { |child| remove_breaks(child) } + stack += doc.flat_contents.reverse + end + end + end + + private + + def remove_breaks(doc) + case doc + when PrettyPrint::Breakable + text = PrettyPrint::Text.new + text.add(object: doc.force? ? "; " : doc.separator, width: doc.width) + text + when PrettyPrint::IfBreak + PrettyPrint::Align.new(indent: 0, contents: doc.flat_contents) + else + doc + end + end + end + end + + # BlockVar represents the parameters being declared for a block. Effectively + # this node is everything contained within the pipes. This includes all of the + # various parameter types, as well as block-local variable declarations. + # + # method do |positional, optional = value, keyword:, █ local| + # end + # + class BlockVar < Node + # [Params] the parameters being declared with the block + attr_reader :params + + # [Array[ Ident ]] the list of block-local variable declarations + attr_reader :locals + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(params:, locals:, location:, comments: []) + @params = params + @locals = locals + @location = location + @comments = comments + end + + def child_nodes + [params, *locals] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { params: params, locals: locals, location: location, comments: comments } + end + + def format(q) + q.group(0, "|", "|") do + doc = q.format(params) + RemoveBreaks.call(doc) + + if locals.any? + q.text("; ") + q.seplist(locals, -> { q.text(", ") }) { |local| q.format(local) } + end + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("block_var") + + q.breakable + q.pp(params) + + if locals.any? + q.breakable + q.group(2, "(", ")") { q.seplist(locals) { |local| q.pp(local) } } + end + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :block_var, + params: params, + locals: locals, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # BlockArg represents declaring a block parameter on a method definition. + # + # def method(&block); end + # + class BlockArg < Node + # [nil | Ident] the name of the block argument + attr_reader :name + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(name:, location:, comments: []) + @name = name + @location = location + @comments = comments + end + + def child_nodes + [name] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { name: name, location: location, comments: comments } + end + + def format(q) + q.text("&") + q.format(name) if name + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("blockarg") + + if name + q.breakable + q.pp(name) + end + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :blockarg, name: name, loc: location, cmts: comments }.to_json( + *opts + ) + end + end + + # bodystmt can't actually determine its bounds appropriately because it + # doesn't necessarily know where it started. So the parent node needs to + # report back down into this one where it goes. + class BodyStmt < Node + # [Statements] the list of statements inside the begin clause + attr_reader :statements + + # [nil | Rescue] the optional rescue chain attached to the begin clause + attr_reader :rescue_clause + + # [nil | Statements] the optional set of statements inside the else clause + attr_reader :else_clause + + # [nil | Ensure] the optional ensure clause + attr_reader :ensure_clause + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize( + statements:, + rescue_clause:, + else_clause:, + ensure_clause:, + location:, + comments: [] + ) + @statements = statements + @rescue_clause = rescue_clause + @else_clause = else_clause + @ensure_clause = ensure_clause + @location = location + @comments = comments + end + + def bind(start_char, end_char) + @location = + Location.new( + start_line: location.start_line, + start_char: start_char, + end_line: location.end_line, + end_char: end_char + ) + + parts = [rescue_clause, else_clause, ensure_clause] + + # Here we're going to determine the bounds for the statements + consequent = parts.compact.first + statements.bind( + start_char, + consequent ? consequent.location.start_char : end_char + ) + + # Next we're going to determine the rescue clause if there is one + if rescue_clause + consequent = parts.drop(1).compact.first + rescue_clause.bind_end( + consequent ? consequent.location.start_char : end_char + ) + end + end + + def empty? + statements.empty? && !rescue_clause && !else_clause && !ensure_clause + end + + def child_nodes + [statements, rescue_clause, else_clause, ensure_clause] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + statements: statements, + rescue_clause: rescue_clause, + else_clause: else_clause, + ensure_clause: ensure_clause, + location: location, + comments: comments + } + end + + def format(q) + q.group do + q.format(statements) unless statements.empty? + + if rescue_clause + q.nest(-2) do + q.breakable(force: true) + q.format(rescue_clause) + end + end + + if else_clause + q.nest(-2) do + q.breakable(force: true) + q.text("else") + end + q.breakable(force: true) + q.format(else_clause) + end + + if ensure_clause + q.nest(-2) do + q.breakable(force: true) + q.format(ensure_clause) + end + end + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("bodystmt") + + q.breakable + q.pp(statements) + + if rescue_clause + q.breakable + q.pp(rescue_clause) + end + + if else_clause + q.breakable + q.pp(else_clause) + end + + if ensure_clause + q.breakable + q.pp(ensure_clause) + end + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :bodystmt, + stmts: statements, + rsc: rescue_clause, + els: else_clause, + ens: ensure_clause, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # Responsible for formatting either a BraceBlock or a DoBlock. + class BlockFormatter + class BlockOpenFormatter + # [String] the actual output that should be printed + attr_reader :text + + # [LBrace | Keyword] the node that is being represented + attr_reader :node + + def initialize(text, node) + @text = text + @node = node + end + + def comments + node.comments + end + + def format(q) + q.text(text) + end + end + + # [BraceBlock | DoBlock] the block node to be formatted + attr_reader :node + + # [LBrace | Keyword] the node that opens the block + attr_reader :block_open + + # [String] the string that closes the block + attr_reader :block_close + + # [BodyStmt | Statements] the statements inside the block + attr_reader :statements + + def initialize(node, block_open, block_close, statements) + @node = node + @block_open = block_open + @block_close = block_close + @statements = statements + end + + def format(q) + # If this is nested anywhere inside of a Command or CommandCall node, then + # we can't change which operators we're using for the bounds of the block. + break_opening, break_closing, flat_opening, flat_closing = + if unchangeable_bounds?(q) + [block_open.value, block_close, block_open.value, block_close] + elsif forced_do_end_bounds?(q) + %w[do end do end] + elsif forced_brace_bounds?(q) + %w[{ } { }] + else + %w[do end { }] + end + + # If the receiver of this block a Command or CommandCall node, then there + # are no parentheses around the arguments to that command, so we need to + # break the block. + receiver = q.parent.call + if receiver.is_a?(Command) || receiver.is_a?(CommandCall) + q.break_parent + format_break(q, break_opening, break_closing) + return + end + + q.group do + q.if_break { format_break(q, break_opening, break_closing) }.if_flat do + format_flat(q, flat_opening, flat_closing) + end + end + end + + private + + # If this is nested anywhere inside certain nodes, then we can't change + # which operators/keywords we're using for the bounds of the block. + def unchangeable_bounds?(q) + q.parents.any? do |parent| + # If we hit a statements, then we're safe to use whatever since we + # know for certain we're going to get split over multiple lines + # anyway. + break false if parent.is_a?(Statements) + + [Command, CommandCall].include?(parent.class) + end + end + + # If we're a sibling of a control-flow keyword, then we're going to have to + # use the do..end bounds. + def forced_do_end_bounds?(q) + [Break, Next, Return, Super].include?(q.parent.call.class) + end + + # If we're the predicate of a loop or conditional, then we're going to have + # to go with the {..} bounds. + def forced_brace_bounds?(q) + parents = q.parents.to_a + parents.each_with_index.any? do |parent, index| + # If we hit certain breakpoints then we know we're safe. + break false if [Paren, Statements].include?(parent.class) + + [ + If, + IfMod, + IfOp, + Unless, + UnlessMod, + While, + WhileMod, + Until, + UntilMod + ].include?(parent.class) && parent.predicate == parents[index - 1] + end + end + + def format_break(q, opening, closing) + q.text(" ") + q.format(BlockOpenFormatter.new(opening, block_open), stackable: false) + + if node.block_var + q.text(" ") + q.format(node.block_var) + end + + unless statements.empty? + q.indent do + q.breakable + q.format(statements) + end + end + + q.breakable + q.text(closing) + end + + def format_flat(q, opening, closing) + q.text(" ") + q.format(BlockOpenFormatter.new(opening, block_open), stackable: false) + + if node.block_var + q.breakable + q.format(node.block_var) + q.breakable + end + + if statements.empty? + q.text(" ") if opening == "do" + else + q.breakable unless node.block_var + q.format(statements) + q.breakable + end + + q.text(closing) + end + end + + # BraceBlock represents passing a block to a method call using the { } + # operators. + # + # method { |variable| variable + 1 } + # + class BraceBlock < Node + # [LBrace] the left brace that opens this block + attr_reader :lbrace + + # [nil | BlockVar] the optional set of parameters to the block + attr_reader :block_var + + # [Statements] the list of expressions to evaluate within the block + attr_reader :statements + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(lbrace:, block_var:, statements:, location:, comments: []) + @lbrace = lbrace + @block_var = block_var + @statements = statements + @location = location + @comments = comments + end + + def child_nodes + [lbrace, block_var, statements] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + lbrace: lbrace, + block_var: block_var, + statements: statements, + location: location, + comments: comments + } + end + + def format(q) + BlockFormatter.new(self, lbrace, "}", statements).format(q) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("brace_block") + + if block_var + q.breakable + q.pp(block_var) + end + + q.breakable + q.pp(statements) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :brace_block, + lbrace: lbrace, + block_var: block_var, + stmts: statements, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # Formats either a Break or Next node. + class FlowControlFormatter + # [String] the keyword to print + attr_reader :keyword + + # [Break | Next] the node being formatted + attr_reader :node + + def initialize(keyword, node) + @keyword = keyword + @node = node + end + + def format(q) + arguments = node.arguments + + q.group do + q.text(keyword) + + if arguments.parts.any? + if arguments.parts.length == 1 + part = arguments.parts.first + + if part.is_a?(Paren) + q.format(arguments) + elsif part.is_a?(ArrayLiteral) + q.text(" ") + q.format(arguments) + else + format_arguments(q, "(", ")") + end + else + format_arguments(q, " [", "]") + end + end + end + end + + private + + def format_arguments(q, opening, closing) + q.if_break { q.text(opening) } + q.indent do + q.breakable(" ") + q.format(node.arguments) + end + q.breakable("") + q.if_break { q.text(closing) } + end + end + + # Break represents using the +break+ keyword. + # + # break + # + # It can also optionally accept arguments, as in: + # + # break 1 + # + class Break < Node + # [Args] the arguments being sent to the keyword + attr_reader :arguments + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(arguments:, location:, comments: []) + @arguments = arguments + @location = location + @comments = comments + end + + def child_nodes + [arguments] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { arguments: arguments, location: location, comments: comments } + end + + def format(q) + FlowControlFormatter.new("break", self).format(q) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("break") + + q.breakable + q.pp(arguments) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :break, args: arguments, loc: location, cmts: comments }.to_json( + *opts + ) + end + end + + # Wraps a call operator (which can be a string literal :: or an Op node or a + # Period node) and formats it when called. + class CallOperatorFormatter + # [:"::" | Op | Period] the operator being formatted + attr_reader :operator + + def initialize(operator) + @operator = operator + end + + def comments + operator == :"::" ? [] : operator.comments + end + + def format(q) + if operator == :"::" || (operator.is_a?(Op) && operator.value == "::") + q.text(".") + else + operator.format(q) + end + end + end + + # Call represents a method call. + # + # receiver.message + # + class Call < Node + # [untyped] the receiver of the method call + attr_reader :receiver + + # [:"::" | Op | Period] the operator being used to send the message + attr_reader :operator + + # [:call | Backtick | Const | Ident | Op] the message being sent + attr_reader :message + + # [nil | ArgParen | Args] the arguments to the method call + attr_reader :arguments + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize( + receiver:, + operator:, + message:, + arguments:, + location:, + comments: [] + ) + @receiver = receiver + @operator = operator + @message = message + @arguments = arguments + @location = location + @comments = comments + end + + def child_nodes + [ + receiver, + (operator if operator != :"::"), + (message if message != :call), + arguments + ] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + receiver: receiver, + operator: operator, + message: message, + arguments: arguments, + location: location, + comments: comments + } + end + + def format(q) + call_operator = CallOperatorFormatter.new(operator) + + q.group do + q.format(receiver) + + # If there are trailing comments on the call operator, then we need to + # use the trailing form as opposed to the leading form. + q.format(call_operator) if call_operator.comments.any? + + q.group do + q.indent do + if receiver.comments.any? || call_operator.comments.any? + q.breakable(force: true) + end + + if call_operator.comments.empty? + q.format(call_operator, stackable: false) + end + + q.format(message) if message != :call + end + + q.format(arguments) if arguments + end + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("call") + + q.breakable + q.pp(receiver) + + q.breakable + q.pp(operator) + + q.breakable + q.pp(message) + + if arguments + q.breakable + q.pp(arguments) + end + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :call, + receiver: receiver, + op: operator, + message: message, + args: arguments, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # Case represents the beginning of a case chain. + # + # case value + # when 1 + # "one" + # when 2 + # "two" + # else + # "number" + # end + # + class Case < Node + # [Kw] the keyword that opens this expression + attr_reader :keyword + + # [nil | untyped] optional value being switched on + attr_reader :value + + # [In | When] the next clause in the chain + attr_reader :consequent + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(keyword:, value:, consequent:, location:, comments: []) + @keyword = keyword + @value = value + @consequent = consequent + @location = location + @comments = comments + end + + def child_nodes + [keyword, value, consequent] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + keyword: keyword, + value: value, + consequent: consequent, + location: location, + comments: comments + } + end + + def format(q) + q.group do + q.format(keyword) + + if value + q.text(" ") + q.format(value) + end + + q.breakable(force: true) + q.format(consequent) + q.breakable(force: true) + + q.text("end") + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("case") + + q.breakable + q.pp(keyword) + + if value + q.breakable + q.pp(value) + end + + q.breakable + q.pp(consequent) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :case, + value: value, + cons: consequent, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # RAssign represents a single-line pattern match. + # + # value in pattern + # value => pattern + # + class RAssign < Node + # [untyped] the left-hand expression + attr_reader :value + + # [Kw | Op] the operator being used to match against the pattern, which is + # either => or in + attr_reader :operator + + # [untyped] the pattern on the right-hand side of the expression + attr_reader :pattern + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, operator:, pattern:, location:, comments: []) + @value = value + @operator = operator + @pattern = pattern + @location = location + @comments = comments + end + + def child_nodes + [value, operator, pattern] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + value: value, + operator: operator, + pattern: pattern, + location: location, + comments: comments + } + end + + def format(q) + q.group do + q.format(value) + q.text(" ") + q.format(operator) + q.group do + q.indent do + q.breakable + q.format(pattern) + end + end + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("rassign") + + q.breakable + q.pp(value) + + q.breakable + q.pp(operator) + + q.breakable + q.pp(pattern) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :rassign, + value: value, + op: operator, + pattern: pattern, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # Class represents defining a class using the +class+ keyword. + # + # class Container + # end + # + # Classes can have path names as their class name in case it's being nested + # under a namespace, as in: + # + # class Namespace::Container + # end + # + # Classes can also be defined as a top-level path, in the case that it's + # already in a namespace but you want to define it at the top-level instead, + # as in: + # + # module OtherNamespace + # class ::Namespace::Container + # end + # end + # + # All of these declarations can also have an optional superclass reference, as + # in: + # + # class Child < Parent + # end + # + # That superclass can actually be any Ruby expression, it doesn't necessarily + # need to be a constant, as in: + # + # class Child < method + # end + # + class ClassDeclaration < Node + # [ConstPathRef | ConstRef | TopConstRef] the name of the class being + # defined + attr_reader :constant + + # [nil | untyped] the optional superclass declaration + attr_reader :superclass + + # [BodyStmt] the expressions to execute within the context of the class + attr_reader :bodystmt + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(constant:, superclass:, bodystmt:, location:, comments: []) + @constant = constant + @superclass = superclass + @bodystmt = bodystmt + @location = location + @comments = comments + end + + def child_nodes + [constant, superclass, bodystmt] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + constant: constant, + superclass: superclass, + bodystmt: bodystmt, + location: location, + comments: comments + } + end + + def format(q) + declaration = -> do + q.group do + q.text("class ") + q.format(constant) + + if superclass + q.text(" < ") + q.format(superclass) + end + end + end + + if bodystmt.empty? + q.group do + declaration.call + q.breakable(force: true) + q.text("end") + end + else + q.group do + declaration.call + + q.indent do + q.breakable(force: true) + q.format(bodystmt) + end + + q.breakable(force: true) + q.text("end") + end + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("class") + + q.breakable + q.pp(constant) + + if superclass + q.breakable + q.pp(superclass) + end + + q.breakable + q.pp(bodystmt) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :class, + constant: constant, + superclass: superclass, + bodystmt: bodystmt, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # Comma represents the use of the , operator. + class Comma < Node + # [String] the comma in the string + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + def initialize(value:, location:) + @value = value + @location = location + end + end + + # Command represents a method call with arguments and no parentheses. Note + # that Command nodes only happen when there is no explicit receiver for this + # method. + # + # method argument + # + class Command < Node + # [Const | Ident] the message being sent to the implicit receiver + attr_reader :message + + # [Args] the arguments being sent with the message + attr_reader :arguments + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(message:, arguments:, location:, comments: []) + @message = message + @arguments = arguments + @location = location + @comments = comments + end + + def child_nodes + [message, arguments] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + message: message, + arguments: arguments, + location: location, + comments: comments + } + end + + def format(q) + q.group do + q.format(message) + q.text(" ") + q.nest(message.value.length + 1) { q.format(arguments) } + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("command") + + q.breakable + q.pp(message) + + q.breakable + q.pp(arguments) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :command, + message: message, + args: arguments, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # CommandCall represents a method call on an object with arguments and no + # parentheses. + # + # object.method argument + # + class CommandCall < Node + # [untyped] the receiver of the message + attr_reader :receiver + + # [:"::" | Op | Period] the operator used to send the message + attr_reader :operator + + # [Const | Ident | Op] the message being send + attr_reader :message + + # [nil | Args] the arguments going along with the message + attr_reader :arguments + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize( + receiver:, + operator:, + message:, + arguments:, + location:, + comments: [] + ) + @receiver = receiver + @operator = operator + @message = message + @arguments = arguments + @location = location + @comments = comments + end + + def child_nodes + [receiver, message, arguments] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + receiver: receiver, + operator: operator, + message: message, + arguments: arguments, + location: location, + comments: comments + } + end + + def format(q) + q.group do + doc = + q.nest(0) do + q.format(receiver) + q.format(CallOperatorFormatter.new(operator), stackable: false) + q.format(message) + end + + if arguments + q.text(" ") + q.nest(argument_alignment(q, doc)) { q.format(arguments) } + end + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("command_call") + + q.breakable + q.pp(receiver) + + q.breakable + q.pp(operator) + + q.breakable + q.pp(message) + + if arguments + q.breakable + q.pp(arguments) + end + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :command_call, + receiver: receiver, + op: operator, + message: message, + args: arguments, + loc: location, + cmts: comments + }.to_json(*opts) + end + + private + + # This is a somewhat naive method that is attempting to sum up the width of + # the doc nodes that make up the given doc node. This is used to align + # content. + def doc_width(parent) + queue = [parent] + width = 0 + + until queue.empty? + doc = queue.shift + + case doc + when PrettyPrint::Text + width += doc.width + when PrettyPrint::Indent, PrettyPrint::Align, PrettyPrint::Group + queue = doc.contents + queue + when PrettyPrint::IfBreak + queue = doc.break_contents + queue + when PrettyPrint::Breakable + width = 0 + end + end + + width + end + + def argument_alignment(q, doc) + # Very special handling case for rspec matchers. In general with rspec + # matchers you expect to see something like: + # + # expect(foo).to receive(:bar).with( + # 'one', + # 'two', + # 'three', + # 'four', + # 'five' + # ) + # + # In this case the arguments are aligned to the left side as opposed to + # being aligned with the `receive` call. + if %w[to not_to to_not].include?(message.value) + 0 + else + width = doc_width(doc) + 1 + width > (q.maxwidth / 2) ? 0 : width + end + end + end + + # Comment represents a comment in the source. + # + # # comment + # + class Comment < Node + class List + # [Array[ Comment ]] the list of comments this list represents + attr_reader :comments + + def initialize(comments) + @comments = comments + end + + def pretty_print(q) + return if comments.empty? + + q.breakable + q.group(2, "(", ")") { q.seplist(comments) { |comment| q.pp(comment) } } + end + end + + # [String] the contents of the comment + attr_reader :value + + # [boolean] whether or not there is code on the same line as this comment. + # If there is, then inline will be true. + attr_reader :inline + alias inline? inline + + # [Location] the location of this node + attr_reader :location + + def initialize(value:, inline:, location:) + @value = value + @inline = inline + @location = location + + @leading = false + @trailing = false + end + + def leading! + @leading = true + end + + def leading? + @leading + end + + def trailing! + @trailing = true + end + + def trailing? + @trailing + end + + def ignore? + value[1..-1].strip == "stree-ignore" + end + + def comments + [] + end + + def child_nodes + [] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, inline: inline, location: location } + end + + def format(q) + q.text(value) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("comment") + + q.breakable + q.pp(value) + end + end + + def to_json(*opts) + { + type: :comment, + value: value.force_encoding("UTF-8"), + inline: inline, + loc: location + }.to_json(*opts) + end + end + + # Const represents a literal value that _looks_ like a constant. This could + # actually be a reference to a constant: + # + # Constant + # + # It could also be something that looks like a constant in another context, as + # in a method call to a capitalized method: + # + # object.Constant + # + # or a symbol that starts with a capital letter: + # + # :Constant + # + class Const < Node + # [String] the name of the constant + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) + @value = value + @location = location + @comments = comments + end + + def child_nodes + [] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + + def format(q) + q.text(value) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("const") + + q.breakable + q.pp(value) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :const, value: value, loc: location, cmts: comments }.to_json( + *opts + ) + end + end + + # ConstPathField represents the child node of some kind of assignment. It + # represents when you're assigning to a constant that is being referenced as + # a child of another variable. + # + # object::Const = value + # + class ConstPathField < Node + # [untyped] the source of the constant + attr_reader :parent + + # [Const] the constant itself + attr_reader :constant + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(parent:, constant:, location:, comments: []) + @parent = parent + @constant = constant + @location = location + @comments = comments + end + + def child_nodes + [parent, constant] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + parent: parent, + constant: constant, + location: location, + comments: comments + } + end + + def format(q) + q.format(parent) + q.text("::") + q.format(constant) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("const_path_field") + + q.breakable + q.pp(parent) + + q.breakable + q.pp(constant) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :const_path_field, + parent: parent, + constant: constant, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # ConstPathRef represents referencing a constant by a path. + # + # object::Const + # + class ConstPathRef < Node + # [untyped] the source of the constant + attr_reader :parent + + # [Const] the constant itself + attr_reader :constant + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(parent:, constant:, location:, comments: []) + @parent = parent + @constant = constant + @location = location + @comments = comments + end + + def child_nodes + [parent, constant] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + parent: parent, + constant: constant, + location: location, + comments: comments + } + end + + def format(q) + q.format(parent) + q.text("::") + q.format(constant) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("const_path_ref") + + q.breakable + q.pp(parent) + + q.breakable + q.pp(constant) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :const_path_ref, + parent: parent, + constant: constant, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # ConstRef represents the name of the constant being used in a class or module + # declaration. + # + # class Container + # end + # + class ConstRef < Node + # [Const] the constant itself + attr_reader :constant + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(constant:, location:, comments: []) + @constant = constant + @location = location + @comments = comments + end + + def child_nodes + [constant] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { constant: constant, location: location, comments: comments } + end + + def format(q) + q.format(constant) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("const_ref") + + q.breakable + q.pp(constant) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :const_ref, + constant: constant, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # CVar represents the use of a class variable. + # + # @@variable + # + class CVar < Node + # [String] the name of the class variable + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) + @value = value + @location = location + @comments = comments + end + + def child_nodes + [] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + + def format(q) + q.text(value) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("cvar") + + q.breakable + q.pp(value) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :cvar, value: value, loc: location, cmts: comments }.to_json( + *opts + ) + end + end + + # Def represents defining a regular method on the current self object. + # + # def method(param) result end + # + class Def < Node + # [Backtick | Const | Ident | Kw | Op] the name of the method + attr_reader :name + + # [Params | Paren] the parameter declaration for the method + attr_reader :params + + # [BodyStmt] the expressions to be executed by the method + attr_reader :bodystmt + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(name:, params:, bodystmt:, location:, comments: []) + @name = name + @params = params + @bodystmt = bodystmt + @location = location + @comments = comments + end + + def child_nodes + [name, params, bodystmt] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + name: name, + params: params, + bodystmt: bodystmt, + location: location, + comments: comments + } + end + + def format(q) + q.group do + q.group do + q.text("def ") + q.format(name) + q.format(params) if !params.is_a?(Params) || !params.empty? + end + + unless bodystmt.empty? + q.indent do + q.breakable(force: true) + q.format(bodystmt) + end + end + + q.breakable(force: true) + q.text("end") + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("def") + + q.breakable + q.pp(name) + + q.breakable + q.pp(params) + + q.breakable + q.pp(bodystmt) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :def, + name: name, + params: params, + bodystmt: bodystmt, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # DefEndless represents defining a single-line method since Ruby 3.0+. + # + # def method = result + # + class DefEndless < Node + # [untyped] the target where the method is being defined + attr_reader :target + + # [Op | Period] the operator being used to declare the method + attr_reader :operator + + # [Backtick | Const | Ident | Kw | Op] the name of the method + attr_reader :name + + # [nil | Params | Paren] the parameter declaration for the method + attr_reader :paren + + # [untyped] the expression to be executed by the method + attr_reader :statement + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize( + target:, + operator:, + name:, + paren:, + statement:, + location:, + comments: [] + ) + @target = target + @operator = operator + @name = name + @paren = paren + @statement = statement + @location = location + @comments = comments + end + + def child_nodes + [target, operator, name, paren, statement] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + target: target, + operator: operator, + name: name, + paren: paren, + statement: statement, + location: location, + comments: comments + } + end + + def format(q) + q.group do + q.text("def ") + + if target + q.format(target) + q.format(CallOperatorFormatter.new(operator), stackable: false) + end + + q.format(name) + + if paren + params = paren + params = params.contents if params.is_a?(Paren) + q.format(paren) unless params.empty? + end + + q.text(" =") + q.group do + q.indent do + q.breakable + q.format(statement) + end + end + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("def_endless") + + if target + q.breakable + q.pp(target) + + q.breakable + q.pp(operator) + end + + q.breakable + q.pp(name) + + if paren + q.breakable + q.pp(paren) + end + + q.breakable + q.pp(statement) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :def_endless, + name: name, + paren: paren, + stmt: statement, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # Defined represents the use of the +defined?+ operator. It can be used with + # and without parentheses. + # + # defined?(variable) + # + class Defined < Node + # [untyped] the value being sent to the keyword + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) + @value = value + @location = location + @comments = comments + end + + def child_nodes + [value] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + + def format(q) + q.group(0, "defined?(", ")") do + q.indent do + q.breakable("") + q.format(value) + end + q.breakable("") + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("defined") + + q.breakable + q.pp(value) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :defined, value: value, loc: location, cmts: comments }.to_json( + *opts + ) + end + end + + # Defs represents defining a singleton method on an object. + # + # def object.method(param) result end + # + class Defs < Node + # [untyped] the target where the method is being defined + attr_reader :target + + # [Op | Period] the operator being used to declare the method + attr_reader :operator + + # [Backtick | Const | Ident | Kw | Op] the name of the method + attr_reader :name + + # [Params | Paren] the parameter declaration for the method + attr_reader :params + + # [BodyStmt] the expressions to be executed by the method + attr_reader :bodystmt + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize( + target:, + operator:, + name:, + params:, + bodystmt:, + location:, + comments: [] + ) + @target = target + @operator = operator + @name = name + @params = params + @bodystmt = bodystmt + @location = location + @comments = comments + end + + def child_nodes + [target, operator, name, params, bodystmt] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + target: target, + operator: operator, + name: name, + params: params, + bodystmt: bodystmt, + location: location, + comments: comments + } + end + + def format(q) + q.group do + q.group do + q.text("def ") + q.format(target) + q.format(CallOperatorFormatter.new(operator), stackable: false) + q.format(name) + q.format(params) if !params.is_a?(Params) || !params.empty? + end + + unless bodystmt.empty? + q.indent do + q.breakable(force: true) + q.format(bodystmt) + end + end + + q.breakable(force: true) + q.text("end") + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("defs") + + q.breakable + q.pp(target) + + q.breakable + q.pp(operator) + + q.breakable + q.pp(name) + + q.breakable + q.pp(params) + + q.breakable + q.pp(bodystmt) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :defs, + target: target, + op: operator, + name: name, + params: params, + bodystmt: bodystmt, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # DoBlock represents passing a block to a method call using the +do+ and +end+ + # keywords. + # + # method do |value| + # end + # + class DoBlock < Node + # [Kw] the do keyword that opens this block + attr_reader :keyword + + # [nil | BlockVar] the optional variable declaration within this block + attr_reader :block_var + + # [BodyStmt] the expressions to be executed within this block + attr_reader :bodystmt + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(keyword:, block_var:, bodystmt:, location:, comments: []) + @keyword = keyword + @block_var = block_var + @bodystmt = bodystmt + @location = location + @comments = comments + end + + def child_nodes + [keyword, block_var, bodystmt] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + keyword: keyword, + block_var: block_var, + bodystmt: bodystmt, + location: location, + comments: comments + } + end + + def format(q) + BlockFormatter.new(self, keyword, "end", bodystmt).format(q) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("do_block") + + if block_var + q.breakable + q.pp(block_var) + end + + q.breakable + q.pp(bodystmt) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :do_block, + keyword: keyword, + block_var: block_var, + bodystmt: bodystmt, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # Responsible for formatting Dot2 and Dot3 nodes. + class DotFormatter + # [String] the operator to display + attr_reader :operator + + # [Dot2 | Dot3] the node that is being formatter + attr_reader :node + + def initialize(operator, node) + @operator = operator + @node = node + end + + def format(q) + space = [If, IfMod, Unless, UnlessMod].include?(q.parent.class) + + left = node.left + right = node.right + + q.format(left) if left + q.text(" ") if space + q.text(operator) + q.text(" ") if space + q.format(right) if right + end + end + + # Dot2 represents using the .. operator between two expressions. Usually this + # is to create a range object. + # + # 1..2 + # + # Sometimes this operator is used to create a flip-flop. + # + # if value == 5 .. value == 10 + # end + # + # One of the sides of the expression may be nil, but not both. + class Dot2 < Node + # [nil | untyped] the left side of the expression + attr_reader :left + + # [nil | untyped] the right side of the expression + attr_reader :right + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(left:, right:, location:, comments: []) + @left = left + @right = right + @location = location + @comments = comments + end + + def child_nodes + [left, right] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { left: left, right: right, location: location, comments: comments } + end + + def format(q) + DotFormatter.new("..", self).format(q) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("dot2") + + if left + q.breakable + q.pp(left) + end + + if right + q.breakable + q.pp(right) + end + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :dot2, + left: left, + right: right, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # Dot3 represents using the ... operator between two expressions. Usually this + # is to create a range object. It's effectively the same event as the Dot2 + # node but with this operator you're asking Ruby to omit the final value. + # + # 1...2 + # + # Like Dot2 it can also be used to create a flip-flop. + # + # if value == 5 ... value == 10 + # end + # + # One of the sides of the expression may be nil, but not both. + class Dot3 < Node + # [nil | untyped] the left side of the expression + attr_reader :left + + # [nil | untyped] the right side of the expression + attr_reader :right + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(left:, right:, location:, comments: []) + @left = left + @right = right + @location = location + @comments = comments + end + + def child_nodes + [left, right] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { left: left, right: right, location: location, comments: comments } + end + + def format(q) + DotFormatter.new("...", self).format(q) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("dot3") + + if left + q.breakable + q.pp(left) + end + + if right + q.breakable + q.pp(right) + end + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :dot3, + left: left, + right: right, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # Responsible for providing information about quotes to be used for strings + # and dynamic symbols. + module Quotes + # The matching pairs of quotes that can be used with % literals. + PAIRS = { "(" => ")", "[" => "]", "{" => "}", "<" => ">" }.freeze + + # If there is some part of this string that matches an escape sequence or + # that contains the interpolation pattern ("#{"), then we are locked into + # whichever quote the user chose. (If they chose single quotes, then double + # quoting would activate the escape sequence, and if they chose double + # quotes, then single quotes would deactivate it.) + def self.locked?(node) + node.parts.any? do |part| + part.is_a?(TStringContent) && part.value.match?(/#[@${]|[\\]/) + end + end + + # Find the matching closing quote for the given opening quote. + def self.matching(quote) + PAIRS.fetch(quote) { quote } + end + + # Escape and unescape single and double quotes as needed to be able to + # enclose +content+ with +enclosing+. + def self.normalize(content, enclosing) + return content if enclosing != "\"" && enclosing != "'" + + content.gsub(/\\([\s\S])|(['"])/) do + _match, escaped, quote = Regexp.last_match.to_a + + if quote == enclosing + "\\#{quote}" + elsif quote + quote + else + "\\#{escaped}" + end + end + end + end + + # DynaSymbol represents a symbol literal that uses quotes to dynamically + # define its value. + # + # :"#{variable}" + # + # They can also be used as a special kind of dynamic hash key, as in: + # + # { "#{key}": value } + # + class DynaSymbol < Node + # [Array[ StringDVar | StringEmbExpr | TStringContent ]] the parts of the + # dynamic symbol + attr_reader :parts + + # [String] the quote used to delimit the dynamic symbol + attr_reader :quote + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(parts:, quote:, location:, comments: []) + @parts = parts + @quote = quote + @location = location + @comments = comments + end + + def child_nodes + parts + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { parts: parts, quote: quote, location: location, comments: comments } + end + + def format(q) + opening_quote, closing_quote = quotes(q) + + q.group(0, opening_quote, closing_quote) do + parts.each do |part| + if part.is_a?(TStringContent) + value = Quotes.normalize(part.value, closing_quote) + separator = -> { q.breakable(force: true, indent: false) } + q.seplist(value.split(/\r?\n/, -1), separator) do |text| + q.text(text) + end + else + q.format(part) + end + end + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("dyna_symbol") + + q.breakable + q.group(2, "(", ")") { q.seplist(parts) { |part| q.pp(part) } } + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :dyna_symbol, + parts: parts, + quote: quote, + loc: location, + cmts: comments + }.to_json(*opts) + end + + private + + # Here we determine the quotes to use for a dynamic symbol. It's bound by a + # lot of rules because it could be in many different contexts with many + # different kinds of escaping. + def quotes(q) + # If we're inside of an assoc node as the key, then it will handle + # printing the : on its own since it could change sides. + parent = q.parent + hash_key = parent.is_a?(Assoc) && parent.key == self + + if quote.start_with?("%s") + # Here we're going to check if there is a closing character, a new line, + # or a quote in the content of the dyna symbol. If there is, then + # quoting could get weird, so just bail out and stick to the original + # quotes in the source. + matching = Quotes.matching(quote[2]) + pattern = /[\n#{Regexp.escape(matching)}'"]/ + + if parts.any? { |part| + part.is_a?(TStringContent) && part.value.match?(pattern) + } + [quote, matching] + elsif Quotes.locked?(self) + ["#{":" unless hash_key}'", "'"] + else + ["#{":" unless hash_key}#{q.quote}", q.quote] + end + elsif Quotes.locked?(self) + if quote.start_with?(":") + [hash_key ? quote[1..-1] : quote, quote[1..-1]] + else + [hash_key ? quote : ":#{quote}", quote] + end + else + [hash_key ? q.quote : ":#{q.quote}", q.quote] + end + end + end + + # Else represents the end of an +if+, +unless+, or +case+ chain. + # + # if variable + # else + # end + # + class Else < Node + # [Statements] the expressions to be executed + attr_reader :statements + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(statements:, location:, comments: []) + @statements = statements + @location = location + @comments = comments + end + + def child_nodes + [statements] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { statements: statements, location: location, comments: comments } + end + + def format(q) + q.group do + q.text("else") + + unless statements.empty? + q.indent do + q.breakable(force: true) + q.format(statements) + end + end + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("else") + + q.breakable + q.pp(statements) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :else, stmts: statements, loc: location, cmts: comments }.to_json( + *opts + ) + end + end + + # Elsif represents another clause in an +if+ or +unless+ chain. + # + # if variable + # elsif other_variable + # end + # + class Elsif < Node + # [untyped] the expression to be checked + attr_reader :predicate + + # [Statements] the expressions to be executed + attr_reader :statements + + # [nil | Elsif | Else] the next clause in the chain + attr_reader :consequent + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize( + predicate:, + statements:, + consequent:, + location:, + comments: [] + ) + @predicate = predicate + @statements = statements + @consequent = consequent + @location = location + @comments = comments + end + + def child_nodes + [predicate, statements, consequent] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + predicate: predicate, + statements: statements, + consequent: consequent, + location: location, + comments: comments + } + end + + def format(q) + q.group do + q.group do + q.text("elsif ") + q.nest("elsif".length - 1) { q.format(predicate) } + end + + unless statements.empty? + q.indent do + q.breakable(force: true) + q.format(statements) + end + end + + if consequent + q.group do + q.breakable(force: true) + q.format(consequent) + end + end + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("elsif") + + q.breakable + q.pp(predicate) + + q.breakable + q.pp(statements) + + if consequent + q.breakable + q.pp(consequent) + end + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :elsif, + pred: predicate, + stmts: statements, + cons: consequent, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # EmbDoc represents a multi-line comment. + # + # =begin + # first line + # second line + # =end + # + class EmbDoc < Node + # [String] the contents of the comment + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + def initialize(value:, location:) + @value = value + @location = location + end + + def inline? + false + end + + def ignore? + false + end + + def comments + [] + end + + def child_nodes + [] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location } + end + + def format(q) + q.trim + q.text(value) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("embdoc") + + q.breakable + q.pp(value) + end + end + + def to_json(*opts) + { type: :embdoc, value: value, loc: location }.to_json(*opts) + end + end + + # EmbExprBeg represents the beginning token for using interpolation inside of + # a parent node that accepts string content (like a string or regular + # expression). + # + # "Hello, #{person}!" + # + class EmbExprBeg < Node + # [String] the #{ used in the string + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + def initialize(value:, location:) + @value = value + @location = location + end + end + + # EmbExprEnd represents the ending token for using interpolation inside of a + # parent node that accepts string content (like a string or regular + # expression). + # + # "Hello, #{person}!" + # + class EmbExprEnd < Node + # [String] the } used in the string + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + def initialize(value:, location:) + @value = value + @location = location + end + end + + # EmbVar represents the use of shorthand interpolation for an instance, class, + # or global variable into a parent node that accepts string content (like a + # string or regular expression). + # + # "#@variable" + # + # In the example above, an EmbVar node represents the # because it forces + # @variable to be interpolated. + class EmbVar < Node + # [String] the # used in the string + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + def initialize(value:, location:) + @value = value + @location = location + end + end + + # Ensure represents the use of the +ensure+ keyword and its subsequent + # statements. + # + # begin + # ensure + # end + # + class Ensure < Node + # [Kw] the ensure keyword that began this node + attr_reader :keyword + + # [Statements] the expressions to be executed + attr_reader :statements + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(keyword:, statements:, location:, comments: []) + @keyword = keyword + @statements = statements + @location = location + @comments = comments + end + + def child_nodes + [keyword, statements] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + keyword: keyword, + statements: statements, + location: location, + comments: comments + } + end + + def format(q) + q.format(keyword) + + unless statements.empty? + q.indent do + q.breakable(force: true) + q.format(statements) + end + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("ensure") + + q.breakable + q.pp(statements) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :ensure, + keyword: keyword, + stmts: statements, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # ExcessedComma represents a trailing comma in a list of block parameters. It + # changes the block parameters such that they will destructure. + # + # [[1, 2, 3], [2, 3, 4]].each do |first, second,| + # end + # + # In the above example, an ExcessedComma node would appear in the third + # position of the Params node that is used to declare that block. The third + # position typically represents a rest-type parameter, but in this case is + # used to indicate that a trailing comma was used. + class ExcessedComma < Node + # [String] the comma + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) + @value = value + @location = location + @comments = comments + end + + def child_nodes + [] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + + def format(q) + q.text(value) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("excessed_comma") + + q.breakable + q.pp(value) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :excessed_comma, + value: value, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # FCall represents the piece of a method call that comes before any arguments + # (i.e., just the name of the method). It is used in places where the parser + # is sure that it is a method call and not potentially a local variable. + # + # method(argument) + # + # In the above example, it's referring to the +method+ segment. + class FCall < Node + # [Const | Ident] the name of the method + attr_reader :value + + # [nil | ArgParen | Args] the arguments to the method call + attr_reader :arguments + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, arguments:, location:, comments: []) + @value = value + @arguments = arguments + @location = location + @comments = comments + end + + def child_nodes + [value, arguments] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + value: value, + arguments: arguments, + location: location, + comments: comments + } + end + + def format(q) + q.format(value) + q.format(arguments) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("fcall") + + q.breakable + q.pp(value) + + if arguments + q.breakable + q.pp(arguments) + end + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :fcall, + value: value, + args: arguments, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # Field is always the child of an assignment. It represents assigning to a + # “field” on an object. + # + # object.variable = value + # + class Field < Node + # [untyped] the parent object that owns the field being assigned + attr_reader :parent + + # [:"::" | Op | Period] the operator being used for the assignment + attr_reader :operator + + # [Const | Ident] the name of the field being assigned + attr_reader :name + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(parent:, operator:, name:, location:, comments: []) + @parent = parent + @operator = operator + @name = name + @location = location + @comments = comments + end + + def child_nodes + [parent, (operator if operator != :"::"), name] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + parent: parent, + operator: operator, + name: name, + location: location, + comments: comments + } + end + + def format(q) + q.group do + q.format(parent) + q.format(CallOperatorFormatter.new(operator), stackable: false) + q.format(name) + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("field") + + q.breakable + q.pp(parent) + + q.breakable + q.pp(operator) + + q.breakable + q.pp(name) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :field, + parent: parent, + op: operator, + name: name, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # FloatLiteral represents a floating point number literal. + # + # 1.0 + # + class FloatLiteral < Node + # [String] the value of the floating point number literal + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) + @value = value + @location = location + @comments = comments + end + + def child_nodes + [] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + + def format(q) + q.text(value) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("float") + + q.breakable + q.pp(value) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :float, value: value, loc: location, cmts: comments }.to_json( + *opts + ) + end + end + + # FndPtn represents matching against a pattern where you find a pattern in an + # array using the Ruby 3.0+ pattern matching syntax. + # + # case value + # in [*, 7, *] + # end + # + class FndPtn < Node + # [nil | untyped] the optional constant wrapper + attr_reader :constant + + # [VarField] the splat on the left-hand side + attr_reader :left + + # [Array[ untyped ]] the list of positional expressions in the pattern that + # are being matched + attr_reader :values + + # [VarField] the splat on the right-hand side + attr_reader :right + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(constant:, left:, values:, right:, location:, comments: []) + @constant = constant + @left = left + @values = values + @right = right + @location = location + @comments = comments + end + + def child_nodes + [constant, left, *values, right] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + constant: constant, + left: left, + values: values, + right: right, + location: location, + comments: comments + } + end + + def format(q) + q.format(constant) if constant + q.group(0, "[", "]") do + q.text("*") + q.format(left) + q.comma_breakable + + q.seplist(values) { |value| q.format(value) } + q.comma_breakable + + q.text("*") + q.format(right) + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("fndptn") + + if constant + q.breakable + q.pp(constant) + end + + q.breakable + q.pp(left) + + q.breakable + q.group(2, "(", ")") { q.seplist(values) { |value| q.pp(value) } } + + q.breakable + q.pp(right) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :fndptn, + constant: constant, + left: left, + values: values, + right: right, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # For represents using a +for+ loop. + # + # for value in list do + # end + # + class For < Node + # [MLHS | VarField] the variable declaration being used to + # pull values out of the object being enumerated + attr_reader :index + + # [untyped] the object being enumerated in the loop + attr_reader :collection + + # [Statements] the statements to be executed + attr_reader :statements + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(index:, collection:, statements:, location:, comments: []) + @index = index + @collection = collection + @statements = statements + @location = location + @comments = comments + end + + def child_nodes + [index, collection, statements] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + index: index, + collection: collection, + statements: statements, + location: location, + comments: comments + } + end + + def format(q) + q.group do + q.text("for ") + q.group { q.format(index) } + q.text(" in ") + q.format(collection) + + unless statements.empty? + q.indent do + q.breakable(force: true) + q.format(statements) + end + end + + q.breakable(force: true) + q.text("end") + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("for") + + q.breakable + q.pp(index) + + q.breakable + q.pp(collection) + + q.breakable + q.pp(statements) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :for, + index: index, + collection: collection, + stmts: statements, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # GVar represents a global variable literal. + # + # $variable + # + class GVar < Node + # [String] the name of the global variable + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) + @value = value + @location = location + @comments = comments + end + + def child_nodes + [] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + + def format(q) + q.text(value) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("gvar") + + q.breakable + q.pp(value) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :gvar, value: value, loc: location, cmts: comments }.to_json( + *opts + ) + end + end + + # HashLiteral represents a hash literal. + # + # { key => value } + # + class HashLiteral < Node + # [LBrace] the left brace that opens this hash + attr_reader :lbrace + + # [Array[ AssocNew | AssocSplat ]] the optional contents of the hash + attr_reader :assocs + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(lbrace:, assocs:, location:, comments: []) + @lbrace = lbrace + @assocs = assocs + @location = location + @comments = comments + end + + def child_nodes + [lbrace] + assocs + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { lbrace: lbrace, assocs: assocs, location: location, comments: comments } + end + + def format(q) + if q.parent.is_a?(Assoc) + format_contents(q) + else + q.group { format_contents(q) } + end + end + + def format_key(q, key) + (@key_formatter ||= HashKeyFormatter.for(self)).format_key(q, key) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("hash") + + if assocs.any? + q.breakable + q.group(2, "(", ")") { q.seplist(assocs) { |assoc| q.pp(assoc) } } + end + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :hash, assocs: assocs, loc: location, cmts: comments }.to_json( + *opts + ) + end + + private + + def format_contents(q) + q.format(lbrace) + + if assocs.empty? + q.breakable("") + else + q.indent do + q.breakable + q.seplist(assocs) { |assoc| q.format(assoc) } + end + q.breakable + end + + q.text("}") + end + end + + # Heredoc represents a heredoc string literal. + # + # <<~DOC + # contents + # DOC + # + class Heredoc < Node + # [HeredocBeg] the opening of the heredoc + attr_reader :beginning + + # [String] the ending of the heredoc + attr_reader :ending + + # [Array[ StringEmbExpr | StringDVar | TStringContent ]] the parts of the + # heredoc string literal + attr_reader :parts + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(beginning:, ending: nil, parts: [], location:, comments: []) + @beginning = beginning + @ending = ending + @parts = parts + @location = location + @comments = comments + end + + def child_nodes + [beginning, *parts] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + beginning: beginning, + location: location, + ending: ending, + parts: parts, + comments: comments + } + end + + def format(q) + # This is a very specific behavior that should probably be included in the + # prettyprint module. It's when you want to force a newline, but don't + # want to force the break parent. + breakable = -> do + q.target << + PrettyPrint::Breakable.new(" ", 1, indent: false, force: true) + end + + q.group do + q.format(beginning) + + q.line_suffix(priority: Formatter::HEREDOC_PRIORITY) do + q.group do + breakable.call + + parts.each do |part| + if part.is_a?(TStringContent) + texts = part.value.split(/\r?\n/, -1) + q.seplist(texts, breakable) { |text| q.text(text) } + else + q.format(part) + end + end + + q.text(ending) + end + end + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("heredoc") + + q.breakable + q.group(2, "(", ")") { q.seplist(parts) { |part| q.pp(part) } } + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :heredoc, + beging: beginning, + ending: ending, + parts: parts, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # HeredocBeg represents the beginning declaration of a heredoc. + # + # <<~DOC + # contents + # DOC + # + # In the example above the HeredocBeg node represents <<~DOC. + class HeredocBeg < Node + # [String] the opening declaration of the heredoc + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) + @value = value + @location = location + @comments = comments + end + + def child_nodes + [] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + + def format(q) + q.text(value) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("heredoc_beg") + + q.breakable + q.pp(value) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :heredoc_beg, + value: value, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # HshPtn represents matching against a hash pattern using the Ruby 2.7+ + # pattern matching syntax. + # + # case value + # in { key: } + # end + # + class HshPtn < Node + class KeywordFormatter + # [Label] the keyword being used + attr_reader :key + + # [untyped] the optional value for the keyword + attr_reader :value + + def initialize(key, value) + @key = key + @value = value + end + + def comments + [] + end + + def format(q) + q.format(key) + + if value + q.text(" ") + q.format(value) + end + end + end + + class KeywordRestFormatter + # [VarField] the parameter that matches the remaining keywords + attr_reader :keyword_rest + + def initialize(keyword_rest) + @keyword_rest = keyword_rest + end + + def comments + [] + end + + def format(q) + q.text("**") + q.format(keyword_rest) + end + end + + # [nil | untyped] the optional constant wrapper + attr_reader :constant + + # [Array[ [Label, untyped] ]] the set of tuples representing the keywords + # that should be matched against in the pattern + attr_reader :keywords + + # [nil | VarField] an optional parameter to gather up all remaining keywords + attr_reader :keyword_rest + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(constant:, keywords:, keyword_rest:, location:, comments: []) + @constant = constant + @keywords = keywords + @keyword_rest = keyword_rest + @location = location + @comments = comments + end + + def child_nodes + [constant, *keywords.flatten(1), keyword_rest] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + constant: constant, + keywords: keywords, + keyword_rest: keyword_rest, + location: location, + comments: comments + } + end + + def format(q) + parts = keywords.map { |(key, value)| KeywordFormatter.new(key, value) } + parts << KeywordRestFormatter.new(keyword_rest) if keyword_rest + contents = -> do + q.seplist(parts) { |part| q.format(part, stackable: false) } + end + + if constant + q.format(constant) + q.group(0, "[", "]", &contents) + return + end + + parent = q.parent + if PATTERNS.include?(parent.class) + q.text("{ ") + contents.call + q.text(" }") + else + contents.call + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("hshptn") + + if constant + q.breakable + q.pp(constant) + end + + if keywords.any? + q.breakable + q.group(2, "(", ")") do + q.seplist(keywords) do |(key, value)| + q.group(2, "(", ")") do + q.pp(key) + + if value + q.breakable + q.pp(value) + end + end + end + end + end + + if keyword_rest + q.breakable + q.pp(keyword_rest) + end + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :hshptn, + constant: constant, + keywords: keywords, + kwrest: keyword_rest, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # The list of nodes that represent patterns inside of pattern matching so that + # when a pattern is being printed it knows if it's nested. + PATTERNS = [AryPtn, Binary, FndPtn, HshPtn, RAssign] + + # Ident represents an identifier anywhere in code. It can represent a very + # large number of things, depending on where it is in the syntax tree. + # + # value + # + class Ident < Node + # [String] the value of the identifier + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) + @value = value + @location = location + @comments = comments + end + + def child_nodes + [] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + + def format(q) + q.text(value) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("ident") + + q.breakable + q.pp(value) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :ident, + value: value.force_encoding("UTF-8"), + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # If the predicate of a conditional or loop contains an assignment (in which + # case we can't know for certain that that assignment doesn't impact the + # statements inside the conditional) then we can't use the modifier form + # and we must use the block form. + module ContainsAssignment + def self.call(parent) + queue = [parent] + + while node = queue.shift + return true if [Assign, MAssign, OpAssign].include?(node.class) + queue += node.child_nodes + end + + false + end + end + + # Formats an If or Unless node. + class ConditionalFormatter + # [String] the keyword associated with this conditional + attr_reader :keyword + + # [If | Unless] the node that is being formatted + attr_reader :node + + def initialize(keyword, node) + @keyword = keyword + @node = node + end + + def format(q) + # If the predicate of the conditional contains an assignment (in which + # case we can't know for certain that that assignment doesn't impact the + # statements inside the conditional) then we can't use the modifier form + # and we must use the block form. + if ContainsAssignment.call(node.predicate) + format_break(q, force: true) + return + end + + if node.consequent || node.statements.empty? + q.group { format_break(q, force: true) } + else + q.group do + q.if_break { format_break(q, force: false) }.if_flat do + Parentheses.flat(q) do + q.format(node.statements) + q.text(" #{keyword} ") + q.format(node.predicate) + end + end + end + end + end + + private + + def format_break(q, force:) + q.text("#{keyword} ") + q.nest(keyword.length + 1) { q.format(node.predicate) } + + unless node.statements.empty? + q.indent do + q.breakable(force: force) + q.format(node.statements) + end + end + + if node.consequent + q.breakable(force: force) + q.format(node.consequent) + end + + q.breakable(force: force) + q.text("end") + end + end + + # If represents the first clause in an +if+ chain. + # + # if predicate + # end + # + class If < Node + # [untyped] the expression to be checked + attr_reader :predicate + + # [Statements] the expressions to be executed + attr_reader :statements + + # [nil, Elsif, Else] the next clause in the chain + attr_reader :consequent + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize( + predicate:, + statements:, + consequent:, + location:, + comments: [] + ) + @predicate = predicate + @statements = statements + @consequent = consequent + @location = location + @comments = comments + end + + def child_nodes + [predicate, statements, consequent] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + predicate: predicate, + statements: statements, + consequent: consequent, + location: location, + comments: comments + } + end + + def format(q) + ConditionalFormatter.new("if", self).format(q) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("if") + + q.breakable + q.pp(predicate) + + q.breakable + q.pp(statements) + + if consequent + q.breakable + q.pp(consequent) + end + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :if, + pred: predicate, + stmts: statements, + cons: consequent, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # IfOp represents a ternary clause. + # + # predicate ? truthy : falsy + # + class IfOp < Node + # [untyped] the expression to be checked + attr_reader :predicate + + # [untyped] the expression to be executed if the predicate is truthy + attr_reader :truthy + + # [untyped] the expression to be executed if the predicate is falsy + attr_reader :falsy + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(predicate:, truthy:, falsy:, location:, comments: []) + @predicate = predicate + @truthy = truthy + @falsy = falsy + @location = location + @comments = comments + end + + def child_nodes + [predicate, truthy, falsy] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + predicate: predicate, + truthy: truthy, + falsy: falsy, + location: location, + comments: comments + } + end + + def format(q) + force_flat = [ + Alias, Assign, Break, Command, CommandCall, Heredoc, If, IfMod, IfOp, + Lambda, MAssign, Next, OpAssign, RescueMod, Return, Return0, Super, + Undef, Unless, UnlessMod, UntilMod, VarAlias, VoidStmt, WhileMod, Yield, + Yield0, ZSuper + ] + + if force_flat.include?(truthy.class) || force_flat.include?(falsy.class) + q.group { format_flat(q) } + return + end + + q.group { q.if_break { format_break(q) }.if_flat { format_flat(q) } } + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("ifop") + + q.breakable + q.pp(predicate) + + q.breakable + q.pp(truthy) + + q.breakable + q.pp(falsy) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :ifop, + pred: predicate, + tthy: truthy, + flsy: falsy, + loc: location, + cmts: comments + }.to_json(*opts) + end + + private + + def format_break(q) + Parentheses.break(q) do + q.text("if ") + q.nest("if ".length) { q.format(predicate) } + + q.indent do + q.breakable + q.format(truthy) + end + + q.breakable + q.text("else") + + q.indent do + q.breakable + q.format(falsy) + end + + q.breakable + q.text("end") + end + end + + def format_flat(q) + q.format(predicate) + q.text(" ?") + + q.breakable + q.format(truthy) + q.text(" :") + + q.breakable + q.format(falsy) + end + end + + # Formats an IfMod or UnlessMod node. + class ConditionalModFormatter + # [String] the keyword associated with this conditional + attr_reader :keyword + + # [IfMod | UnlessMod] the node that is being formatted + attr_reader :node + + def initialize(keyword, node) + @keyword = keyword + @node = node + end + + def format(q) + if ContainsAssignment.call(node.statement) || q.parent.is_a?(In) + q.group { format_flat(q) } + else + q.group { q.if_break { format_break(q) }.if_flat { format_flat(q) } } + end + end + + private + + def format_break(q) + q.text("#{keyword} ") + q.nest(keyword.length + 1) { q.format(node.predicate) } + q.indent do + q.breakable + q.format(node.statement) + end + q.breakable + q.text("end") + end + + def format_flat(q) + Parentheses.flat(q) do + q.format(node.statement) + q.text(" #{keyword} ") + q.format(node.predicate) + end + end + end + + # IfMod represents the modifier form of an +if+ statement. + # + # expression if predicate + # + class IfMod < Node + # [untyped] the expression to be executed + attr_reader :statement + + # [untyped] the expression to be checked + attr_reader :predicate + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(statement:, predicate:, location:, comments: []) + @statement = statement + @predicate = predicate + @location = location + @comments = comments + end + + def child_nodes + [statement, predicate] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + statement: statement, + predicate: predicate, + location: location, + comments: comments + } + end + + def format(q) + ConditionalModFormatter.new("if", self).format(q) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("if_mod") + + q.breakable + q.pp(statement) + + q.breakable + q.pp(predicate) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :if_mod, + stmt: statement, + pred: predicate, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # Imaginary represents an imaginary number literal. + # + # 1i + # + class Imaginary < Node + # [String] the value of the imaginary number literal + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) + @value = value + @location = location + @comments = comments + end + + def child_nodes + [] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + + def format(q) + q.text(value) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("imaginary") + + q.breakable + q.pp(value) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :imaginary, value: value, loc: location, cmts: comments }.to_json( + *opts + ) + end + end + + # In represents using the +in+ keyword within the Ruby 2.7+ pattern matching + # syntax. + # + # case value + # in pattern + # end + # + class In < Node + # [untyped] the pattern to check against + attr_reader :pattern + + # [Statements] the expressions to execute if the pattern matched + attr_reader :statements + + # [nil | In | Else] the next clause in the chain + attr_reader :consequent + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(pattern:, statements:, consequent:, location:, comments: []) + @pattern = pattern + @statements = statements + @consequent = consequent + @location = location + @comments = comments + end + + def child_nodes + [pattern, statements, consequent] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + pattern: pattern, + statements: statements, + consequent: consequent, + location: location, + comments: comments + } + end + + def format(q) + keyword = "in " + + q.group do + q.text(keyword) + q.nest(keyword.length) { q.format(pattern) } + + unless statements.empty? + q.indent do + q.breakable(force: true) + q.format(statements) + end + end + + if consequent + q.breakable(force: true) + q.format(consequent) + end + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("in") + + q.breakable + q.pp(pattern) + + q.breakable + q.pp(statements) + + if consequent + q.breakable + q.pp(consequent) + end + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :in, + pattern: pattern, + stmts: statements, + cons: consequent, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # Int represents an integer number literal. + # + # 1 + # + class Int < Node + # [String] the value of the integer + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) + @value = value + @location = location + @comments = comments + end + + def child_nodes + [] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + + def format(q) + if !value.start_with?(/\+?0/) && value.length >= 5 && !value.include?("_") + # If it's a plain integer and it doesn't have any underscores separating + # the values, then we're going to insert them every 3 characters + # starting from the right. + index = (value.length + 2) % 3 + q.text(" #{value}"[index..-1].scan(/.../).join("_").strip) + else + q.text(value) + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("int") + + q.breakable + q.pp(value) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :int, value: value, loc: location, cmts: comments }.to_json(*opts) + end + end + + # IVar represents an instance variable literal. + # + # @variable + # + class IVar < Node + # [String] the name of the instance variable + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) + @value = value + @location = location + @comments = comments + end + + def child_nodes + [] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + + def format(q) + q.text(value) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("ivar") + + q.breakable + q.pp(value) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :ivar, value: value, loc: location, cmts: comments }.to_json( + *opts + ) + end + end + + # Kw represents the use of a keyword. It can be almost anywhere in the syntax + # tree, so you end up seeing it quite a lot. + # + # if value + # end + # + # In the above example, there would be two Kw nodes: one for the if and one + # for the end. Note that anything that matches the list of keywords in Ruby + # will use a Kw, so if you use a keyword in a symbol literal for instance: + # + # :if + # + # then the contents of the symbol node will contain a Kw node. + class Kw < Node + # [String] the value of the keyword + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) + @value = value + @location = location + @comments = comments + end + + def child_nodes + [] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + + def format(q) + q.text(value) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("kw") + + q.breakable + q.pp(value) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :kw, value: value, loc: location, cmts: comments }.to_json(*opts) + end + end + + # KwRestParam represents defining a parameter in a method definition that + # accepts all remaining keyword parameters. + # + # def method(**kwargs) end + # + class KwRestParam < Node + # [nil | Ident] the name of the parameter + attr_reader :name + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(name:, location:, comments: []) + @name = name + @location = location + @comments = comments + end + + def child_nodes + [name] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { name: name, location: location, comments: comments } + end + + def format(q) + q.text("**") + q.format(name) if name + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("kwrest_param") + + q.breakable + q.pp(name) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :kwrest_param, + name: name, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # Label represents the use of an identifier to associate with an object. You + # can find it in a hash key, as in: + # + # { key: value } + # + # In this case "key:" would be the body of the label. You can also find it in + # pattern matching, as in: + # + # case value + # in key: + # end + # + # In this case "key:" would be the body of the label. + class Label < Node + # [String] the value of the label + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) + @value = value + @location = location + @comments = comments + end + + def child_nodes + [] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + + def format(q) + q.text(value) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("label") + + q.breakable + q.text(":") + q.text(value[0...-1]) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :label, value: value, loc: location, cmts: comments }.to_json( + *opts + ) + end + end + + # LabelEnd represents the end of a dynamic symbol. + # + # { "key": value } + # + # In the example above, LabelEnd represents the "\":" token at the end of the + # hash key. This node is important for determining the type of quote being + # used by the label. + class LabelEnd < Node + # [String] the end of the label + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + def initialize(value:, location:) + @value = value + @location = location + end + end + + # Lambda represents using a lambda literal (not the lambda method call). + # + # ->(value) { value * 2 } + # + class Lambda < Node + # [Params | Paren] the parameter declaration for this lambda + attr_reader :params + + # [BodyStmt | Statements] the expressions to be executed in this lambda + attr_reader :statements + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(params:, statements:, location:, comments: []) + @params = params + @statements = statements + @location = location + @comments = comments + end + + def child_nodes + [params, statements] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + params: params, + statements: statements, + location: location, + comments: comments + } + end + + def format(q) + q.group(0, "->") do + if params.is_a?(Paren) + q.format(params) unless params.contents.empty? + elsif !params.empty? + q.group do + q.text("(") + q.format(params) + q.text(")") + end + end + + q.text(" ") + q.if_break do + force_parens = + q.parents.any? do |node| + node.is_a?(Command) || node.is_a?(CommandCall) + end + + q.text(force_parens ? "{" : "do") + q.indent do + q.breakable + q.format(statements) + end + + q.breakable + q.text(force_parens ? "}" : "end") + end.if_flat do + q.text("{ ") + q.format(statements) + q.text(" }") + end + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("lambda") + + q.breakable + q.pp(params) + + q.breakable + q.pp(statements) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :lambda, + params: params, + stmts: statements, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # LBrace represents the use of a left brace, i.e., {. + class LBrace < Node + # [String] the left brace + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) + @value = value + @location = location + @comments = comments + end + + def child_nodes + [] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + + def format(q) + q.text(value) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("lbrace") + + q.breakable + q.pp(value) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :lbrace, value: value, loc: location, cmts: comments }.to_json( + *opts + ) + end + end + + # LBracket represents the use of a left bracket, i.e., [. + class LBracket < Node + # [String] the left bracket + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) + @value = value + @location = location + @comments = comments + end + + def child_nodes + [] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + + def format(q) + q.text(value) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("lbracket") + + q.breakable + q.pp(value) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :lbracket, value: value, loc: location, cmts: comments }.to_json( + *opts + ) + end + end + + # LParen represents the use of a left parenthesis, i.e., (. + class LParen < Node + # [String] the left parenthesis + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) + @value = value + @location = location + @comments = comments + end + + def child_nodes + [] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + + def format(q) + q.text(value) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("lparen") + + q.breakable + q.pp(value) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :lparen, value: value, loc: location, cmts: comments }.to_json( + *opts + ) + end + end + + # MAssign is a parent node of any kind of multiple assignment. This includes + # splitting out variables on the left like: + # + # first, second, third = value + # + # as well as splitting out variables on the right, as in: + # + # value = first, second, third + # + # Both sides support splats, as well as variables following them. There's also + # destructuring behavior that you can achieve with the following: + # + # first, = value + # + class MAssign < Node + # [MLHS | MLHSParen] the target of the multiple assignment + attr_reader :target + + # [untyped] the value being assigned + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(target:, value:, location:, comments: []) + @target = target + @value = value + @location = location + @comments = comments + end + + def child_nodes + [target, value] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { target: target, value: value, location: location, comments: comments } + end + + def format(q) + q.group do + q.group { q.format(target) } + q.text(" =") + q.indent do + q.breakable + q.format(value) + end + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("massign") + + q.breakable + q.pp(target) + + q.breakable + q.pp(value) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :massign, + target: target, + value: value, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # MethodAddBlock represents a method call with a block argument. + # + # method {} + # + class MethodAddBlock < Node + # [Call | Command | CommandCall | FCall] the method call + attr_reader :call + + # [BraceBlock | DoBlock] the block being sent with the method call + attr_reader :block + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(call:, block:, location:, comments: []) + @call = call + @block = block + @location = location + @comments = comments + end + + def child_nodes + [call, block] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { call: call, block: block, location: location, comments: comments } + end + + def format(q) + q.format(call) + q.format(block) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("method_add_block") + + q.breakable + q.pp(call) + + q.breakable + q.pp(block) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :method_add_block, + call: call, + block: block, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # MLHS represents a list of values being destructured on the left-hand side + # of a multiple assignment. + # + # first, second, third = value + # + class MLHS < Node + # Array[ARefField | ArgStar | Field | Ident | MLHSParen | VarField] the + # parts of the left-hand side of a multiple assignment + attr_reader :parts + + # [boolean] whether or not there is a trailing comma at the end of this + # list, which impacts destructuring. It's an attr_accessor so that while + # the syntax tree is being built it can be set by its parent node + attr_accessor :comma + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(parts:, comma: false, location:, comments: []) + @parts = parts + @comma = comma + @location = location + @comments = comments + end + + def child_nodes + parts + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { parts: parts, location: location, comma: comma, comments: comments } + end + + def format(q) + q.seplist(parts) { |part| q.format(part) } + q.text(",") if comma + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("mlhs") + + q.breakable + q.group(2, "(", ")") { q.seplist(parts) { |part| q.pp(part) } } + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :mlhs, + parts: parts, + comma: comma, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # MLHSParen represents parentheses being used to destruct values in a multiple + # assignment on the left hand side. + # + # (left, right) = value + # + class MLHSParen < Node + # [MLHS | MLHSParen] the contents inside of the parentheses + attr_reader :contents + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(contents:, location:, comments: []) + @contents = contents + @location = location + @comments = comments + end + + def child_nodes + [contents] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { contents: contents, location: location, comments: comments } + end + + def format(q) + parent = q.parent + + if parent.is_a?(MAssign) || parent.is_a?(MLHSParen) + q.format(contents) + else + q.group(0, "(", ")") do + q.indent do + q.breakable("") + q.format(contents) + end + + q.breakable("") + end + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("mlhs_paren") + + q.breakable + q.pp(contents) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :mlhs_paren, + cnts: contents, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # ModuleDeclaration represents defining a module using the +module+ keyword. + # + # module Namespace + # end + # + class ModuleDeclaration < Node + # [ConstPathRef | ConstRef | TopConstRef] the name of the module + attr_reader :constant + + # [BodyStmt] the expressions to be executed in the context of the module + attr_reader :bodystmt + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(constant:, bodystmt:, location:, comments: []) + @constant = constant + @bodystmt = bodystmt + @location = location + @comments = comments + end + + def child_nodes + [constant, bodystmt] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + constant: constant, + bodystmt: bodystmt, + location: location, + comments: comments + } + end + + def format(q) + declaration = -> do + q.group do + q.text("module ") + q.format(constant) + end + end + + if bodystmt.empty? + q.group do + declaration.call + q.breakable(force: true) + q.text("end") + end + else + q.group do + declaration.call + + q.indent do + q.breakable(force: true) + q.format(bodystmt) + end + + q.breakable(force: true) + q.text("end") + end + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("module") + + q.breakable + q.pp(constant) + + q.breakable + q.pp(bodystmt) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :module, + constant: constant, + bodystmt: bodystmt, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # MRHS represents the values that are being assigned on the right-hand side of + # a multiple assignment. + # + # values = first, second, third + # + class MRHS < Node + # Array[untyped] the parts that are being assigned + attr_reader :parts + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(parts:, location:, comments: []) + @parts = parts + @location = location + @comments = comments + end + + def child_nodes + parts + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { parts: parts, location: location, comments: comments } + end + + def format(q) + q.seplist(parts) { |part| q.format(part) } + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("mrhs") + + q.breakable + q.group(2, "(", ")") { q.seplist(parts) { |part| q.pp(part) } } + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :mrhs, parts: parts, loc: location, cmts: comments }.to_json( + *opts + ) + end + end + + # Next represents using the +next+ keyword. + # + # next + # + # The +next+ keyword can also optionally be called with an argument: + # + # next value + # + # +next+ can even be called with multiple arguments, but only if parentheses + # are omitted, as in: + # + # next first, second, third + # + # If a single value is being given, parentheses can be used, as in: + # + # next(value) + # + class Next < Node + # [Args] the arguments passed to the next keyword + attr_reader :arguments + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(arguments:, location:, comments: []) + @arguments = arguments + @location = location + @comments = comments + end + + def child_nodes + [arguments] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { arguments: arguments, location: location, comments: comments } + end + + def format(q) + FlowControlFormatter.new("next", self).format(q) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("next") + + q.breakable + q.pp(arguments) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :next, args: arguments, loc: location, cmts: comments }.to_json( + *opts + ) + end + end + + # Op represents an operator literal in the source. + # + # 1 + 2 + # + # In the example above, the Op node represents the + operator. + class Op < Node + # [String] the operator + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) + @value = value + @location = location + @comments = comments + end + + def child_nodes + [] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + + def format(q) + q.text(value) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("op") + + q.breakable + q.pp(value) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :op, value: value, loc: location, cmts: comments }.to_json(*opts) + end + end + + # OpAssign represents assigning a value to a variable or constant using an + # operator like += or ||=. + # + # variable += value + # + class OpAssign < Node + # [ARefField | ConstPathField | Field | TopConstField | VarField] the target + # to assign the result of the expression to + attr_reader :target + + # [Op] the operator being used for the assignment + attr_reader :operator + + # [untyped] the expression to be assigned + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(target:, operator:, value:, location:, comments: []) + @target = target + @operator = operator + @value = value + @location = location + @comments = comments + end + + def child_nodes + [target, operator, value] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + target: target, + operator: operator, + value: value, + location: location, + comments: comments + } + end + + def format(q) + q.group do + q.format(target) + q.text(" ") + q.format(operator) + q.indent do + q.breakable + q.format(value) + end + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("opassign") + + q.breakable + q.pp(target) + + q.breakable + q.pp(operator) + + q.breakable + q.pp(value) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :opassign, + target: target, + op: operator, + value: value, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # If you have a modifier statement (for instance a modifier if statement or a + # modifier while loop) there are times when you need to wrap the entire + # statement in parentheses. This occurs when you have something like: + # + # foo[:foo] = + # if bar? + # baz + # end + # + # Normally we would shorten this to an inline version, which would result in: + # + # foo[:foo] = baz if bar? + # + # but this actually has different semantic meaning. The first example will + # result in a nil being inserted into the hash for the :foo key, whereas the + # second example will result in an empty hash because the if statement applies + # to the entire assignment. + # + # We can fix this in a couple of ways. We can use the then keyword, as in: + # + # foo[:foo] = if bar? then baz end + # + # But this isn't used very often. We can also just leave it as is with the + # multi-line version, but for a short predicate and short value it looks + # verbose. The last option and the one used here is to add parentheses on + # both sides of the expression, as in: + # + # foo[:foo] = (baz if bar?) + # + # This approach maintains the nice conciseness of the inline version, while + # keeping the correct semantic meaning. + module Parentheses + NODES = [Args, Assign, Assoc, Binary, Call, Defined, MAssign, OpAssign] + + def self.flat(q) + return yield unless NODES.include?(q.parent.class) + + q.text("(") + yield + q.text(")") + end + + def self.break(q) + return yield unless NODES.include?(q.parent.class) + + q.text("(") + q.indent do + q.breakable("") + yield + end + q.breakable("") + q.text(")") + end + end + + # def on_operator_ambiguous(value) + # value + # end + + # Params represents defining parameters on a method or lambda. + # + # def method(param) end + # + class Params < Node + class OptionalFormatter + # [Ident] the name of the parameter + attr_reader :name + + # [untyped] the value of the parameter + attr_reader :value + + def initialize(name, value) + @name = name + @value = value + end + + def comments + [] + end + + def format(q) + q.format(name) + q.text(" = ") + q.format(value) + end + end + + class KeywordFormatter + # [Ident] the name of the parameter + attr_reader :name + + # [nil | untyped] the value of the parameter + attr_reader :value + + def initialize(name, value) + @name = name + @value = value + end + + def comments + [] + end + + def format(q) + q.format(name) + + if value + q.text(" ") + q.format(value) + end + end + end + + class KeywordRestFormatter + # [:nil | ArgsForward | KwRestParam] the value of the parameter + attr_reader :value + + def initialize(value) + @value = value + end + + def comments + [] + end + + def format(q) + if value == :nil + q.text("**nil") + else + q.format(value) + end + end + end + + # [Array[ Ident ]] any required parameters + attr_reader :requireds + + # [Array[ [ Ident, untyped ] ]] any optional parameters and their default + # values + attr_reader :optionals + + # [nil | ArgsForward | ExcessedComma | RestParam] the optional rest + # parameter + attr_reader :rest + + # [Array[ Ident ]] any positional parameters that exist after a rest + # parameter + attr_reader :posts + + # [Array[ [ Ident, nil | untyped ] ]] any keyword parameters and their + # optional default values + attr_reader :keywords + + # [nil | :nil | KwRestParam] the optional keyword rest parameter + attr_reader :keyword_rest + + # [nil | BlockArg] the optional block parameter + attr_reader :block + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize( + requireds: [], + optionals: [], + rest: nil, + posts: [], + keywords: [], + keyword_rest: nil, + block: nil, + location:, + comments: [] + ) + @requireds = requireds + @optionals = optionals + @rest = rest + @posts = posts + @keywords = keywords + @keyword_rest = keyword_rest + @block = block + @location = location + @comments = comments + end + + # Params nodes are the most complicated in the tree. Occasionally you want + # to know if they are "empty", which means not having any parameters + # declared. This logic accesses every kind of parameter and determines if + # it's missing. + def empty? + requireds.empty? && optionals.empty? && !rest && posts.empty? && + keywords.empty? && !keyword_rest && !block + end + + def child_nodes + [ + *requireds, + *optionals.flatten(1), + rest, + *posts, + *keywords.flatten(1), + (keyword_rest if keyword_rest != :nil), + block + ] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + location: location, + requireds: requireds, + optionals: optionals, + rest: rest, + posts: posts, + keywords: keywords, + keyword_rest: keyword_rest, + block: block, + comments: comments + } + end + + def format(q) + parts = [ + *requireds, + *optionals.map { |(name, value)| OptionalFormatter.new(name, value) } + ] + + parts << rest if rest && !rest.is_a?(ExcessedComma) + parts += + [ + *posts, + *keywords.map { |(name, value)| KeywordFormatter.new(name, value) } + ] + + parts << KeywordRestFormatter.new(keyword_rest) if keyword_rest + parts << block if block + + contents = -> do + q.seplist(parts) { |part| q.format(part) } + q.format(rest) if rest && rest.is_a?(ExcessedComma) + end + + if [Def, Defs, DefEndless].include?(q.parent.class) + q.group(0, "(", ")") do + q.indent do + q.breakable("") + contents.call + end + q.breakable("") + end + else + q.nest(0, &contents) + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("params") + + if requireds.any? + q.breakable + q.group(2, "(", ")") { q.seplist(requireds) { |name| q.pp(name) } } + end + + if optionals.any? + q.breakable + q.group(2, "(", ")") do + q.seplist(optionals) do |(name, default)| + q.pp(name) + q.text("=") + q.group(2) do + q.breakable("") + q.pp(default) + end + end + end + end + + if rest + q.breakable + q.pp(rest) + end + + if posts.any? + q.breakable + q.group(2, "(", ")") { q.seplist(posts) { |value| q.pp(value) } } + end + + if keywords.any? + q.breakable + q.group(2, "(", ")") do + q.seplist(keywords) do |(name, default)| + q.pp(name) + + if default + q.text("=") + q.group(2) do + q.breakable("") + q.pp(default) + end + end + end + end + end + + if keyword_rest + q.breakable + q.pp(keyword_rest) + end + + if block + q.breakable + q.pp(block) + end + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :params, + reqs: requireds, + opts: optionals, + rest: rest, + posts: posts, + keywords: keywords, + kwrest: keyword_rest, + block: block, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # Paren represents using balanced parentheses in a couple places in a Ruby + # program. In general parentheses can be used anywhere a Ruby expression can + # be used. + # + # (1 + 2) + # + class Paren < Node + # [LParen] the left parenthesis that opened this statement + attr_reader :lparen + + # [nil | untyped] the expression inside the parentheses + attr_reader :contents + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(lparen:, contents:, location:, comments: []) + @lparen = lparen + @contents = contents + @location = location + @comments = comments + end + + def child_nodes + [lparen, contents] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + lparen: lparen, + contents: contents, + location: location, + comments: comments + } + end + + def format(q) + q.group do + q.format(lparen) + + if contents && (!contents.is_a?(Params) || !contents.empty?) + q.indent do + q.breakable("") + q.format(contents) + end + end + + q.breakable("") + q.text(")") + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("paren") + + q.breakable + q.pp(contents) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :paren, + lparen: lparen, + cnts: contents, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # Period represents the use of the +.+ operator. It is usually found in method + # calls. + class Period < Node + # [String] the period + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) + @value = value + @location = location + @comments = comments + end + + def child_nodes + [] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + + def format(q) + q.text(value) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("period") + + q.breakable + q.pp(value) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :period, value: value, loc: location, cmts: comments }.to_json( + *opts + ) + end + end + + # Program represents the overall syntax tree. + class Program < Node + # [Statements] the top-level expressions of the program + attr_reader :statements + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(statements:, location:, comments: []) + @statements = statements + @location = location + @comments = comments + end + + def child_nodes + [statements] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { statements: statements, location: location, comments: comments } + end + + def format(q) + q.format(statements) + + # We're going to put a newline on the end so that it always has one unless + # it ends with the special __END__ syntax. In that case we want to + # replicate the text exactly so we will just let it be. + q.breakable(force: true) unless statements.body.last.is_a?(EndContent) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("program") + + q.breakable + q.pp(statements) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :program, + stmts: statements, + comments: comments, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # QSymbols represents a symbol literal array without interpolation. + # + # %i[one two three] + # + class QSymbols < Node + # [QSymbolsBeg] the token that opens this array literal + attr_reader :beginning + + # [Array[ TStringContent ]] the elements of the array + attr_reader :elements + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(beginning:, elements:, location:, comments: []) + @beginning = beginning + @elements = elements + @location = location + @comments = comments + end + + def child_nodes + [] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + beginning: beginning, + elements: elements, + location: location, + comments: comments + } + end + + def format(q) + opening, closing = "%i[", "]" + + if elements.any? { |element| element.match?(/[\[\]]/) } + opening = beginning.value + closing = Quotes.matching(opening[2]) + end + + q.group(0, opening, closing) do + q.indent do + q.breakable("") + q.seplist(elements, -> { q.breakable }) do |element| + q.format(element) + end + end + q.breakable("") + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("qsymbols") + + q.breakable + q.group(2, "(", ")") { q.seplist(elements) { |element| q.pp(element) } } + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :qsymbols, + elems: elements, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # QSymbolsBeg represents the beginning of a symbol literal array. + # + # %i[one two three] + # + # In the snippet above, QSymbolsBeg represents the "%i[" token. Note that + # these kinds of arrays can start with a lot of different delimiter types + # (e.g., %i| or %i<). + class QSymbolsBeg < Node + # [String] the beginning of the array literal + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + def initialize(value:, location:) + @value = value + @location = location + end + end + + # QWords represents a string literal array without interpolation. + # + # %w[one two three] + # + class QWords < Node + # [QWordsBeg] the token that opens this array literal + attr_reader :beginning + + # [Array[ TStringContent ]] the elements of the array + attr_reader :elements + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(beginning:, elements:, location:, comments: []) + @beginning = beginning + @elements = elements + @location = location + @comments = comments + end + + def child_nodes + [] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + beginning: beginning, + elements: elements, + location: location, + comments: comments + } + end + + def format(q) + opening, closing = "%w[", "]" + + if elements.any? { |element| element.match?(/[\[\]]/) } + opening = beginning.value + closing = Quotes.matching(opening[2]) + end + + q.group(0, opening, closing) do + q.indent do + q.breakable("") + q.seplist(elements, -> { q.breakable }) do |element| + q.format(element) + end + end + q.breakable("") + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("qwords") + + q.breakable + q.group(2, "(", ")") { q.seplist(elements) { |element| q.pp(element) } } + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :qwords, elems: elements, loc: location, cmts: comments }.to_json( + *opts + ) + end + end + + # QWordsBeg represents the beginning of a string literal array. + # + # %w[one two three] + # + # In the snippet above, QWordsBeg represents the "%w[" token. Note that these + # kinds of arrays can start with a lot of different delimiter types (e.g., + # %w| or %w<). + class QWordsBeg < Node + # [String] the beginning of the array literal + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + def initialize(value:, location:) + @value = value + @location = location + end + end + + # RationalLiteral represents the use of a rational number literal. + # + # 1r + # + class RationalLiteral < Node + # [String] the rational number literal + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) + @value = value + @location = location + @comments = comments + end + + def child_nodes + [] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + + def format(q) + q.text(value) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("rational") + + q.breakable + q.pp(value) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :rational, value: value, loc: location, cmts: comments }.to_json( + *opts + ) + end + end + + # RBrace represents the use of a right brace, i.e., +++. + class RBrace < Node + # [String] the right brace + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + def initialize(value:, location:) + @value = value + @location = location + end + end + + # RBracket represents the use of a right bracket, i.e., +]+. + class RBracket < Node + # [String] the right bracket + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + def initialize(value:, location:) + @value = value + @location = location + end + end + + # Redo represents the use of the +redo+ keyword. + # + # redo + # + class Redo < Node + # [String] the value of the keyword + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) + @value = value + @location = location + @comments = comments + end + + def child_nodes + [] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + + def format(q) + q.text(value) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("redo") + + q.breakable + q.pp(value) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :redo, value: value, loc: location, cmts: comments }.to_json( + *opts + ) + end + end + + # RegexpContent represents the body of a regular expression. + # + # /.+ #{pattern} .+/ + # + # In the example above, a RegexpContent node represents everything contained + # within the forward slashes. + class RegexpContent < Node + # [String] the opening of the regular expression + attr_reader :beginning + + # [Array[ StringDVar | StringEmbExpr | TStringContent ]] the parts of the + # regular expression + attr_reader :parts + + # [Location] the location of this node + attr_reader :location + + def initialize(beginning:, parts:, location:) + @beginning = beginning + @parts = parts + @location = location + end + end + + # RegexpBeg represents the start of a regular expression literal. + # + # /.+/ + # + # In the example above, RegexpBeg represents the first / token. Regular + # expression literals can also be declared using the %r syntax, as in: + # + # %r{.+} + # + class RegexpBeg < Node + # [String] the beginning of the regular expression + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + def initialize(value:, location:) + @value = value + @location = location + end + end + + # RegexpEnd represents the end of a regular expression literal. + # + # /.+/m + # + # In the example above, the RegexpEnd event represents the /m at the end of + # the regular expression literal. You can also declare regular expression + # literals using %r, as in: + # + # %r{.+}m + # + class RegexpEnd < Node + # [String] the end of the regular expression + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + def initialize(value:, location:) + @value = value + @location = location + end + end + + # RegexpLiteral represents a regular expression literal. + # + # /.+/ + # + class RegexpLiteral < Node + # [String] the beginning of the regular expression literal + attr_reader :beginning + + # [String] the ending of the regular expression literal + attr_reader :ending + + # [Array[ StringEmbExpr | StringDVar | TStringContent ]] the parts of the + # regular expression literal + attr_reader :parts + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(beginning:, ending:, parts:, location:, comments: []) + @beginning = beginning + @ending = ending + @parts = parts + @location = location + @comments = comments + end + + def child_nodes + parts + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + beginning: beginning, + ending: ending, + parts: parts, + location: location, + comments: comments + } + end + + def format(q) + braces = ambiguous?(q) || include?(%r{\/}) + + if braces && include?(/[{}]/) + q.group do + q.text(beginning) + q.format_each(parts) + q.text(ending) + end + elsif braces + q.group do + q.text("%r{") + + if beginning == "/" + # If we're changing from a forward slash to a %r{, then we can + # replace any escaped forward slashes with regular forward slashes. + parts.each do |part| + if part.is_a?(TStringContent) + q.text(part.value.gsub("\\/", "/")) + else + q.format(part) + end + end + else + q.format_each(parts) + end + + q.text("}") + q.text(ending[1..-1]) + end + else + q.group do + q.text("/") + q.format_each(parts) + q.text("/") + q.text(ending[1..-1]) + end + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("regexp_literal") + + q.breakable + q.group(2, "(", ")") { q.seplist(parts) { |part| q.pp(part) } } + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :regexp_literal, + beging: beginning, + ending: ending, + parts: parts, + loc: location, + cmts: comments + }.to_json(*opts) + end + + private + + def include?(pattern) + parts.any? do |part| + part.is_a?(TStringContent) && part.value.match?(pattern) + end + end + + # If the first part of this regex is plain string content, we have a space + # or an =, and we're contained within a command or command_call node, then + # we want to use braces because otherwise we could end up with an ambiguous + # operator, e.g. foo / bar/ or foo /=bar/ + def ambiguous?(q) + return false if parts.empty? + part = parts.first + + part.is_a?(TStringContent) && part.value.start_with?(" ", "=") && + q.parents.any? { |node| node.is_a?(Command) || node.is_a?(CommandCall) } + end + end + + # RescueEx represents the list of exceptions being rescued in a rescue clause. + # + # begin + # rescue Exception => exception + # end + # + class RescueEx < Node + # [untyped] the list of exceptions being rescued + attr_reader :exceptions + + # [nil | Field | VarField] the expression being used to capture the raised + # exception + attr_reader :variable + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(exceptions:, variable:, location:, comments: []) + @exceptions = exceptions + @variable = variable + @location = location + @comments = comments + end + + def child_nodes + [*exceptions, variable] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + exceptions: exceptions, + variable: variable, + location: location, + comments: comments + } + end + + def format(q) + q.group do + if exceptions + q.text(" ") + q.format(exceptions) + end + + if variable + q.text(" => ") + q.format(variable) + end + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("rescue_ex") + + q.breakable + q.pp(exceptions) + + q.breakable + q.pp(variable) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :rescue_ex, + extns: exceptions, + var: variable, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # Rescue represents the use of the rescue keyword inside of a BodyStmt node. + # + # begin + # rescue + # end + # + class Rescue < Node + # [RescueEx] the exceptions being rescued + attr_reader :exception + + # [Statements] the expressions to evaluate when an error is rescued + attr_reader :statements + + # [nil | Rescue] the optional next clause in the chain + attr_reader :consequent + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize( + exception:, + statements:, + consequent:, + location:, + comments: [] + ) + @exception = exception + @statements = statements + @consequent = consequent + @location = location + @comments = comments + end + + def bind_end(end_char) + @location = + Location.new( + start_line: location.start_line, + start_char: location.start_char, + end_line: location.end_line, + end_char: end_char + ) + + if consequent + consequent.bind_end(end_char) + statements.bind_end(consequent.location.start_char) + else + statements.bind_end(end_char) + end + end + + def child_nodes + [exception, statements, consequent] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + exception: exception, + statements: statements, + consequent: consequent, + location: location, + comments: comments + } + end + + def format(q) + q.group do + q.text("rescue") + + if exception + q.nest("rescue ".length) { q.format(exception) } + else + q.text(" StandardError") + end + + unless statements.empty? + q.indent do + q.breakable(force: true) + q.format(statements) + end + end + + if consequent + q.breakable(force: true) + q.format(consequent) + end + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("rescue") + + if exception + q.breakable + q.pp(exception) + end + + q.breakable + q.pp(statements) + + if consequent + q.breakable + q.pp(consequent) + end + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :rescue, + extn: exception, + stmts: statements, + cons: consequent, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # RescueMod represents the use of the modifier form of a +rescue+ clause. + # + # expression rescue value + # + class RescueMod < Node + # [untyped] the expression to execute + attr_reader :statement + + # [untyped] the value to use if the executed expression raises an error + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(statement:, value:, location:, comments: []) + @statement = statement + @value = value + @location = location + @comments = comments + end + + def child_nodes + [statement, value] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + statement: statement, + value: value, + location: location, + comments: comments + } + end + + def format(q) + q.group(0, "begin", "end") do + q.indent do + q.breakable(force: true) + q.format(statement) + end + q.breakable(force: true) + q.text("rescue StandardError") + q.indent do + q.breakable(force: true) + q.format(value) + end + q.breakable(force: true) + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("rescue_mod") + + q.breakable + q.pp(statement) + + q.breakable + q.pp(value) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :rescue_mod, + stmt: statement, + value: value, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # RestParam represents defining a parameter in a method definition that + # accepts all remaining positional parameters. + # + # def method(*rest) end + # + class RestParam < Node + # [nil | Ident] the name of the parameter + attr_reader :name + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(name:, location:, comments: []) + @name = name + @location = location + @comments = comments + end + + def child_nodes + [name] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { name: name, location: location, comments: comments } + end + + def format(q) + q.text("*") + q.format(name) if name + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("rest_param") + + q.breakable + q.pp(name) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :rest_param, name: name, loc: location, cmts: comments }.to_json( + *opts + ) + end + end + + # Retry represents the use of the +retry+ keyword. + # + # retry + # + class Retry < Node + # [String] the value of the keyword + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) + @value = value + @location = location + @comments = comments + end + + def child_nodes + [] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + + def format(q) + q.text(value) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("retry") + + q.breakable + q.pp(value) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :retry, value: value, loc: location, cmts: comments }.to_json( + *opts + ) + end + end + + # Return represents using the +return+ keyword with arguments. + # + # return value + # + class Return < Node + # [Args] the arguments being passed to the keyword + attr_reader :arguments + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(arguments:, location:, comments: []) + @arguments = arguments + @location = location + @comments = comments + end + + def child_nodes + [arguments] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { arguments: arguments, location: location, comments: comments } + end + + def format(q) + FlowControlFormatter.new("return", self).format(q) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("return") + + q.breakable + q.pp(arguments) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :return, args: arguments, loc: location, cmts: comments }.to_json( + *opts + ) + end + end + + # Return0 represents the bare +return+ keyword with no arguments. + # + # return + # + class Return0 < Node + # [String] the value of the keyword + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) + @value = value + @location = location + @comments = comments + end + + def child_nodes + [] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + + def format(q) + q.text(value) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("return0") + + q.breakable + q.pp(value) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :return0, value: value, loc: location, cmts: comments }.to_json( + *opts + ) + end + end + + # RParen represents the use of a right parenthesis, i.e., +)+. + class RParen < Node + # [String] the parenthesis + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + def initialize(value:, location:) + @value = value + @location = location + end + end + + # SClass represents a block of statements that should be evaluated within the + # context of the singleton class of an object. It's frequently used to define + # singleton methods. + # + # class << self + # end + # + class SClass < Node + # [untyped] the target of the singleton class to enter + attr_reader :target + + # [BodyStmt] the expressions to be executed + attr_reader :bodystmt + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(target:, bodystmt:, location:, comments: []) + @target = target + @bodystmt = bodystmt + @location = location + @comments = comments + end + + def child_nodes + [target, bodystmt] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + target: target, + bodystmt: bodystmt, + location: location, + comments: comments + } + end + + def format(q) + q.group(0, "class << ", "end") do + q.format(target) + q.indent do + q.breakable(force: true) + q.format(bodystmt) + end + q.breakable(force: true) + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("sclass") + + q.breakable + q.pp(target) + + q.breakable + q.pp(bodystmt) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :sclass, + target: target, + bodystmt: bodystmt, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # Everything that has a block of code inside of it has a list of statements. + # Normally we would just track those as a node that has an array body, but we + # have some special handling in order to handle empty statement lists. They + # need to have the right location information, so all of the parent node of + # stmts nodes will report back down the location information. We then + # propagate that onto void_stmt nodes inside the stmts in order to make sure + # all comments get printed appropriately. + class Statements < Node + # [SyntaxTree] the parser that is generating this node + attr_reader :parser + + # [Array[ untyped ]] the list of expressions contained within this node + attr_reader :body + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(parser, body:, location:, comments: []) + @parser = parser + @body = body + @location = location + @comments = comments + end + + def bind(start_char, end_char) + @location = + Location.new( + start_line: location.start_line, + start_char: start_char, + end_line: location.end_line, + end_char: end_char + ) + + if body[0].is_a?(VoidStmt) + location = body[0].location + location = + Location.new( + start_line: location.start_line, + start_char: start_char, + end_line: location.end_line, + end_char: start_char + ) + + body[0] = VoidStmt.new(location: location) + end + + attach_comments(start_char, end_char) + end + + def bind_end(end_char) + @location = + Location.new( + start_line: location.start_line, + start_char: location.start_char, + end_line: location.end_line, + end_char: end_char + ) + end + + def empty? + body.all? do |statement| + statement.is_a?(VoidStmt) && statement.comments.empty? + end + end + + def child_nodes + body + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { parser: parser, body: body, location: location, comments: comments } + end + + def format(q) + line = nil + + # This handles a special case where you've got a block of statements where + # the only value is a comment. In that case a lot of nodes like + # brace_block will attempt to format as a single line, but since that + # wouldn't work with a comment, we intentionally break the parent group. + if body.length == 2 + void_stmt, comment = body + + if void_stmt.is_a?(VoidStmt) && comment.is_a?(Comment) + q.format(comment) + q.break_parent + return + end + end + + access_controls = + Hash.new do |hash, node| + hash[node] = node.is_a?(VCall) && + %w[private protected public].include?(node.value.value) + end + + body.each_with_index do |statement, index| + next if statement.is_a?(VoidStmt) + + if line.nil? + q.format(statement) + elsif (statement.location.start_line - line) > 1 + q.breakable(force: true) + q.breakable(force: true) + q.format(statement) + elsif access_controls[statement] || access_controls[body[index - 1]] + q.breakable(force: true) + q.breakable(force: true) + q.format(statement) + elsif statement.location.start_line != line + q.breakable(force: true) + q.format(statement) + elsif !q.parent.is_a?(StringEmbExpr) + q.breakable(force: true) + q.format(statement) + else + q.text("; ") + q.format(statement) + end + + line = statement.location.end_line + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("statements") + + q.breakable + q.seplist(body) { |statement| q.pp(statement) } + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :statements, body: body, loc: location, cmts: comments }.to_json( + *opts + ) + end + + private + + # As efficiently as possible, gather up all of the comments that have been + # found while this statements list was being parsed and add them into the + # body. + def attach_comments(start_char, end_char) + parser_comments = parser.comments + + comment_index = 0 + body_index = 0 + + while comment_index < parser_comments.size + comment = parser_comments[comment_index] + location = comment.location + + if !comment.inline? && (start_char <= location.start_char) && + (end_char >= location.end_char) && !comment.ignore? + parser_comments.delete_at(comment_index) + + while (node = body[body_index]) && + ( + node.is_a?(VoidStmt) || + node.location.start_char < location.start_char + ) + body_index += 1 + end + + body.insert(body_index, comment) + else + comment_index += 1 + end + end + end + end + + # StringContent represents the contents of a string-like value. + # + # "string" + # + class StringContent < Node + # [Array[ StringEmbExpr | StringDVar | TStringContent ]] the parts of the + # string + attr_reader :parts + + # [Location] the location of this node + attr_reader :location + + def initialize(parts:, location:) + @parts = parts + @location = location + end + end + + # StringConcat represents concatenating two strings together using a backward + # slash. + # + # "first" \ + # "second" + # + class StringConcat < Node + # [StringConcat | StringLiteral] the left side of the concatenation + attr_reader :left + + # [StringLiteral] the right side of the concatenation + attr_reader :right + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(left:, right:, location:, comments: []) + @left = left + @right = right + @location = location + @comments = comments + end + + def child_nodes + [left, right] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { left: left, right: right, location: location, comments: comments } + end + + def format(q) + q.group do + q.format(left) + q.text(' \\') + q.indent do + q.breakable(force: true) + q.format(right) + end + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("string_concat") + + q.breakable + q.pp(left) + + q.breakable + q.pp(right) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :string_concat, + left: left, + right: right, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # StringDVar represents shorthand interpolation of a variable into a string. + # It allows you to take an instance variable, class variable, or global + # variable and omit the braces when interpolating. + # + # "#@variable" + # + class StringDVar < Node + # [Backref | VarRef] the variable being interpolated + attr_reader :variable + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(variable:, location:, comments: []) + @variable = variable + @location = location + @comments = comments + end + + def child_nodes + [variable] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { variable: variable, location: location, comments: comments } + end + + def format(q) + q.text('#{') + q.format(variable) + q.text("}") + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("string_dvar") + + q.breakable + q.pp(variable) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :string_dvar, + var: variable, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # StringEmbExpr represents interpolated content. It can be contained within a + # couple of different parent nodes, including regular expressions, strings, + # and dynamic symbols. + # + # "string #{expression}" + # + class StringEmbExpr < Node + # [Statements] the expressions to be interpolated + attr_reader :statements + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(statements:, location:, comments: []) + @statements = statements + @location = location + @comments = comments + end + + def child_nodes + [statements] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { statements: statements, location: location, comments: comments } + end + + def format(q) + if location.start_line == location.end_line + # If the contents of this embedded expression were originally on the + # same line in the source, then we're going to leave them in place and + # assume that's the way the developer wanted this expression + # represented. + doc = q.group(0, '#{', "}") { q.format(statements) } + RemoveBreaks.call(doc) + else + q.group do + q.text('#{') + q.indent do + q.breakable("") + q.format(statements) + end + q.breakable("") + q.text("}") + end + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("string_embexpr") + + q.breakable + q.pp(statements) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :string_embexpr, + stmts: statements, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # StringLiteral represents a string literal. + # + # "string" + # + class StringLiteral < Node + # [Array[ StringEmbExpr | StringDVar | TStringContent ]] the parts of the + # string literal + attr_reader :parts + + # [String] which quote was used by the string literal + attr_reader :quote + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(parts:, quote:, location:, comments: []) + @parts = parts + @quote = quote + @location = location + @comments = comments + end + + def child_nodes + parts + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { parts: parts, quote: quote, location: location, comments: comments } + end + + def format(q) + if parts.empty? + q.text("#{q.quote}#{q.quote}") + return + end + + opening_quote, closing_quote = + if !Quotes.locked?(self) + [q.quote, q.quote] + elsif quote.start_with?("%") + [quote, Quotes.matching(quote[/%[qQ]?(.)/, 1])] + else + [quote, quote] + end + + q.group(0, opening_quote, closing_quote) do + parts.each do |part| + if part.is_a?(TStringContent) + value = Quotes.normalize(part.value, closing_quote) + separator = -> { q.breakable(force: true, indent: false) } + q.seplist(value.split(/\r?\n/, -1), separator) do |text| + q.text(text) + end + else + q.format(part) + end + end + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("string_literal") + + q.breakable + q.group(2, "(", ")") { q.seplist(parts) { |part| q.pp(part) } } + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :string_literal, + parts: parts, + quote: quote, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # Super represents using the +super+ keyword with arguments. It can optionally + # use parentheses. + # + # super(value) + # + class Super < Node + # [ArgParen | Args] the arguments to the keyword + attr_reader :arguments + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(arguments:, location:, comments: []) + @arguments = arguments + @location = location + @comments = comments + end + + def child_nodes + [arguments] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { arguments: arguments, location: location, comments: comments } + end + + def format(q) + q.group do + q.text("super") + + if arguments.is_a?(ArgParen) + q.format(arguments) + else + q.text(" ") + q.nest("super ".length) { q.format(arguments) } + end + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("super") + + q.breakable + q.pp(arguments) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :super, args: arguments, loc: location, cmts: comments }.to_json( + *opts + ) + end + end + + # SymBeg represents the beginning of a symbol literal. + # + # :symbol + # + # SymBeg is also used for dynamic symbols, as in: + # + # :"symbol" + # + # Finally, SymBeg is also used for symbols using the %s syntax, as in: + # + # %s[symbol] + # + # The value of this node is a string. In most cases (as in the first example + # above) it will contain just ":". In the case of dynamic symbols it will + # contain ":'" or ":\"". In the case of %s symbols, it will contain the start + # of the symbol including the %s and the delimiter. + class SymBeg < Node + # [String] the beginning of the symbol + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + def initialize(value:, location:) + @value = value + @location = location + end + end + + # SymbolContent represents symbol contents and is always the child of a + # SymbolLiteral node. + # + # :symbol + # + class SymbolContent < Node + # [Backtick | Const | CVar | GVar | Ident | IVar | Kw | Op] the value of the + # symbol + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + def initialize(value:, location:) + @value = value + @location = location + end + end + + # SymbolLiteral represents a symbol in the system with no interpolation + # (as opposed to a DynaSymbol which has interpolation). + # + # :symbol + # + class SymbolLiteral < Node + # [Backtick | Const | CVar | GVar | Ident | IVar | Kw | Op] the value of the + # symbol + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) + @value = value + @location = location + @comments = comments + end + + def child_nodes + [value] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + + def format(q) + q.text(":") + q.format(value) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("symbol_literal") + + q.breakable + q.pp(value) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :symbol_literal, + value: value, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # Symbols represents a symbol array literal with interpolation. + # + # %I[one two three] + # + class Symbols < Node + # [SymbolsBeg] the token that opens this array literal + attr_reader :beginning + + # [Array[ Word ]] the words in the symbol array literal + attr_reader :elements + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(beginning:, elements:, location:, comments: []) + @beginning = beginning + @elements = elements + @location = location + @comments = comments + end + + def child_nodes + [] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + beginning: beginning, + elements: elements, + location: location, + comments: comments + } + end + + def format(q) + opening, closing = "%I[", "]" + + if elements.any? { |element| element.match?(/[\[\]]/) } + opening = beginning.value + closing = Quotes.matching(opening[2]) + end + + q.group(0, opening, closing) do + q.indent do + q.breakable("") + q.seplist(elements, -> { q.breakable }) do |element| + q.format(element) + end + end + q.breakable("") + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("symbols") + + q.breakable + q.group(2, "(", ")") { q.seplist(elements) { |element| q.pp(element) } } + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :symbols, + elems: elements, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # SymbolsBeg represents the start of a symbol array literal with + # interpolation. + # + # %I[one two three] + # + # In the snippet above, SymbolsBeg represents the "%I[" token. Note that these + # kinds of arrays can start with a lot of different delimiter types + # (e.g., %I| or %I<). + class SymbolsBeg < Node + # [String] the beginning of the symbol literal array + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + def initialize(value:, location:) + @value = value + @location = location + end + end + + # TLambda represents the beginning of a lambda literal. + # + # -> { value } + # + # In the example above the TLambda represents the +->+ operator. + class TLambda < Node + # [String] the beginning of the lambda literal + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + def initialize(value:, location:) + @value = value + @location = location + end + end + + # TLamBeg represents the beginning of the body of a lambda literal using + # braces. + # + # -> { value } + # + # In the example above the TLamBeg represents the +{+ operator. + class TLamBeg < Node + # [String] the beginning of the body of the lambda literal + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + def initialize(value:, location:) + @value = value + @location = location + end + end + + # TopConstField is always the child node of some kind of assignment. It + # represents when you're assigning to a constant that is being referenced at + # the top level. + # + # ::Constant = value + # + class TopConstField < Node + # [Const] the constant being assigned + attr_reader :constant + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(constant:, location:, comments: []) + @constant = constant + @location = location + @comments = comments + end + + def child_nodes + [constant] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { constant: constant, location: location, comments: comments } + end + + def format(q) + q.text("::") + q.format(constant) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("top_const_field") + + q.breakable + q.pp(constant) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :top_const_field, + constant: constant, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # TopConstRef is very similar to TopConstField except that it is not involved + # in an assignment. + # + # ::Constant + # + class TopConstRef < Node + # [Const] the constant being referenced + attr_reader :constant + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(constant:, location:, comments: []) + @constant = constant + @location = location + @comments = comments + end + + def child_nodes + [constant] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { constant: constant, location: location, comments: comments } + end + + def format(q) + q.text("::") + q.format(constant) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("top_const_ref") + + q.breakable + q.pp(constant) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :top_const_ref, + constant: constant, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # TStringBeg represents the beginning of a string literal. + # + # "string" + # + # In the example above, TStringBeg represents the first set of quotes. Strings + # can also use single quotes. They can also be declared using the +%q+ and + # +%Q+ syntax, as in: + # + # %q{string} + # + class TStringBeg < Node + # [String] the beginning of the string + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + def initialize(value:, location:) + @value = value + @location = location + end + end + + # TStringContent represents plain characters inside of an entity that accepts + # string content like a string, heredoc, command string, or regular + # expression. + # + # "string" + # + # In the example above, TStringContent represents the +string+ token contained + # within the string. + class TStringContent < Node + # [String] the content of the string + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) + @value = value + @location = location + @comments = comments + end + + def match?(pattern) + value.match?(pattern) + end + + def child_nodes + [] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + + def format(q) + q.text(value) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("tstring_content") + + q.breakable + q.pp(value) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :tstring_content, + value: value.force_encoding("UTF-8"), + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # TStringEnd represents the end of a string literal. + # + # "string" + # + # In the example above, TStringEnd represents the second set of quotes. + # Strings can also use single quotes. They can also be declared using the +%q+ + # and +%Q+ syntax, as in: + # + # %q{string} + # + class TStringEnd < Node + # [String] the end of the string + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + def initialize(value:, location:) + @value = value + @location = location + end + end + + # Not represents the unary +not+ method being called on an expression. + # + # not value + # + class Not < Node + # [untyped] the statement on which to operate + attr_reader :statement + + # [boolean] whether or not parentheses were used + attr_reader :parentheses + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(statement:, parentheses:, location:, comments: []) + @statement = statement + @parentheses = parentheses + @location = location + @comments = comments + end + + def child_nodes + [statement] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + statement: statement, + parentheses: parentheses, + location: location, + comments: comments + } + end + + def format(q) + q.text(parentheses ? "not(" : "not ") + q.format(statement) + q.text(")") if parentheses + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("not") + + q.breakable + q.pp(statement) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :not, + value: statement, + paren: parentheses, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # Unary represents a unary method being called on an expression, as in +!+ or + # +~+. + # + # !value + # + class Unary < Node + # [String] the operator being used + attr_reader :operator + + # [untyped] the statement on which to operate + attr_reader :statement + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(operator:, statement:, location:, comments: []) + @operator = operator + @statement = statement + @location = location + @comments = comments + end + + def child_nodes + [statement] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + operator: operator, + statement: statement, + location: location, + comments: comments + } + end + + def format(q) + q.text(operator) + q.format(statement) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("unary") + + q.breakable + q.pp(operator) + + q.breakable + q.pp(statement) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :unary, + op: operator, + value: statement, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # Undef represents the use of the +undef+ keyword. + # + # undef method + # + class Undef < Node + class UndefArgumentFormatter + # [DynaSymbol | SymbolLiteral] the symbol to undefine + attr_reader :node + + def initialize(node) + @node = node + end + + def comments + if node.is_a?(SymbolLiteral) + node.comments + node.value.comments + else + node.comments + end + end + + def format(q) + node.is_a?(SymbolLiteral) ? q.format(node.value) : q.format(node) + end + end + + # [Array[ DynaSymbol | SymbolLiteral ]] the symbols to undefine + attr_reader :symbols + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(symbols:, location:, comments: []) + @symbols = symbols + @location = location + @comments = comments + end + + def child_nodes + symbols + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { symbols: symbols, location: location, comments: comments } + end + + def format(q) + keyword = "undef " + formatters = symbols.map { |symbol| UndefArgumentFormatter.new(symbol) } + + q.group do + q.text(keyword) + q.nest(keyword.length) do + q.seplist(formatters) { |formatter| q.format(formatter) } + end + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("undef") + + q.breakable + q.group(2, "(", ")") { q.seplist(symbols) { |symbol| q.pp(symbol) } } + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :undef, syms: symbols, loc: location, cmts: comments }.to_json( + *opts + ) + end + end + + # Unless represents the first clause in an +unless+ chain. + # + # unless predicate + # end + # + class Unless < Node + # [untyped] the expression to be checked + attr_reader :predicate + + # [Statements] the expressions to be executed + attr_reader :statements + + # [nil, Elsif, Else] the next clause in the chain + attr_reader :consequent + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize( + predicate:, + statements:, + consequent:, + location:, + comments: [] + ) + @predicate = predicate + @statements = statements + @consequent = consequent + @location = location + @comments = comments + end + + def child_nodes + [predicate, statements, consequent] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + predicate: predicate, + statements: statements, + consequent: consequent, + location: location, + comments: comments + } + end + + def format(q) + ConditionalFormatter.new("unless", self).format(q) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("unless") + + q.breakable + q.pp(predicate) + + q.breakable + q.pp(statements) + + if consequent + q.breakable + q.pp(consequent) + end + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :unless, + pred: predicate, + stmts: statements, + cons: consequent, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # UnlessMod represents the modifier form of an +unless+ statement. + # + # expression unless predicate + # + class UnlessMod < Node + # [untyped] the expression to be executed + attr_reader :statement + + # [untyped] the expression to be checked + attr_reader :predicate + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(statement:, predicate:, location:, comments: []) + @statement = statement + @predicate = predicate + @location = location + @comments = comments + end + + def child_nodes + [statement, predicate] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + statement: statement, + predicate: predicate, + location: location, + comments: comments + } + end + + def format(q) + ConditionalModFormatter.new("unless", self).format(q) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("unless_mod") + + q.breakable + q.pp(statement) + + q.breakable + q.pp(predicate) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :unless_mod, + stmt: statement, + pred: predicate, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # Formats an Until, UntilMod, While, or WhileMod node. + class LoopFormatter + # [String] the name of the keyword used for this loop + attr_reader :keyword + + # [Until | UntilMod | While | WhileMod] the node that is being formatted + attr_reader :node + + # [untyped] the statements associated with the node + attr_reader :statements + + def initialize(keyword, node, statements) + @keyword = keyword + @node = node + @statements = statements + end + + def format(q) + if ContainsAssignment.call(node.predicate) + format_break(q) + q.break_parent + return + end + + q.group do + q.if_break { format_break(q) }.if_flat do + Parentheses.flat(q) do + q.format(statements) + q.text(" #{keyword} ") + q.format(node.predicate) + end + end + end + end + + private + + def format_break(q) + q.text("#{keyword} ") + q.nest(keyword.length + 1) { q.format(node.predicate) } + q.indent do + q.breakable("") + q.format(statements) + end + q.breakable("") + q.text("end") + end + end + + # Until represents an +until+ loop. + # + # until predicate + # end + # + class Until < Node + # [untyped] the expression to be checked + attr_reader :predicate + + # [Statements] the expressions to be executed + attr_reader :statements + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(predicate:, statements:, location:, comments: []) + @predicate = predicate + @statements = statements + @location = location + @comments = comments + end + + def child_nodes + [predicate, statements] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + predicate: predicate, + statements: statements, + location: location, + comments: comments + } + end + + def format(q) + if statements.empty? + keyword = "until " + + q.group do + q.text(keyword) + q.nest(keyword.length) { q.format(predicate) } + q.breakable(force: true) + q.text("end") + end + else + LoopFormatter.new("until", self, statements).format(q) + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("until") + + q.breakable + q.pp(predicate) + + q.breakable + q.pp(statements) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :until, + pred: predicate, + stmts: statements, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # UntilMod represents the modifier form of a +until+ loop. + # + # expression until predicate + # + class UntilMod < Node + # [untyped] the expression to be executed + attr_reader :statement + + # [untyped] the expression to be checked + attr_reader :predicate + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(statement:, predicate:, location:, comments: []) + @statement = statement + @predicate = predicate + @location = location + @comments = comments + end + + def child_nodes + [statement, predicate] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + statement: statement, + predicate: predicate, + location: location, + comments: comments + } + end + + def format(q) + # If we're in the modifier form and we're modifying a `begin`, then this + # is a special case where we need to explicitly use the modifier form + # because otherwise the semantic meaning changes. This looks like: + # + # begin + # foo + # end until bar + # + # Also, if the statement of the modifier includes an assignment, then we + # can't know for certain that it won't impact the predicate, so we need to + # force it to stay as it is. This looks like: + # + # foo = bar until foo + # + if statement.is_a?(Begin) || ContainsAssignment.call(statement) + q.format(statement) + q.text(" until ") + q.format(predicate) + else + LoopFormatter.new("until", self, statement).format(q) + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("until_mod") + + q.breakable + q.pp(statement) + + q.breakable + q.pp(predicate) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :until_mod, + stmt: statement, + pred: predicate, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # VarAlias represents when you're using the +alias+ keyword with global + # variable arguments. + # + # alias $new $old + # + class VarAlias < Node + # [GVar] the new alias of the variable + attr_reader :left + + # [Backref | GVar] the current name of the variable to be aliased + attr_reader :right + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(left:, right:, location:, comments: []) + @left = left + @right = right + @location = location + @comments = comments + end + + def child_nodes + [left, right] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { left: left, right: right, location: location, comments: comments } + end + + def format(q) + keyword = "alias " + + q.text(keyword) + q.format(left) + q.text(" ") + q.format(right) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("var_alias") + + q.breakable + q.pp(left) + + q.breakable + q.pp(right) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :var_alias, + left: left, + right: right, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # VarField represents a variable that is being assigned a value. As such, it + # is always a child of an assignment type node. + # + # variable = value + # + # In the example above, the VarField node represents the +variable+ token. + class VarField < Node + # [nil | Const | CVar | GVar | Ident | IVar] the target of this node + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) + @value = value + @location = location + @comments = comments + end + + def child_nodes + [value] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + + def format(q) + q.format(value) if value + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("var_field") + + q.breakable + q.pp(value) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :var_field, value: value, loc: location, cmts: comments }.to_json( + *opts + ) + end + end + + # VarRef represents a variable reference. + # + # true + # + # This can be a plain local variable like the example above. It can also be a + # constant, a class variable, a global variable, an instance variable, a + # keyword (like +self+, +nil+, +true+, or +false+), or a numbered block + # variable. + class VarRef < Node + # [Const | CVar | GVar | Ident | IVar | Kw] the value of this node + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) + @value = value + @location = location + @comments = comments + end + + def child_nodes + [value] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + + def format(q) + q.format(value) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("var_ref") + + q.breakable + q.pp(value) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :var_ref, value: value, loc: location, cmts: comments }.to_json( + *opts + ) + end + end + + # PinnedVarRef represents a pinned variable reference within a pattern + # matching pattern. + # + # case value + # in ^variable + # end + # + # This can be a plain local variable like the example above. It can also be a + # a class variable, a global variable, or an instance variable. + class PinnedVarRef < Node + # [VarRef] the value of this node + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) + @value = value + @location = location + @comments = comments + end + + def child_nodes + [value] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + + def format(q) + q.group do + q.text("^") + q.format(value) + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("pinned_var_ref") + + q.breakable + q.pp(value) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :pinned_var_ref, + value: value, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # VCall represent any plain named object with Ruby that could be either a + # local variable or a method call. + # + # variable + # + class VCall < Node + # [Ident] the value of this expression + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) + @value = value + @location = location + @comments = comments + end + + def child_nodes + [value] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + + def format(q) + q.format(value) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("vcall") + + q.breakable + q.pp(value) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :vcall, value: value, loc: location, cmts: comments }.to_json( + *opts + ) + end + end + + # VoidStmt represents an empty lexical block of code. + # + # ;; + # + class VoidStmt < Node + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(location:, comments: []) + @location = location + @comments = comments + end + + def child_nodes + [] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { location: location, comments: comments } + end + + def format(q) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("void_stmt") + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :void_stmt, loc: location, cmts: comments }.to_json(*opts) + end + end + + # When represents a +when+ clause in a +case+ chain. + # + # case value + # when predicate + # end + # + class When < Node + # [Args] the arguments to the when clause + attr_reader :arguments + + # [Statements] the expressions to be executed + attr_reader :statements + + # [nil | Else | When] the next clause in the chain + attr_reader :consequent + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize( + arguments:, + statements:, + consequent:, + location:, + comments: [] + ) + @arguments = arguments + @statements = statements + @consequent = consequent + @location = location + @comments = comments + end + + def child_nodes + [arguments, statements, consequent] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + arguments: arguments, + statements: statements, + consequent: consequent, + location: location, + comments: comments + } + end + + def format(q) + keyword = "when " + + q.group do + q.group do + q.text(keyword) + q.nest(keyword.length) do + if arguments.comments.any? + q.format(arguments) + else + separator = -> { q.group { q.comma_breakable } } + q.seplist(arguments.parts, separator) { |part| q.format(part) } + end + + # Very special case here. If you're inside of a when clause and the + # last argument to the predicate is and endless range, then you are + # forced to use the "then" keyword to make it parse properly. + last = arguments.parts.last + if (last.is_a?(Dot2) || last.is_a?(Dot3)) && !last.right + q.text(" then") + end + end + end + + unless statements.empty? + q.indent do + q.breakable(force: true) + q.format(statements) + end + end + + if consequent + q.breakable(force: true) + q.format(consequent) + end + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("when") + + q.breakable + q.pp(arguments) + + q.breakable + q.pp(statements) + + if consequent + q.breakable + q.pp(consequent) + end + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :when, + args: arguments, + stmts: statements, + cons: consequent, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # While represents a +while+ loop. + # + # while predicate + # end + # + class While < Node + # [untyped] the expression to be checked + attr_reader :predicate + + # [Statements] the expressions to be executed + attr_reader :statements + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(predicate:, statements:, location:, comments: []) + @predicate = predicate + @statements = statements + @location = location + @comments = comments + end + + def child_nodes + [predicate, statements] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + predicate: predicate, + statements: statements, + location: location, + comments: comments + } + end + + def format(q) + if statements.empty? + keyword = "while " + + q.group do + q.text(keyword) + q.nest(keyword.length) { q.format(predicate) } + q.breakable(force: true) + q.text("end") + end + else + LoopFormatter.new("while", self, statements).format(q) + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("while") + + q.breakable + q.pp(predicate) + + q.breakable + q.pp(statements) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :while, + pred: predicate, + stmts: statements, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # WhileMod represents the modifier form of a +while+ loop. + # + # expression while predicate + # + class WhileMod < Node + # [untyped] the expression to be executed + attr_reader :statement + + # [untyped] the expression to be checked + attr_reader :predicate + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(statement:, predicate:, location:, comments: []) + @statement = statement + @predicate = predicate + @location = location + @comments = comments + end + + def child_nodes + [statement, predicate] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + statement: statement, + predicate: predicate, + location: location, + comments: comments + } + end + + def format(q) + # If we're in the modifier form and we're modifying a `begin`, then this + # is a special case where we need to explicitly use the modifier form + # because otherwise the semantic meaning changes. This looks like: + # + # begin + # foo + # end while bar + # + # Also, if the statement of the modifier includes an assignment, then we + # can't know for certain that it won't impact the predicate, so we need to + # force it to stay as it is. This looks like: + # + # foo = bar while foo + # + if statement.is_a?(Begin) || ContainsAssignment.call(statement) + q.format(statement) + q.text(" while ") + q.format(predicate) + else + LoopFormatter.new("while", self, statement).format(q) + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("while_mod") + + q.breakable + q.pp(statement) + + q.breakable + q.pp(predicate) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :while_mod, + stmt: statement, + pred: predicate, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # Word represents an element within a special array literal that accepts + # interpolation. + # + # %W[a#{b}c xyz] + # + # In the example above, there would be two Word nodes within a parent Words + # node. + class Word < Node + # [Array[ StringEmbExpr | StringDVar | TStringContent ]] the parts of the + # word + attr_reader :parts + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(parts:, location:, comments: []) + @parts = parts + @location = location + @comments = comments + end + + def match?(pattern) + parts.any? { |part| part.is_a?(TStringContent) && part.match?(pattern) } + end + + def child_nodes + parts + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { parts: parts, location: location, comments: comments } + end + + def format(q) + q.format_each(parts) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("word") + + q.breakable + q.group(2, "(", ")") { q.seplist(parts) { |part| q.pp(part) } } + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :word, parts: parts, loc: location, cmts: comments }.to_json( + *opts + ) + end + end + + # Words represents a string literal array with interpolation. + # + # %W[one two three] + # + class Words < Node + # [WordsBeg] the token that opens this array literal + attr_reader :beginning + + # [Array[ Word ]] the elements of this array + attr_reader :elements + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(beginning:, elements:, location:, comments: []) + @beginning = beginning + @elements = elements + @location = location + @comments = comments + end + + def child_nodes + [] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { + beginning: beginning, + elements: elements, + location: location, + comments: comments + } + end + + def format(q) + opening, closing = "%W[", "]" + + if elements.any? { |element| element.match?(/[\[\]]/) } + opening = beginning.value + closing = Quotes.matching(opening[2]) + end + + q.group(0, opening, closing) do + q.indent do + q.breakable("") + q.seplist(elements, -> { q.breakable }) do |element| + q.format(element) + end + end + q.breakable("") + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("words") + + q.breakable + q.group(2, "(", ")") { q.seplist(elements) { |element| q.pp(element) } } + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :words, elems: elements, loc: location, cmts: comments }.to_json( + *opts + ) + end + end + + # WordsBeg represents the beginning of a string literal array with + # interpolation. + # + # %W[one two three] + # + # In the snippet above, a WordsBeg would be created with the value of "%W[". + # Note that these kinds of arrays can start with a lot of different delimiter + # types (e.g., %W| or %W<). + class WordsBeg < Node + # [String] the start of the word literal array + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + def initialize(value:, location:) + @value = value + @location = location + end + end + + # XString represents the contents of an XStringLiteral. + # + # `ls` + # + class XString < Node + # [Array[ StringEmbExpr | StringDVar | TStringContent ]] the parts of the + # xstring + attr_reader :parts + + # [Location] the location of this node + attr_reader :location + + def initialize(parts:, location:) + @parts = parts + @location = location + end + end + + # XStringLiteral represents a string that gets executed. + # + # `ls` + # + class XStringLiteral < Node + # [Array[ StringEmbExpr | StringDVar | TStringContent ]] the parts of the + # xstring + attr_reader :parts + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(parts:, location:, comments: []) + @parts = parts + @location = location + @comments = comments + end + + def child_nodes + parts + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { parts: parts, location: location, comments: comments } + end + + def format(q) + q.text("`") + q.format_each(parts) + q.text("`") + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("xstring_literal") + + q.breakable + q.group(2, "(", ")") { q.seplist(parts) { |part| q.pp(part) } } + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { + type: :xstring_literal, + parts: parts, + loc: location, + cmts: comments + }.to_json(*opts) + end + end + + # Yield represents using the +yield+ keyword with arguments. + # + # yield value + # + class Yield < Node + # [Args | Paren] the arguments passed to the yield + attr_reader :arguments + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(arguments:, location:, comments: []) + @arguments = arguments + @location = location + @comments = comments + end + + def child_nodes + [arguments] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { arguments: arguments, location: location, comments: comments } + end + + def format(q) + q.group do + q.text("yield") + + if arguments.is_a?(Paren) + q.format(arguments) + else + q.if_break { q.text("(") }.if_flat { q.text(" ") } + q.indent do + q.breakable("") + q.format(arguments) + end + q.breakable("") + q.if_break { q.text(")") } + end + end + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("yield") + + q.breakable + q.pp(arguments) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :yield, args: arguments, loc: location, cmts: comments }.to_json( + *opts + ) + end + end + + # Yield0 represents the bare +yield+ keyword with no arguments. + # + # yield + # + class Yield0 < Node + # [String] the value of the keyword + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) + @value = value + @location = location + @comments = comments + end + + def child_nodes + [] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + + def format(q) + q.text(value) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("yield0") + + q.breakable + q.pp(value) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :yield0, value: value, loc: location, cmts: comments }.to_json( + *opts + ) + end + end + + # ZSuper represents the bare +super+ keyword with no arguments. + # + # super + # + class ZSuper < Node + # [String] the value of the keyword + attr_reader :value + + # [Location] the location of this node + attr_reader :location + + # [Array[ Comment | EmbDoc ]] the comments attached to this node + attr_reader :comments + + def initialize(value:, location:, comments: []) + @value = value + @location = location + @comments = comments + end + + def child_nodes + [] + end + + alias deconstruct child_nodes + + def deconstruct_keys(keys) + { value: value, location: location, comments: comments } + end + + def format(q) + q.text(value) + end + + def pretty_print(q) + q.group(2, "(", ")") do + q.text("zsuper") + + q.breakable + q.pp(value) + + q.pp(Comment::List.new(comments)) + end + end + + def to_json(*opts) + { type: :zsuper, value: value, loc: location, cmts: comments }.to_json( + *opts + ) + end + end +end From 2ea7b5ffa5cdff0470f6990b3c6a8eb0298a5a67 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Wed, 30 Mar 2022 22:39:47 -0400 Subject: [PATCH 20/25] Split up files somewhat --- bin/bench | 2 +- lib/syntax_tree.rb | 3144 +--------------------------- lib/syntax_tree/cli.rb | 2 +- lib/syntax_tree/code_actions.rb | 2 +- lib/syntax_tree/implicits.rb | 2 +- lib/syntax_tree/language_server.rb | 2 +- lib/syntax_tree/node.rb | 50 +- lib/syntax_tree/parser.rb | 3098 +++++++++++++++++++++++++++ lib/syntax_tree/version.rb | 4 +- test/behavior_test.rb | 4 +- test/formatting_test.rb | 2 +- test/idempotency_test.rb | 2 +- test/node_test.rb | 6 +- 13 files changed, 3163 insertions(+), 3157 deletions(-) create mode 100644 lib/syntax_tree/parser.rb diff --git a/bin/bench b/bin/bench index 76270c87..f918daea 100755 --- a/bin/bench +++ b/bin/bench @@ -15,7 +15,7 @@ def compare(filepath) source = File.read(filepath) Benchmark.ips do |x| - x.report("syntax_tree") { SyntaxTree.new(source).parse } + x.report("syntax_tree") { SyntaxTree.parse(source) } x.report("parser") { Parser::CurrentRuby.parse(source) } x.report("ruby_parser") { RubyParser.new.parse(source) } x.compare! diff --git a/lib/syntax_tree.rb b/lib/syntax_tree.rb index b0236031..cdb9bb68 100644 --- a/lib/syntax_tree.rb +++ b/lib/syntax_tree.rb @@ -6,6 +6,7 @@ require "stringio" require_relative "syntax_tree/node" +require_relative "syntax_tree/parser" require_relative "syntax_tree/version" # If PrettyPrint::Align isn't defined, then we haven't gotten the updated @@ -22,106 +23,7 @@ end end -class SyntaxTree < Ripper - # Represents a line in the source. If this class is being used, it means that - # every character in the string is 1 byte in length, so we can just return the - # start of the line + the index. - class SingleByteString - attr_reader :start - - def initialize(start) - @start = start - end - - def [](byteindex) - start + byteindex - end - end - - # Represents a line in the source. If this class is being used, it means that - # there are characters in the string that are multi-byte, so we will build up - # an array of indices, such that array[byteindex] will be equal to the index - # of the character within the string. - class MultiByteString - attr_reader :start, :indices - - def initialize(start, line) - @start = start - @indices = [] - - line.each_char.with_index(start) do |char, index| - char.bytesize.times { @indices << index } - end - end - - # Technically it's possible for the column index to be a negative value if - # there's a BOM at the beginning of the file, which is the reason we need to - # compare it to 0 here. - def [](byteindex) - indices[byteindex < 0 ? 0 : byteindex] - end - end - - # Represents the location of a node in the tree from the source code. - class Location - attr_reader :start_line, :start_char, :end_line, :end_char - - def initialize(start_line:, start_char:, end_line:, end_char:) - @start_line = start_line - @start_char = start_char - @end_line = end_line - @end_char = end_char - end - - def lines - start_line..end_line - end - - def ==(other) - other.is_a?(Location) && start_line == other.start_line && - start_char == other.start_char && end_line == other.end_line && - end_char == other.end_char - end - - def to(other) - Location.new( - start_line: start_line, - start_char: start_char, - end_line: [end_line, other.end_line].max, - end_char: other.end_char - ) - end - - def to_json(*opts) - [start_line, start_char, end_line, end_char].to_json(*opts) - end - - def self.token(line:, char:, size:) - new( - start_line: line, - start_char: char, - end_line: line, - end_char: char + size - ) - end - - def self.fixed(line:, char:) - new(start_line: line, start_char: char, end_line: line, end_char: char) - end - end - - # A special parser error so that we can get nice syntax displays on the error - # message when prettier prints out the results. - class ParseError < StandardError - attr_reader :lineno, :column - - def initialize(error, lineno, column) - super(error) - @lineno = lineno - @column = column - end - end - +module SyntaxTree # A slightly enhanced PP that knows how to format recursively including # comments. class Formatter < PP @@ -190,101 +92,8 @@ def parents end end - # [String] the source being parsed - attr_reader :source - - # [Array[ String ]] the list of lines in the source - attr_reader :lines - - # [Array[ SingleByteString | MultiByteString ]] the list of objects that - # represent the start of each line in character offsets - attr_reader :line_counts - - # [Array[ untyped ]] a running list of tokens that have been found in the - # source. This list changes a lot as certain nodes will "consume" these tokens - # to determine their bounds. - attr_reader :tokens - - # [Array[ Comment | EmbDoc ]] the list of comments that have been found while - # parsing the source. - attr_reader :comments - - def initialize(source, *) - super - - # We keep the source around so that we can refer back to it when we're - # generating the AST. Sometimes it's easier to just reference the source - # string when you want to check if it contains a certain character, for - # example. - @source = source - - # Similarly, we keep the lines of the source string around to be able to - # check if certain lines contain certain characters. For example, we'll use - # this to generate the content that goes after the __END__ keyword. Or we'll - # use this to check if a comment has other content on its line. - @lines = source.split(/\r?\n/) - - # This is the full set of comments that have been found by the parser. It's - # a running list. At the end of every block of statements, they will go in - # and attempt to grab any comments that are on their own line and turn them - # into regular statements. So at the end of parsing the only comments left - # in here will be comments on lines that also contain code. - @comments = [] - - # This is the current embdoc (comments that start with =begin and end with - # =end). Since they can't be nested, there's no need for a stack here, as - # there can only be one active. These end up getting dumped into the - # comments list before getting picked up by the statements that surround - # them. - @embdoc = nil - - # This is an optional node that can be present if the __END__ keyword is - # used in the file. In that case, this will represent the content after that - # keyword. - @__end__ = nil - - # Heredocs can actually be nested together if you're using interpolation, so - # this is a stack of heredoc nodes that are currently being created. When we - # get to the token that finishes off a heredoc node, we pop the top - # one off. If there are others surrounding it, then the body events will now - # be added to the correct nodes. - @heredocs = [] - - # This is a running list of tokens that have fired. It's useful - # mostly for maintaining location information. For example, if you're inside - # the handle of a def event, then in order to determine where the AST node - # started, you need to look backward in the tokens to find a def - # keyword. Most of the time, when a parser event consumes one of these - # events, it will be deleted from the list. So ideally, this list stays - # pretty short over the course of parsing a source string. - @tokens = [] - - # Here we're going to build up a list of SingleByteString or MultiByteString - # objects. They're each going to represent a string in the source. They are - # used by the `char_pos` method to determine where we are in the source - # string. - @line_counts = [] - last_index = 0 - - @source.lines.each do |line| - if line.size == line.bytesize - @line_counts << SingleByteString.new(last_index) - else - @line_counts << MultiByteString.new(last_index, line) - end - - last_index += line.size - end - - # Make sure line counts is filled out with the first and last line at - # minimum so that it has something to compare against if the parser is in a - # lineno=2 state for an empty file. - @line_counts << SingleByteString.new(0) if @line_counts.empty? - @line_counts << SingleByteString.new(last_index) - end - def self.parse(source) - parser = new(source) + parser = Parser.new(source) response = parser.parse response unless parser.error? end @@ -311,2951 +120,4 @@ def self.read(filepath) File.read(filepath, encoding: encoding) end - - private - - # ---------------------------------------------------------------------------- - # :section: Helper methods - # The following methods are used by the ripper event handlers to either - # determine their bounds or query other nodes. - # ---------------------------------------------------------------------------- - - # This represents the current place in the source string that we've gotten to - # so far. We have a memoized line_counts object that we can use to get the - # number of characters that we've had to go through to get to the beginning of - # this line, then we add the number of columns into this line that we've gone - # through. - def char_pos - line_counts[lineno - 1][column] - end - - # As we build up a list of tokens, we'll periodically need to go backwards and - # find the ones that we've already hit in order to determine the location - # information for nodes that use them. For example, if you have a module node - # then you'll look backward for a kw token to determine your start location. - # - # This works with nesting since we're deleting tokens from the list once - # they've been used up. For example if you had nested module declarations then - # the innermost declaration would grab the last kw node that matches "module" - # (which would happen to be the innermost keyword). Then the outer one would - # only be able to grab the first one. In this way all of the tokens act as - # their own stack. - def find_token(type, value = :any, consume: true, location: nil) - index = - tokens.rindex do |token| - token.is_a?(type) && (value == :any || (token.value == value)) - end - - if consume - # If we're expecting to be able to find a token and consume it, - # but can't actually find it, then we need to raise an error. This is - # _usually_ caused by a syntax error in the source that we're printing. It - # could also be caused by accidentally attempting to consume a token twice - # by two different parser event handlers. - unless index - token = value == :any ? type.name.split("::", 2).last : value - message = "Cannot find expected #{token}" - - if location - lineno = location.start_line - column = location.start_char - line_counts[lineno - 1].start - raise ParseError.new(message, lineno, column) - else - raise ParseError.new(message, lineno, column) - end - end - - tokens.delete_at(index) - elsif index - tokens[index] - end - end - - # A helper function to find a :: operator. We do special handling instead of - # using find_token here because we don't pop off all of the :: - # operators so you could end up getting the wrong information if you have for - # instance ::X::Y::Z. - def find_colon2_before(const) - index = - tokens.rindex do |token| - token.is_a?(Op) && token.value == "::" && - token.location.start_char < const.location.start_char - end - - tokens[index] - end - - # Finds the next position in the source string that begins a statement. This - # is used to bind statements lists and make sure they don't include a - # preceding comment. For example, we want the following comment to be attached - # to the class node and not the statement node: - # - # class Foo # :nodoc: - # ... - # end - # - # By finding the next non-space character, we can make sure that the bounds of - # the statement list are correct. - def find_next_statement_start(position) - remaining = source[position..-1] - - if remaining.sub(/\A +/, "")[0] == "#" - return position + remaining.index("\n") - end - - position - end - - # ---------------------------------------------------------------------------- - # :section: Ripper event handlers - # The following methods all handle a dispatched ripper event. - # ---------------------------------------------------------------------------- - - # :call-seq: - # on_BEGIN: (Statements statements) -> BEGINBlock - def on_BEGIN(statements) - lbrace = find_token(LBrace) - rbrace = find_token(RBrace) - - statements.bind( - find_next_statement_start(lbrace.location.end_char), - rbrace.location.start_char - ) - - keyword = find_token(Kw, "BEGIN") - - BEGINBlock.new( - lbrace: lbrace, - statements: statements, - location: keyword.location.to(rbrace.location) - ) - end - - # :call-seq: - # on_CHAR: (String value) -> CHAR - def on_CHAR(value) - CHAR.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - end - - # :call-seq: - # on_END: (Statements statements) -> ENDBlock - def on_END(statements) - lbrace = find_token(LBrace) - rbrace = find_token(RBrace) - - statements.bind( - find_next_statement_start(lbrace.location.end_char), - rbrace.location.start_char - ) - - keyword = find_token(Kw, "END") - - ENDBlock.new( - lbrace: lbrace, - statements: statements, - location: keyword.location.to(rbrace.location) - ) - end - - # :call-seq: - # on___end__: (String value) -> EndContent - def on___end__(value) - @__end__ = - EndContent.new( - value: source[(char_pos + value.length)..-1], - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - end - - # :call-seq: - # on_alias: ( - # (DynaSymbol | SymbolLiteral) left, - # (DynaSymbol | SymbolLiteral) right - # ) -> Alias - def on_alias(left, right) - keyword = find_token(Kw, "alias") - - Alias.new( - left: left, - right: right, - location: keyword.location.to(right.location) - ) - end - - # :call-seq: - # on_aref: (untyped collection, (nil | Args) index) -> ARef - def on_aref(collection, index) - find_token(LBracket) - rbracket = find_token(RBracket) - - ARef.new( - collection: collection, - index: index, - location: collection.location.to(rbracket.location) - ) - end - - # :call-seq: - # on_aref_field: ( - # untyped collection, - # (nil | Args) index - # ) -> ARefField - def on_aref_field(collection, index) - find_token(LBracket) - rbracket = find_token(RBracket) - - ARefField.new( - collection: collection, - index: index, - location: collection.location.to(rbracket.location) - ) - end - - # def on_arg_ambiguous(value) - # value - # end - - # :call-seq: - # on_arg_paren: ( - # (nil | Args | ArgsForward) arguments - # ) -> ArgParen - def on_arg_paren(arguments) - lparen = find_token(LParen) - rparen = find_token(RParen) - - # If the arguments exceed the ending of the parentheses, then we know we - # have a heredoc in the arguments, and we need to use the bounds of the - # arguments to determine how large the arg_paren is. - ending = - if arguments && arguments.location.end_line > rparen.location.end_line - arguments - else - rparen - end - - ArgParen.new( - arguments: arguments, - location: lparen.location.to(ending.location) - ) - end - - # :call-seq: - # on_args_add: (Args arguments, untyped argument) -> Args - def on_args_add(arguments, argument) - if arguments.parts.empty? - # If this is the first argument being passed into the list of arguments, - # then we're going to use the bounds of the argument to override the - # parent node's location since this will be more accurate. - Args.new(parts: [argument], location: argument.location) - else - # Otherwise we're going to update the existing list with the argument - # being added as well as the new end bounds. - Args.new( - parts: arguments.parts << argument, - location: arguments.location.to(argument.location) - ) - end - end - - # :call-seq: - # on_args_add_block: ( - # Args arguments, - # (false | untyped) block - # ) -> Args - def on_args_add_block(arguments, block) - operator = find_token(Op, "&", consume: false) - - # If we can't find the & operator, then there's no block to add to the list, - # so we're just going to return the arguments as-is. - return arguments unless operator - - # Now we know we have an & operator, so we're going to delete it from the - # list of tokens to make sure it doesn't get confused with anything else. - tokens.delete(operator) - - # Construct the location that represents the block argument. - location = operator.location - location = operator.location.to(block.location) if block - - # If there are any arguments and the operator we found from the list is not - # after them, then we're going to return the arguments as-is because we're - # looking at an & that occurs before the arguments are done. - if arguments.parts.any? && location.start_char < arguments.location.end_char - return arguments - end - - # Otherwise, we're looking at an actual block argument (with or without a - # block, which could be missing because it could be a bare & since 3.1.0). - arg_block = ArgBlock.new(value: block, location: location) - - Args.new( - parts: arguments.parts << arg_block, - location: arguments.location.to(location) - ) - end - - # :call-seq: - # on_args_add_star: (Args arguments, untyped star) -> Args - def on_args_add_star(arguments, argument) - beginning = find_token(Op, "*") - ending = argument || beginning - - location = - if arguments.parts.empty? - ending.location - else - arguments.location.to(ending.location) - end - - arg_star = - ArgStar.new( - value: argument, - location: beginning.location.to(ending.location) - ) - - Args.new(parts: arguments.parts << arg_star, location: location) - end - - # :call-seq: - # on_args_forward: () -> ArgsForward - def on_args_forward - op = find_token(Op, "...") - - ArgsForward.new(value: op.value, location: op.location) - end - - # :call-seq: - # on_args_new: () -> Args - def on_args_new - Args.new(parts: [], location: Location.fixed(line: lineno, char: char_pos)) - end - - # :call-seq: - # on_array: ((nil | Args) contents) -> - # ArrayLiteral | QSymbols | QWords | Symbols | Words - def on_array(contents) - if !contents || contents.is_a?(Args) - lbracket = find_token(LBracket) - rbracket = find_token(RBracket) - - ArrayLiteral.new( - lbracket: lbracket, - contents: contents, - location: lbracket.location.to(rbracket.location) - ) - else - tstring_end = - find_token(TStringEnd, location: contents.beginning.location) - - contents.class.new( - beginning: contents.beginning, - elements: contents.elements, - location: contents.location.to(tstring_end.location) - ) - end - end - - # :call-seq: - # on_aryptn: ( - # (nil | VarRef) constant, - # (nil | Array[untyped]) requireds, - # (nil | VarField) rest, - # (nil | Array[untyped]) posts - # ) -> AryPtn - def on_aryptn(constant, requireds, rest, posts) - parts = [constant, *requireds, rest, *posts].compact - - AryPtn.new( - constant: constant, - requireds: requireds || [], - rest: rest, - posts: posts || [], - location: parts[0].location.to(parts[-1].location) - ) - end - - # :call-seq: - # on_assign: ( - # (ARefField | ConstPathField | Field | TopConstField | VarField) target, - # untyped value - # ) -> Assign - def on_assign(target, value) - Assign.new( - target: target, - value: value, - location: target.location.to(value.location) - ) - end - - # :call-seq: - # on_assoc_new: (untyped key, untyped value) -> Assoc - def on_assoc_new(key, value) - location = key.location - location = location.to(value.location) if value - - Assoc.new(key: key, value: value, location: location) - end - - # :call-seq: - # on_assoc_splat: (untyped value) -> AssocSplat - def on_assoc_splat(value) - operator = find_token(Op, "**") - - AssocSplat.new(value: value, location: operator.location.to(value.location)) - end - - # def on_assoclist_from_args(assocs) - # assocs - # end - - # :call-seq: - # on_backref: (String value) -> Backref - def on_backref(value) - Backref.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - end - - # :call-seq: - # on_backtick: (String value) -> Backtick - def on_backtick(value) - node = - Backtick.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - - tokens << node - node - end - - # :call-seq: - # on_bare_assoc_hash: (Array[AssocNew | AssocSplat] assocs) -> BareAssocHash - def on_bare_assoc_hash(assocs) - BareAssocHash.new( - assocs: assocs, - location: assocs[0].location.to(assocs[-1].location) - ) - end - - # :call-seq: - # on_begin: (untyped bodystmt) -> Begin | PinnedBegin - def on_begin(bodystmt) - pin = find_token(Op, "^", consume: false) - - if pin && pin.location.start_char < bodystmt.location.start_char - tokens.delete(pin) - find_token(LParen) - - rparen = find_token(RParen) - location = pin.location.to(rparen.location) - - PinnedBegin.new(statement: bodystmt, location: location) - else - keyword = find_token(Kw, "begin") - end_char = - if bodystmt.rescue_clause || bodystmt.ensure_clause || - bodystmt.else_clause - bodystmt.location.end_char - else - find_token(Kw, "end").location.end_char - end - - bodystmt.bind(keyword.location.end_char, end_char) - location = keyword.location.to(bodystmt.location) - - Begin.new(bodystmt: bodystmt, location: location) - end - end - - # :call-seq: - # on_binary: (untyped left, (Op | Symbol) operator, untyped right) -> Binary - def on_binary(left, operator, right) - if operator.is_a?(Symbol) - # Here, we're going to search backward for the token that's between the - # two operands that matches the operator so we can delete it from the - # list. - index = - tokens.rindex do |token| - location = token.location - - token.is_a?(Op) && token.value == operator.to_s && - location.start_char > left.location.end_char && - location.end_char < right.location.start_char - end - - tokens.delete_at(index) if index - else - # On most Ruby implementations, operator is a Symbol that represents that - # operation being performed. For instance in the example `1 < 2`, the - # `operator` object would be `:<`. However, on JRuby, it's an `@op` node, - # so here we're going to explicitly convert it into the same normalized - # form. - operator = tokens.delete(operator).value - end - - Binary.new( - left: left, - operator: operator, - right: right, - location: left.location.to(right.location) - ) - end - - # :call-seq: - # on_block_var: (Params params, (nil | Array[Ident]) locals) -> BlockVar - def on_block_var(params, locals) - index = - tokens.rindex do |node| - node.is_a?(Op) && %w[| ||].include?(node.value) && - node.location.start_char < params.location.start_char - end - - beginning = tokens[index] - ending = tokens[-1] - - BlockVar.new( - params: params, - locals: locals || [], - location: beginning.location.to(ending.location) - ) - end - - # :call-seq: - # on_blockarg: (Ident name) -> BlockArg - def on_blockarg(name) - operator = find_token(Op, "&") - - location = operator.location - location = location.to(name.location) if name - - BlockArg.new(name: name, location: location) - end - - # :call-seq: - # on_bodystmt: ( - # Statements statements, - # (nil | Rescue) rescue_clause, - # (nil | Statements) else_clause, - # (nil | Ensure) ensure_clause - # ) -> BodyStmt - def on_bodystmt(statements, rescue_clause, else_clause, ensure_clause) - BodyStmt.new( - statements: statements, - rescue_clause: rescue_clause, - else_clause: else_clause, - ensure_clause: ensure_clause, - location: Location.fixed(line: lineno, char: char_pos) - ) - end - - # :call-seq: - # on_brace_block: ( - # (nil | BlockVar) block_var, - # Statements statements - # ) -> BraceBlock - def on_brace_block(block_var, statements) - lbrace = find_token(LBrace) - rbrace = find_token(RBrace) - - statements.bind( - find_next_statement_start((block_var || lbrace).location.end_char), - rbrace.location.start_char - ) - - location = - Location.new( - start_line: lbrace.location.start_line, - start_char: lbrace.location.start_char, - end_line: [rbrace.location.end_line, statements.location.end_line].max, - end_char: rbrace.location.end_char - ) - - BraceBlock.new( - lbrace: lbrace, - block_var: block_var, - statements: statements, - location: location - ) - end - - # :call-seq: - # on_break: (Args arguments) -> Break - def on_break(arguments) - keyword = find_token(Kw, "break") - - location = keyword.location - location = location.to(arguments.location) if arguments.parts.any? - - Break.new(arguments: arguments, location: location) - end - - # :call-seq: - # on_call: ( - # untyped receiver, - # (:"::" | Op | Period) operator, - # (:call | Backtick | Const | Ident | Op) message - # ) -> Call - def on_call(receiver, operator, message) - ending = message - ending = operator if message == :call - - Call.new( - receiver: receiver, - operator: operator, - message: message, - arguments: nil, - location: receiver.location.to(ending.location) - ) - end - - # :call-seq: - # on_case: (untyped value, untyped consequent) -> Case | RAssign - def on_case(value, consequent) - if keyword = find_token(Kw, "case", consume: false) - tokens.delete(keyword) - - Case.new( - keyword: keyword, - value: value, - consequent: consequent, - location: keyword.location.to(consequent.location) - ) - else - operator = find_token(Kw, "in", consume: false) || find_token(Op, "=>") - - RAssign.new( - value: value, - operator: operator, - pattern: consequent, - location: value.location.to(consequent.location) - ) - end - end - - # :call-seq: - # on_class: ( - # (ConstPathRef | ConstRef | TopConstRef) constant, - # untyped superclass, - # BodyStmt bodystmt - # ) -> ClassDeclaration - def on_class(constant, superclass, bodystmt) - beginning = find_token(Kw, "class") - ending = find_token(Kw, "end") - - bodystmt.bind( - find_next_statement_start((superclass || constant).location.end_char), - ending.location.start_char - ) - - ClassDeclaration.new( - constant: constant, - superclass: superclass, - bodystmt: bodystmt, - location: beginning.location.to(ending.location) - ) - end - - # :call-seq: - # on_comma: (String value) -> Comma - def on_comma(value) - node = - Comma.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - - tokens << node - node - end - - # :call-seq: - # on_command: ((Const | Ident) message, Args arguments) -> Command - def on_command(message, arguments) - Command.new( - message: message, - arguments: arguments, - location: message.location.to(arguments.location) - ) - end - - # :call-seq: - # on_command_call: ( - # untyped receiver, - # (:"::" | Op | Period) operator, - # (Const | Ident | Op) message, - # (nil | Args) arguments - # ) -> CommandCall - def on_command_call(receiver, operator, message, arguments) - ending = arguments || message - - CommandCall.new( - receiver: receiver, - operator: operator, - message: message, - arguments: arguments, - location: receiver.location.to(ending.location) - ) - end - - # :call-seq: - # on_comment: (String value) -> Comment - def on_comment(value) - line = lineno - comment = - Comment.new( - value: value.chomp, - inline: value.strip != lines[line - 1].strip, - location: - Location.token(line: line, char: char_pos, size: value.size - 1) - ) - - @comments << comment - comment - end - - # :call-seq: - # on_const: (String value) -> Const - def on_const(value) - Const.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - end - - # :call-seq: - # on_const_path_field: (untyped parent, Const constant) -> ConstPathField - def on_const_path_field(parent, constant) - ConstPathField.new( - parent: parent, - constant: constant, - location: parent.location.to(constant.location) - ) - end - - # :call-seq: - # on_const_path_ref: (untyped parent, Const constant) -> ConstPathRef - def on_const_path_ref(parent, constant) - ConstPathRef.new( - parent: parent, - constant: constant, - location: parent.location.to(constant.location) - ) - end - - # :call-seq: - # on_const_ref: (Const constant) -> ConstRef - def on_const_ref(constant) - ConstRef.new(constant: constant, location: constant.location) - end - - # :call-seq: - # on_cvar: (String value) -> CVar - def on_cvar(value) - CVar.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - end - - # :call-seq: - # on_def: ( - # (Backtick | Const | Ident | Kw | Op) name, - # (nil | Params | Paren) params, - # untyped bodystmt - # ) -> Def | DefEndless - def on_def(name, params, bodystmt) - # Make sure to delete this token in case you're defining something like def - # class which would lead to this being a kw and causing all kinds of trouble - tokens.delete(name) - - # Find the beginning of the method definition, which works for single-line - # and normal method definitions. - beginning = find_token(Kw, "def") - - # If there aren't any params then we need to correct the params node - # location information - if params.is_a?(Params) && params.empty? - end_char = name.location.end_char - location = - Location.new( - start_line: params.location.start_line, - start_char: end_char, - end_line: params.location.end_line, - end_char: end_char - ) - - params = Params.new(location: location) - end - - ending = find_token(Kw, "end", consume: false) - - if ending - tokens.delete(ending) - bodystmt.bind( - find_next_statement_start(params.location.end_char), - ending.location.start_char - ) - - Def.new( - name: name, - params: params, - bodystmt: bodystmt, - location: beginning.location.to(ending.location) - ) - else - # In Ruby >= 3.1.0, this is a BodyStmt that wraps a single statement in - # the statements list. Before, it was just the individual statement. - statement = bodystmt.is_a?(BodyStmt) ? bodystmt.statements : bodystmt - - DefEndless.new( - target: nil, - operator: nil, - name: name, - paren: params, - statement: statement, - location: beginning.location.to(bodystmt.location) - ) - end - end - - # :call-seq: - # on_defined: (untyped value) -> Defined - def on_defined(value) - beginning = find_token(Kw, "defined?") - ending = value - - range = beginning.location.end_char...value.location.start_char - if source[range].include?("(") - find_token(LParen) - ending = find_token(RParen) - end - - Defined.new(value: value, location: beginning.location.to(ending.location)) - end - - # :call-seq: - # on_defs: ( - # untyped target, - # (Op | Period) operator, - # (Backtick | Const | Ident | Kw | Op) name, - # (Params | Paren) params, - # BodyStmt bodystmt - # ) -> Defs - def on_defs(target, operator, name, params, bodystmt) - # Make sure to delete this token in case you're defining something - # like def class which would lead to this being a kw and causing all kinds - # of trouble - tokens.delete(name) - - # If there aren't any params then we need to correct the params node - # location information - if params.is_a?(Params) && params.empty? - end_char = name.location.end_char - location = - Location.new( - start_line: params.location.start_line, - start_char: end_char, - end_line: params.location.end_line, - end_char: end_char - ) - - params = Params.new(location: location) - end - - beginning = find_token(Kw, "def") - ending = find_token(Kw, "end", consume: false) - - if ending - tokens.delete(ending) - bodystmt.bind( - find_next_statement_start(params.location.end_char), - ending.location.start_char - ) - - Defs.new( - target: target, - operator: operator, - name: name, - params: params, - bodystmt: bodystmt, - location: beginning.location.to(ending.location) - ) - else - # In Ruby >= 3.1.0, this is a BodyStmt that wraps a single statement in - # the statements list. Before, it was just the individual statement. - statement = bodystmt.is_a?(BodyStmt) ? bodystmt.statements : bodystmt - - DefEndless.new( - target: target, - operator: operator, - name: name, - paren: params, - statement: statement, - location: beginning.location.to(bodystmt.location) - ) - end - end - - # :call-seq: - # on_do_block: (BlockVar block_var, BodyStmt bodystmt) -> DoBlock - def on_do_block(block_var, bodystmt) - beginning = find_token(Kw, "do") - ending = find_token(Kw, "end") - - bodystmt.bind( - find_next_statement_start((block_var || beginning).location.end_char), - ending.location.start_char - ) - - DoBlock.new( - keyword: beginning, - block_var: block_var, - bodystmt: bodystmt, - location: beginning.location.to(ending.location) - ) - end - - # :call-seq: - # on_dot2: ((nil | untyped) left, (nil | untyped) right) -> Dot2 - def on_dot2(left, right) - operator = find_token(Op, "..") - - beginning = left || operator - ending = right || operator - - Dot2.new( - left: left, - right: right, - location: beginning.location.to(ending.location) - ) - end - - # :call-seq: - # on_dot3: ((nil | untyped) left, (nil | untyped) right) -> Dot3 - def on_dot3(left, right) - operator = find_token(Op, "...") - - beginning = left || operator - ending = right || operator - - Dot3.new( - left: left, - right: right, - location: beginning.location.to(ending.location) - ) - end - - # :call-seq: - # on_dyna_symbol: (StringContent string_content) -> DynaSymbol - def on_dyna_symbol(string_content) - if find_token(SymBeg, consume: false) - # A normal dynamic symbol - symbeg = find_token(SymBeg) - tstring_end = find_token(TStringEnd, location: symbeg.location) - - DynaSymbol.new( - quote: symbeg.value, - parts: string_content.parts, - location: symbeg.location.to(tstring_end.location) - ) - else - # A dynamic symbol as a hash key - tstring_beg = find_token(TStringBeg) - label_end = find_token(LabelEnd) - - DynaSymbol.new( - parts: string_content.parts, - quote: label_end.value[0], - location: tstring_beg.location.to(label_end.location) - ) - end - end - - # :call-seq: - # on_else: (Statements statements) -> Else - def on_else(statements) - beginning = find_token(Kw, "else") - - # else can either end with an end keyword (in which case we'll want to - # consume that event) or it can end with an ensure keyword (in which case - # we'll leave that to the ensure to handle). - index = - tokens.rindex do |token| - token.is_a?(Kw) && %w[end ensure].include?(token.value) - end - - node = tokens[index] - ending = node.value == "end" ? tokens.delete_at(index) : node - # ending = node - - statements.bind(beginning.location.end_char, ending.location.start_char) - - Else.new( - statements: statements, - location: beginning.location.to(ending.location) - ) - end - - # :call-seq: - # on_elsif: ( - # untyped predicate, - # Statements statements, - # (nil | Elsif | Else) consequent - # ) -> Elsif - def on_elsif(predicate, statements, consequent) - beginning = find_token(Kw, "elsif") - ending = consequent || find_token(Kw, "end") - - statements.bind(predicate.location.end_char, ending.location.start_char) - - Elsif.new( - predicate: predicate, - statements: statements, - consequent: consequent, - location: beginning.location.to(ending.location) - ) - end - - # :call-seq: - # on_embdoc: (String value) -> EmbDoc - def on_embdoc(value) - @embdoc.value << value - @embdoc - end - - # :call-seq: - # on_embdoc_beg: (String value) -> EmbDoc - def on_embdoc_beg(value) - @embdoc = - EmbDoc.new( - value: value, - location: Location.fixed(line: lineno, char: char_pos) - ) - end - - # :call-seq: - # on_embdoc_end: (String value) -> EmbDoc - def on_embdoc_end(value) - location = @embdoc.location - embdoc = - EmbDoc.new( - value: @embdoc.value << value.chomp, - location: - Location.new( - start_line: location.start_line, - start_char: location.start_char, - end_line: lineno, - end_char: char_pos + value.length - 1 - ) - ) - - @comments << embdoc - @embdoc = nil - - embdoc - end - - # :call-seq: - # on_embexpr_beg: (String value) -> EmbExprBeg - def on_embexpr_beg(value) - node = - EmbExprBeg.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - - tokens << node - node - end - - # :call-seq: - # on_embexpr_end: (String value) -> EmbExprEnd - def on_embexpr_end(value) - node = - EmbExprEnd.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - - tokens << node - node - end - - # :call-seq: - # on_embvar: (String value) -> EmbVar - def on_embvar(value) - node = - EmbVar.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - - tokens << node - node - end - - # :call-seq: - # on_ensure: (Statements statements) -> Ensure - def on_ensure(statements) - keyword = find_token(Kw, "ensure") - - # We don't want to consume the :@kw event, because that would break - # def..ensure..end chains. - ending = find_token(Kw, "end", consume: false) - statements.bind( - find_next_statement_start(keyword.location.end_char), - ending.location.start_char - ) - - Ensure.new( - keyword: keyword, - statements: statements, - location: keyword.location.to(ending.location) - ) - end - - # The handler for this event accepts no parameters (though in previous - # versions of Ruby it accepted a string literal with a value of ","). - # - # :call-seq: - # on_excessed_comma: () -> ExcessedComma - def on_excessed_comma(*) - comma = find_token(Comma) - - ExcessedComma.new(value: comma.value, location: comma.location) - end - - # :call-seq: - # on_fcall: ((Const | Ident) value) -> FCall - def on_fcall(value) - FCall.new(value: value, arguments: nil, location: value.location) - end - - # :call-seq: - # on_field: ( - # untyped parent, - # (:"::" | Op | Period) operator - # (Const | Ident) name - # ) -> Field - def on_field(parent, operator, name) - Field.new( - parent: parent, - operator: operator, - name: name, - location: parent.location.to(name.location) - ) - end - - # :call-seq: - # on_float: (String value) -> FloatLiteral - def on_float(value) - FloatLiteral.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - end - - # :call-seq: - # on_fndptn: ( - # (nil | untyped) constant, - # VarField left, - # Array[untyped] values, - # VarField right - # ) -> FndPtn - def on_fndptn(constant, left, values, right) - beginning = constant || find_token(LBracket) - ending = find_token(RBracket) - - FndPtn.new( - constant: constant, - left: left, - values: values, - right: right, - location: beginning.location.to(ending.location) - ) - end - - # :call-seq: - # on_for: ( - # (MLHS | VarField) value, - # untyped collection, - # Statements statements - # ) -> For - def on_for(index, collection, statements) - beginning = find_token(Kw, "for") - in_keyword = find_token(Kw, "in") - ending = find_token(Kw, "end") - - # Consume the do keyword if it exists so that it doesn't get confused for - # some other block - keyword = find_token(Kw, "do", consume: false) - if keyword && keyword.location.start_char > collection.location.end_char && - keyword.location.end_char < ending.location.start_char - tokens.delete(keyword) - end - - statements.bind( - (keyword || collection).location.end_char, - ending.location.start_char - ) - - if index.is_a?(MLHS) - comma_range = index.location.end_char...in_keyword.location.start_char - index.comma = true if source[comma_range].strip.start_with?(",") - end - - For.new( - index: index, - collection: collection, - statements: statements, - location: beginning.location.to(ending.location) - ) - end - - # :call-seq: - # on_gvar: (String value) -> GVar - def on_gvar(value) - GVar.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - end - - # :call-seq: - # on_hash: ((nil | Array[AssocNew | AssocSplat]) assocs) -> HashLiteral - def on_hash(assocs) - lbrace = find_token(LBrace) - rbrace = find_token(RBrace) - - HashLiteral.new( - lbrace: lbrace, - assocs: assocs || [], - location: lbrace.location.to(rbrace.location) - ) - end - - # :call-seq: - # on_heredoc_beg: (String value) -> HeredocBeg - def on_heredoc_beg(value) - location = - Location.token(line: lineno, char: char_pos, size: value.size + 1) - - # Here we're going to artificially create an extra node type so that if - # there are comments after the declaration of a heredoc, they get printed. - beginning = HeredocBeg.new(value: value, location: location) - @heredocs << Heredoc.new(beginning: beginning, location: location) - - beginning - end - - # :call-seq: - # on_heredoc_dedent: (StringContent string, Integer width) -> Heredoc - def on_heredoc_dedent(string, width) - heredoc = @heredocs[-1] - - @heredocs[-1] = Heredoc.new( - beginning: heredoc.beginning, - ending: heredoc.ending, - parts: string.parts, - location: heredoc.location - ) - end - - # :call-seq: - # on_heredoc_end: (String value) -> Heredoc - def on_heredoc_end(value) - heredoc = @heredocs[-1] - - @heredocs[-1] = Heredoc.new( - beginning: heredoc.beginning, - ending: value.chomp, - parts: heredoc.parts, - location: - Location.new( - start_line: heredoc.location.start_line, - start_char: heredoc.location.start_char, - end_line: lineno, - end_char: char_pos - ) - ) - end - - # :call-seq: - # on_hshptn: ( - # (nil | untyped) constant, - # Array[[Label, untyped]] keywords, - # (nil | VarField) keyword_rest - # ) -> HshPtn - def on_hshptn(constant, keywords, keyword_rest) - parts = [constant, keywords, keyword_rest].flatten(2).compact - - HshPtn.new( - constant: constant, - keywords: keywords, - keyword_rest: keyword_rest, - location: parts[0].location.to(parts[-1].location) - ) - end - - # :call-seq: - # on_ident: (String value) -> Ident - def on_ident(value) - Ident.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - end - - # :call-seq: - # on_if: ( - # untyped predicate, - # Statements statements, - # (nil | Elsif | Else) consequent - # ) -> If - def on_if(predicate, statements, consequent) - beginning = find_token(Kw, "if") - ending = consequent || find_token(Kw, "end") - - statements.bind(predicate.location.end_char, ending.location.start_char) - - If.new( - predicate: predicate, - statements: statements, - consequent: consequent, - location: beginning.location.to(ending.location) - ) - end - - # :call-seq: - # on_ifop: (untyped predicate, untyped truthy, untyped falsy) -> IfOp - def on_ifop(predicate, truthy, falsy) - IfOp.new( - predicate: predicate, - truthy: truthy, - falsy: falsy, - location: predicate.location.to(falsy.location) - ) - end - - # :call-seq: - # on_if_mod: (untyped predicate, untyped statement) -> IfMod - def on_if_mod(predicate, statement) - find_token(Kw, "if") - - IfMod.new( - statement: statement, - predicate: predicate, - location: statement.location.to(predicate.location) - ) - end - - # def on_ignored_nl(value) - # value - # end - - # def on_ignored_sp(value) - # value - # end - - # :call-seq: - # on_imaginary: (String value) -> Imaginary - def on_imaginary(value) - Imaginary.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - end - - # :call-seq: - # on_in: (RAssign pattern, nil statements, nil consequent) -> RAssign - # | ( - # untyped pattern, - # Statements statements, - # (nil | In | Else) consequent - # ) -> In - def on_in(pattern, statements, consequent) - # Here we have a rightward assignment - return pattern unless statements - - beginning = find_token(Kw, "in") - ending = consequent || find_token(Kw, "end") - - statements_start = pattern - if token = find_token(Kw, "then", consume: false) - tokens.delete(token) - statements_start = token - end - - statements.bind( - find_next_statement_start(statements_start.location.end_char), - ending.location.start_char - ) - - In.new( - pattern: pattern, - statements: statements, - consequent: consequent, - location: beginning.location.to(ending.location) - ) - end - - # :call-seq: - # on_int: (String value) -> Int - def on_int(value) - Int.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - end - - # :call-seq: - # on_ivar: (String value) -> IVar - def on_ivar(value) - IVar.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - end - - # :call-seq: - # on_kw: (String value) -> Kw - def on_kw(value) - node = - Kw.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - - tokens << node - node - end - - # :call-seq: - # on_kwrest_param: ((nil | Ident) name) -> KwRestParam - def on_kwrest_param(name) - location = find_token(Op, "**").location - location = location.to(name.location) if name - - KwRestParam.new(name: name, location: location) - end - - # :call-seq: - # on_label: (String value) -> Label - def on_label(value) - Label.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - end - - # :call-seq: - # on_label_end: (String value) -> LabelEnd - def on_label_end(value) - node = - LabelEnd.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - - tokens << node - node - end - - # :call-seq: - # on_lambda: ( - # (Params | Paren) params, - # (BodyStmt | Statements) statements - # ) -> Lambda - def on_lambda(params, statements) - beginning = find_token(TLambda) - - if tokens.any? { |token| - token.is_a?(TLamBeg) && - token.location.start_char > beginning.location.start_char - } - opening = find_token(TLamBeg) - closing = find_token(RBrace) - else - opening = find_token(Kw, "do") - closing = find_token(Kw, "end") - end - - statements.bind(opening.location.end_char, closing.location.start_char) - - Lambda.new( - params: params, - statements: statements, - location: beginning.location.to(closing.location) - ) - end - - # :call-seq: - # on_lbrace: (String value) -> LBrace - def on_lbrace(value) - node = - LBrace.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - - tokens << node - node - end - - # :call-seq: - # on_lbracket: (String value) -> LBracket - def on_lbracket(value) - node = - LBracket.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - - tokens << node - node - end - - # :call-seq: - # on_lparen: (String value) -> LParen - def on_lparen(value) - node = - LParen.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - - tokens << node - node - end - - # def on_magic_comment(key, value) - # [key, value] - # end - - # :call-seq: - # on_massign: ((MLHS | MLHSParen) target, untyped value) -> MAssign - def on_massign(target, value) - comma_range = target.location.end_char...value.location.start_char - target.comma = true if source[comma_range].strip.start_with?(",") - - MAssign.new( - target: target, - value: value, - location: target.location.to(value.location) - ) - end - - # :call-seq: - # on_method_add_arg: ( - # (Call | FCall) call, - # (ArgParen | Args) arguments - # ) -> Call | FCall - def on_method_add_arg(call, arguments) - location = call.location - location = location.to(arguments.location) if arguments.is_a?(ArgParen) - - if call.is_a?(FCall) - FCall.new(value: call.value, arguments: arguments, location: location) - else - Call.new( - receiver: call.receiver, - operator: call.operator, - message: call.message, - arguments: arguments, - location: location - ) - end - end - - # :call-seq: - # on_method_add_block: ( - # (Call | Command | CommandCall | FCall) call, - # (BraceBlock | DoBlock) block - # ) -> MethodAddBlock - def on_method_add_block(call, block) - MethodAddBlock.new( - call: call, - block: block, - location: call.location.to(block.location) - ) - end - - # :call-seq: - # on_mlhs_add: ( - # MLHS mlhs, - # (ARefField | Field | Ident | MLHSParen | VarField) part - # ) -> MLHS - def on_mlhs_add(mlhs, part) - location = - mlhs.parts.empty? ? part.location : mlhs.location.to(part.location) - - MLHS.new(parts: mlhs.parts << part, location: location) - end - - # :call-seq: - # on_mlhs_add_post: (MLHS left, MLHS right) -> MLHS - def on_mlhs_add_post(left, right) - MLHS.new( - parts: left.parts + right.parts, - location: left.location.to(right.location) - ) - end - - # :call-seq: - # on_mlhs_add_star: ( - # MLHS mlhs, - # (nil | ARefField | Field | Ident | VarField) part - # ) -> MLHS - def on_mlhs_add_star(mlhs, part) - beginning = find_token(Op, "*") - ending = part || beginning - - location = beginning.location.to(ending.location) - arg_star = ArgStar.new(value: part, location: location) - - location = mlhs.location.to(location) unless mlhs.parts.empty? - MLHS.new(parts: mlhs.parts << arg_star, location: location) - end - - # :call-seq: - # on_mlhs_new: () -> MLHS - def on_mlhs_new - MLHS.new(parts: [], location: Location.fixed(line: lineno, char: char_pos)) - end - - # :call-seq: - # on_mlhs_paren: ((MLHS | MLHSParen) contents) -> MLHSParen - def on_mlhs_paren(contents) - lparen = find_token(LParen) - rparen = find_token(RParen) - - comma_range = lparen.location.end_char...rparen.location.start_char - contents.comma = true if source[comma_range].strip.end_with?(",") - - MLHSParen.new( - contents: contents, - location: lparen.location.to(rparen.location) - ) - end - - # :call-seq: - # on_module: ( - # (ConstPathRef | ConstRef | TopConstRef) constant, - # BodyStmt bodystmt - # ) -> ModuleDeclaration - def on_module(constant, bodystmt) - beginning = find_token(Kw, "module") - ending = find_token(Kw, "end") - - bodystmt.bind( - find_next_statement_start(constant.location.end_char), - ending.location.start_char - ) - - ModuleDeclaration.new( - constant: constant, - bodystmt: bodystmt, - location: beginning.location.to(ending.location) - ) - end - - # :call-seq: - # on_mrhs_new: () -> MRHS - def on_mrhs_new - MRHS.new(parts: [], location: Location.fixed(line: lineno, char: char_pos)) - end - - # :call-seq: - # on_mrhs_add: (MRHS mrhs, untyped part) -> MRHS - def on_mrhs_add(mrhs, part) - location = - if mrhs.parts.empty? - mrhs.location - else - mrhs.location.to(part.location) - end - - MRHS.new(parts: mrhs.parts << part, location: location) - end - - # :call-seq: - # on_mrhs_add_star: (MRHS mrhs, untyped value) -> MRHS - def on_mrhs_add_star(mrhs, value) - beginning = find_token(Op, "*") - ending = value || beginning - - arg_star = - ArgStar.new( - value: value, - location: beginning.location.to(ending.location) - ) - - location = - if mrhs.parts.empty? - arg_star.location - else - mrhs.location.to(arg_star.location) - end - - MRHS.new(parts: mrhs.parts << arg_star, location: location) - end - - # :call-seq: - # on_mrhs_new_from_args: (Args arguments) -> MRHS - def on_mrhs_new_from_args(arguments) - MRHS.new(parts: arguments.parts, location: arguments.location) - end - - # :call-seq: - # on_next: (Args arguments) -> Next - def on_next(arguments) - keyword = find_token(Kw, "next") - - location = keyword.location - location = location.to(arguments.location) if arguments.parts.any? - - Next.new(arguments: arguments, location: location) - end - - # def on_nl(value) - # value - # end - - # def on_nokw_param(value) - # value - # end - - # :call-seq: - # on_op: (String value) -> Op - def on_op(value) - node = - Op.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - - tokens << node - node - end - - # :call-seq: - # on_opassign: ( - # (ARefField | ConstPathField | Field | TopConstField | VarField) target, - # Op operator, - # untyped value - # ) -> OpAssign - def on_opassign(target, operator, value) - OpAssign.new( - target: target, - operator: operator, - value: value, - location: target.location.to(value.location) - ) - end - - # def on_operator_ambiguous(value) - # value - # end - - # :call-seq: - # on_params: ( - # (nil | Array[Ident]) requireds, - # (nil | Array[[Ident, untyped]]) optionals, - # (nil | ArgsForward | ExcessedComma | RestParam) rest, - # (nil | Array[Ident]) posts, - # (nil | Array[[Ident, nil | untyped]]) keywords, - # (nil | :nil | ArgsForward | KwRestParam) keyword_rest, - # (nil | :& | BlockArg) block - # ) -> Params - def on_params( - requireds, - optionals, - rest, - posts, - keywords, - keyword_rest, - block - ) - parts = [ - *requireds, - *optionals&.flatten(1), - rest, - *posts, - *keywords&.flat_map { |(key, value)| [key, value || nil] }, - (keyword_rest if keyword_rest != :nil), - (block if block != :&) - ].compact - - location = - if parts.any? - parts[0].location.to(parts[-1].location) - else - Location.fixed(line: lineno, char: char_pos) - end - - Params.new( - requireds: requireds || [], - optionals: optionals || [], - rest: rest, - posts: posts || [], - keywords: keywords || [], - keyword_rest: keyword_rest, - block: (block if block != :&), - location: location - ) - end - - # :call-seq: - # on_paren: (untyped contents) -> Paren - def on_paren(contents) - lparen = find_token(LParen) - rparen = find_token(RParen) - - if contents && contents.is_a?(Params) - location = contents.location - location = - Location.new( - start_line: location.start_line, - start_char: find_next_statement_start(lparen.location.end_char), - end_line: location.end_line, - end_char: rparen.location.start_char - ) - - contents = - Params.new( - requireds: contents.requireds, - optionals: contents.optionals, - rest: contents.rest, - posts: contents.posts, - keywords: contents.keywords, - keyword_rest: contents.keyword_rest, - block: contents.block, - location: location - ) - end - - Paren.new( - lparen: lparen, - contents: contents || nil, - location: lparen.location.to(rparen.location) - ) - end - - # If we encounter a parse error, just immediately bail out so that our runner - # can catch it. - def on_parse_error(error, *) - raise ParseError.new(error, lineno, column) - end - alias on_alias_error on_parse_error - alias on_assign_error on_parse_error - alias on_class_name_error on_parse_error - alias on_param_error on_parse_error - - # :call-seq: - # on_period: (String value) -> Period - def on_period(value) - Period.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - end - - # :call-seq: - # on_program: (Statements statements) -> Program - def on_program(statements) - location = - Location.new( - start_line: 1, - start_char: 0, - end_line: lines.length, - end_char: source.length - ) - - statements.body << @__end__ if @__end__ - statements.bind(0, source.length) - - program = Program.new(statements: statements, location: location) - attach_comments(program, @comments) - - program - end - - # Attaches comments to the nodes in the tree that most closely correspond to - # the location of the comments. - def attach_comments(program, comments) - comments.each do |comment| - preceding, enclosing, following = nearest_nodes(program, comment) - - if comment.inline? - if preceding - preceding.comments << comment - comment.trailing! - elsif following - following.comments << comment - comment.leading! - elsif enclosing - enclosing.comments << comment - else - program.comments << comment - end - else - # If a comment exists on its own line, prefer a leading comment. - if following - following.comments << comment - comment.leading! - elsif preceding - preceding.comments << comment - comment.trailing! - elsif enclosing - enclosing.comments << comment - else - program.comments << comment - end - end - end - end - - # Responsible for finding the nearest nodes to the given comment within the - # context of the given encapsulating node. - def nearest_nodes(node, comment) - comment_start = comment.location.start_char - comment_end = comment.location.end_char - - child_nodes = node.child_nodes.compact - preceding = nil - following = nil - - left = 0 - right = child_nodes.length - - # This is a custom binary search that finds the nearest nodes to the given - # comment. When it finds a node that completely encapsulates the comment, it - # recursed downward into the tree. - while left < right - middle = (left + right) / 2 - child = child_nodes[middle] - - node_start = child.location.start_char - node_end = child.location.end_char - - if node_start <= comment_start && comment_end <= node_end - # The comment is completely contained by this child node. Abandon the - # binary search at this level. - return nearest_nodes(child, comment) - end - - if node_end <= comment_start - # This child node falls completely before the comment. Because we will - # never consider this node or any nodes before it again, this node must - # be the closest preceding node we have encountered so far. - preceding = child - left = middle + 1 - next - end - - if comment_end <= node_start - # This child node falls completely after the comment. Because we will - # never consider this node or any nodes after it again, this node must - # be the closest following node we have encountered so far. - following = child - right = middle - next - end - - # This should only happen if there is a bug in this parser. - raise "Comment location overlaps with node location" - end - - [preceding, node, following] - end - - # :call-seq: - # on_qsymbols_add: (QSymbols qsymbols, TStringContent element) -> QSymbols - def on_qsymbols_add(qsymbols, element) - QSymbols.new( - beginning: qsymbols.beginning, - elements: qsymbols.elements << element, - location: qsymbols.location.to(element.location) - ) - end - - # :call-seq: - # on_qsymbols_beg: (String value) -> QSymbolsBeg - def on_qsymbols_beg(value) - node = - QSymbolsBeg.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - - tokens << node - node - end - - # :call-seq: - # on_qsymbols_new: () -> QSymbols - def on_qsymbols_new - beginning = find_token(QSymbolsBeg) - - QSymbols.new( - beginning: beginning, - elements: [], - location: beginning.location - ) - end - - # :call-seq: - # on_qwords_add: (QWords qwords, TStringContent element) -> QWords - def on_qwords_add(qwords, element) - QWords.new( - beginning: qwords.beginning, - elements: qwords.elements << element, - location: qwords.location.to(element.location) - ) - end - - # :call-seq: - # on_qwords_beg: (String value) -> QWordsBeg - def on_qwords_beg(value) - node = - QWordsBeg.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - - tokens << node - node - end - - # :call-seq: - # on_qwords_new: () -> QWords - def on_qwords_new - beginning = find_token(QWordsBeg) - - QWords.new(beginning: beginning, elements: [], location: beginning.location) - end - - # :call-seq: - # on_rational: (String value) -> RationalLiteral - def on_rational(value) - RationalLiteral.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - end - - # :call-seq: - # on_rbrace: (String value) -> RBrace - def on_rbrace(value) - node = - RBrace.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - - tokens << node - node - end - - # :call-seq: - # on_rbracket: (String value) -> RBracket - def on_rbracket(value) - node = - RBracket.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - - tokens << node - node - end - - # :call-seq: - # on_redo: () -> Redo - def on_redo - keyword = find_token(Kw, "redo") - - Redo.new(value: keyword.value, location: keyword.location) - end - - # :call-seq: - # on_regexp_add: ( - # RegexpContent regexp_content, - # (StringDVar | StringEmbExpr | TStringContent) part - # ) -> RegexpContent - def on_regexp_add(regexp_content, part) - RegexpContent.new( - beginning: regexp_content.beginning, - parts: regexp_content.parts << part, - location: regexp_content.location.to(part.location) - ) - end - - # :call-seq: - # on_regexp_beg: (String value) -> RegexpBeg - def on_regexp_beg(value) - node = - RegexpBeg.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - - tokens << node - node - end - - # :call-seq: - # on_regexp_end: (String value) -> RegexpEnd - def on_regexp_end(value) - RegexpEnd.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - end - - # :call-seq: - # on_regexp_literal: ( - # RegexpContent regexp_content, - # RegexpEnd ending - # ) -> RegexpLiteral - def on_regexp_literal(regexp_content, ending) - RegexpLiteral.new( - beginning: regexp_content.beginning, - ending: ending.value, - parts: regexp_content.parts, - location: regexp_content.location.to(ending.location) - ) - end - - # :call-seq: - # on_regexp_new: () -> RegexpContent - def on_regexp_new - regexp_beg = find_token(RegexpBeg) - - RegexpContent.new( - beginning: regexp_beg.value, - parts: [], - location: regexp_beg.location - ) - end - - # :call-seq: - # on_rescue: ( - # (nil | [untyped] | MRHS | MRHSAddStar) exceptions, - # (nil | Field | VarField) variable, - # Statements statements, - # (nil | Rescue) consequent - # ) -> Rescue - def on_rescue(exceptions, variable, statements, consequent) - keyword = find_token(Kw, "rescue") - exceptions = exceptions[0] if exceptions.is_a?(Array) - - last_node = variable || exceptions || keyword - statements.bind( - find_next_statement_start(last_node.location.end_char), - char_pos - ) - - # We add an additional inner node here that ripper doesn't provide so that - # we have a nice place to attach inline comments. But we only need it if we - # have an exception or a variable that we're rescuing. - rescue_ex = - if exceptions || variable - RescueEx.new( - exceptions: exceptions, - variable: variable, - location: - Location.new( - start_line: keyword.location.start_line, - start_char: keyword.location.end_char + 1, - end_line: last_node.location.end_line, - end_char: last_node.location.end_char - ) - ) - end - - Rescue.new( - exception: rescue_ex, - statements: statements, - consequent: consequent, - location: - Location.new( - start_line: keyword.location.start_line, - start_char: keyword.location.start_char, - end_line: lineno, - end_char: char_pos - ) - ) - end - - # :call-seq: - # on_rescue_mod: (untyped statement, untyped value) -> RescueMod - def on_rescue_mod(statement, value) - find_token(Kw, "rescue") - - RescueMod.new( - statement: statement, - value: value, - location: statement.location.to(value.location) - ) - end - - # :call-seq: - # on_rest_param: ((nil | Ident) name) -> RestParam - def on_rest_param(name) - location = find_token(Op, "*").location - location = location.to(name.location) if name - - RestParam.new(name: name, location: location) - end - - # :call-seq: - # on_retry: () -> Retry - def on_retry - keyword = find_token(Kw, "retry") - - Retry.new(value: keyword.value, location: keyword.location) - end - - # :call-seq: - # on_return: (Args arguments) -> Return - def on_return(arguments) - keyword = find_token(Kw, "return") - - Return.new( - arguments: arguments, - location: keyword.location.to(arguments.location) - ) - end - - # :call-seq: - # on_return0: () -> Return0 - def on_return0 - keyword = find_token(Kw, "return") - - Return0.new(value: keyword.value, location: keyword.location) - end - - # :call-seq: - # on_rparen: (String value) -> RParen - def on_rparen(value) - node = - RParen.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - - tokens << node - node - end - - # :call-seq: - # on_sclass: (untyped target, BodyStmt bodystmt) -> SClass - def on_sclass(target, bodystmt) - beginning = find_token(Kw, "class") - ending = find_token(Kw, "end") - - bodystmt.bind( - find_next_statement_start(target.location.end_char), - ending.location.start_char - ) - - SClass.new( - target: target, - bodystmt: bodystmt, - location: beginning.location.to(ending.location) - ) - end - - # def on_semicolon(value) - # value - # end - - # def on_sp(value) - # value - # end - - # stmts_add is a parser event that represents a single statement inside a - # list of statements within any lexical block. It accepts as arguments the - # parent stmts node as well as an stmt which can be any expression in - # Ruby. - def on_stmts_add(statements, statement) - location = - if statements.body.empty? - statement.location - else - statements.location.to(statement.location) - end - - Statements.new(self, body: statements.body << statement, location: location) - end - - # :call-seq: - # on_stmts_new: () -> Statements - def on_stmts_new - Statements.new( - self, - body: [], - location: Location.fixed(line: lineno, char: char_pos) - ) - end - - # :call-seq: - # on_string_add: ( - # String string, - # (StringEmbExpr | StringDVar | TStringContent) part - # ) -> StringContent - def on_string_add(string, part) - location = - string.parts.any? ? string.location.to(part.location) : part.location - - StringContent.new(parts: string.parts << part, location: location) - end - - # :call-seq: - # on_string_concat: ( - # (StringConcat | StringLiteral) left, - # StringLiteral right - # ) -> StringConcat - def on_string_concat(left, right) - StringConcat.new( - left: left, - right: right, - location: left.location.to(right.location) - ) - end - - # :call-seq: - # on_string_content: () -> StringContent - def on_string_content - StringContent.new( - parts: [], - location: Location.fixed(line: lineno, char: char_pos) - ) - end - - # :call-seq: - # on_string_dvar: ((Backref | VarRef) variable) -> StringDVar - def on_string_dvar(variable) - embvar = find_token(EmbVar) - - StringDVar.new( - variable: variable, - location: embvar.location.to(variable.location) - ) - end - - # :call-seq: - # on_string_embexpr: (Statements statements) -> StringEmbExpr - def on_string_embexpr(statements) - embexpr_beg = find_token(EmbExprBeg) - embexpr_end = find_token(EmbExprEnd) - - statements.bind( - embexpr_beg.location.end_char, - embexpr_end.location.start_char - ) - - location = - Location.new( - start_line: embexpr_beg.location.start_line, - start_char: embexpr_beg.location.start_char, - end_line: [ - embexpr_end.location.end_line, - statements.location.end_line - ].max, - end_char: embexpr_end.location.end_char - ) - - StringEmbExpr.new(statements: statements, location: location) - end - - # :call-seq: - # on_string_literal: (String string) -> Heredoc | StringLiteral - def on_string_literal(string) - heredoc = @heredocs[-1] - - if heredoc && heredoc.ending - heredoc = @heredocs.pop - - Heredoc.new( - beginning: heredoc.beginning, - ending: heredoc.ending, - parts: string.parts, - location: heredoc.location - ) - else - tstring_beg = find_token(TStringBeg) - tstring_end = find_token(TStringEnd, location: tstring_beg.location) - - location = - Location.new( - start_line: tstring_beg.location.start_line, - start_char: tstring_beg.location.start_char, - end_line: [ - tstring_end.location.end_line, - string.location.end_line - ].max, - end_char: tstring_end.location.end_char - ) - - StringLiteral.new( - parts: string.parts, - quote: tstring_beg.value, - location: location - ) - end - end - - # :call-seq: - # on_super: ((ArgParen | Args) arguments) -> Super - def on_super(arguments) - keyword = find_token(Kw, "super") - - Super.new( - arguments: arguments, - location: keyword.location.to(arguments.location) - ) - end - - # symbeg is a token that represents the beginning of a symbol literal. - # In most cases it will contain just ":" as in the value, but if its a dynamic - # symbol being defined it will contain ":'" or ":\"". - def on_symbeg(value) - node = - SymBeg.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - - tokens << node - node - end - - # :call-seq: - # on_symbol: ( - # (Backtick | Const | CVar | GVar | Ident | IVar | Kw | Op) value - # ) -> SymbolContent - def on_symbol(value) - tokens.delete(value) - - SymbolContent.new(value: value, location: value.location) - end - - # :call-seq: - # on_symbol_literal: ( - # ( - # Backtick | Const | CVar | GVar | Ident | - # IVar | Kw | Op | SymbolContent - # ) value - # ) -> SymbolLiteral - def on_symbol_literal(value) - if value.is_a?(SymbolContent) - symbeg = find_token(SymBeg) - - SymbolLiteral.new( - value: value.value, - location: symbeg.location.to(value.location) - ) - else - tokens.delete(value) - SymbolLiteral.new(value: value, location: value.location) - end - end - - # :call-seq: - # on_symbols_add: (Symbols symbols, Word word) -> Symbols - def on_symbols_add(symbols, word) - Symbols.new( - beginning: symbols.beginning, - elements: symbols.elements << word, - location: symbols.location.to(word.location) - ) - end - - # :call-seq: - # on_symbols_beg: (String value) -> SymbolsBeg - def on_symbols_beg(value) - node = - SymbolsBeg.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - - tokens << node - node - end - - # :call-seq: - # on_symbols_new: () -> Symbols - def on_symbols_new - beginning = find_token(SymbolsBeg) - - Symbols.new( - beginning: beginning, - elements: [], - location: beginning.location - ) - end - - # :call-seq: - # on_tlambda: (String value) -> TLambda - def on_tlambda(value) - node = - TLambda.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - - tokens << node - node - end - - # :call-seq: - # on_tlambeg: (String value) -> TLamBeg - def on_tlambeg(value) - node = - TLamBeg.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - - tokens << node - node - end - - # :call-seq: - # on_top_const_field: (Const constant) -> TopConstRef - def on_top_const_field(constant) - operator = find_colon2_before(constant) - - TopConstField.new( - constant: constant, - location: operator.location.to(constant.location) - ) - end - - # :call-seq: - # on_top_const_ref: (Const constant) -> TopConstRef - def on_top_const_ref(constant) - operator = find_colon2_before(constant) - - TopConstRef.new( - constant: constant, - location: operator.location.to(constant.location) - ) - end - - # :call-seq: - # on_tstring_beg: (String value) -> TStringBeg - def on_tstring_beg(value) - node = - TStringBeg.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - - tokens << node - node - end - - # :call-seq: - # on_tstring_content: (String value) -> TStringContent - def on_tstring_content(value) - TStringContent.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - end - - # :call-seq: - # on_tstring_end: (String value) -> TStringEnd - def on_tstring_end(value) - node = - TStringEnd.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - - tokens << node - node - end - - # :call-seq: - # on_unary: (:not operator, untyped statement) -> Not - # | (Symbol operator, untyped statement) -> Unary - def on_unary(operator, statement) - if operator == :not - # We have somewhat special handling of the not operator since if it has - # parentheses they don't get reported as a paren node for some reason. - - beginning = find_token(Kw, "not") - ending = statement - - range = beginning.location.end_char...statement.location.start_char - paren = source[range].include?("(") - - if paren - find_token(LParen) - ending = find_token(RParen) - end - - Not.new( - statement: statement, - parentheses: paren, - location: beginning.location.to(ending.location) - ) - else - # Special case instead of using find_token here. It turns out that - # if you have a range that goes from a negative number to a negative - # number then you can end up with a .. or a ... that's higher in the - # stack. So we need to explicitly disallow those operators. - index = - tokens.rindex do |token| - token.is_a?(Op) && - token.location.start_char < statement.location.start_char && - !%w[.. ...].include?(token.value) - end - - beginning = tokens.delete_at(index) - - Unary.new( - operator: operator[0], # :+@ -> "+" - statement: statement, - location: beginning.location.to(statement.location) - ) - end - end - - # :call-seq: - # on_undef: (Array[DynaSymbol | SymbolLiteral] symbols) -> Undef - def on_undef(symbols) - keyword = find_token(Kw, "undef") - - Undef.new( - symbols: symbols, - location: keyword.location.to(symbols.last.location) - ) - end - - # :call-seq: - # on_unless: ( - # untyped predicate, - # Statements statements, - # ((nil | Elsif | Else) consequent) - # ) -> Unless - def on_unless(predicate, statements, consequent) - beginning = find_token(Kw, "unless") - ending = consequent || find_token(Kw, "end") - - statements.bind(predicate.location.end_char, ending.location.start_char) - - Unless.new( - predicate: predicate, - statements: statements, - consequent: consequent, - location: beginning.location.to(ending.location) - ) - end - - # :call-seq: - # on_unless_mod: (untyped predicate, untyped statement) -> UnlessMod - def on_unless_mod(predicate, statement) - find_token(Kw, "unless") - - UnlessMod.new( - statement: statement, - predicate: predicate, - location: statement.location.to(predicate.location) - ) - end - - # :call-seq: - # on_until: (untyped predicate, Statements statements) -> Until - def on_until(predicate, statements) - beginning = find_token(Kw, "until") - ending = find_token(Kw, "end") - - # Consume the do keyword if it exists so that it doesn't get confused for - # some other block - keyword = find_token(Kw, "do", consume: false) - if keyword && keyword.location.start_char > predicate.location.end_char && - keyword.location.end_char < ending.location.start_char - tokens.delete(keyword) - end - - # Update the Statements location information - statements.bind(predicate.location.end_char, ending.location.start_char) - - Until.new( - predicate: predicate, - statements: statements, - location: beginning.location.to(ending.location) - ) - end - - # :call-seq: - # on_until_mod: (untyped predicate, untyped statement) -> UntilMod - def on_until_mod(predicate, statement) - find_token(Kw, "until") - - UntilMod.new( - statement: statement, - predicate: predicate, - location: statement.location.to(predicate.location) - ) - end - - # :call-seq: - # on_var_alias: (GVar left, (Backref | GVar) right) -> VarAlias - def on_var_alias(left, right) - keyword = find_token(Kw, "alias") - - VarAlias.new( - left: left, - right: right, - location: keyword.location.to(right.location) - ) - end - - # :call-seq: - # on_var_field: ( - # (nil | Const | CVar | GVar | Ident | IVar) value - # ) -> VarField - def on_var_field(value) - location = - if value - value.location - else - # You can hit this pattern if you're assigning to a splat using pattern - # matching syntax in Ruby 2.7+ - Location.fixed(line: lineno, char: char_pos) - end - - VarField.new(value: value, location: location) - end - - # :call-seq: - # on_var_ref: ((Const | CVar | GVar | Ident | IVar | Kw) value) -> VarRef - def on_var_ref(value) - pin = find_token(Op, "^", consume: false) - - if pin && pin.location.start_char == value.location.start_char - 1 - tokens.delete(pin) - PinnedVarRef.new(value: value, location: pin.location.to(value.location)) - else - VarRef.new(value: value, location: value.location) - end - end - - # :call-seq: - # on_vcall: (Ident ident) -> VCall - def on_vcall(ident) - VCall.new(value: ident, location: ident.location) - end - - # :call-seq: - # on_void_stmt: () -> VoidStmt - def on_void_stmt - VoidStmt.new(location: Location.fixed(line: lineno, char: char_pos)) - end - - # :call-seq: - # on_when: ( - # Args arguments, - # Statements statements, - # (nil | Else | When) consequent - # ) -> When - def on_when(arguments, statements, consequent) - beginning = find_token(Kw, "when") - ending = consequent || find_token(Kw, "end") - - statements_start = arguments - if token = find_token(Kw, "then", consume: false) - tokens.delete(token) - statements_start = token - end - - statements.bind( - find_next_statement_start(statements_start.location.end_char), - ending.location.start_char - ) - - When.new( - arguments: arguments, - statements: statements, - consequent: consequent, - location: beginning.location.to(ending.location) - ) - end - - # :call-seq: - # on_while: (untyped predicate, Statements statements) -> While - def on_while(predicate, statements) - beginning = find_token(Kw, "while") - ending = find_token(Kw, "end") - - # Consume the do keyword if it exists so that it doesn't get confused for - # some other block - keyword = find_token(Kw, "do", consume: false) - if keyword && keyword.location.start_char > predicate.location.end_char && - keyword.location.end_char < ending.location.start_char - tokens.delete(keyword) - end - - # Update the Statements location information - statements.bind(predicate.location.end_char, ending.location.start_char) - - While.new( - predicate: predicate, - statements: statements, - location: beginning.location.to(ending.location) - ) - end - - # :call-seq: - # on_while_mod: (untyped predicate, untyped statement) -> WhileMod - def on_while_mod(predicate, statement) - find_token(Kw, "while") - - WhileMod.new( - statement: statement, - predicate: predicate, - location: statement.location.to(predicate.location) - ) - end - - # :call-seq: - # on_word_add: ( - # Word word, - # (StringEmbExpr | StringDVar | TStringContent) part - # ) -> Word - def on_word_add(word, part) - location = - word.parts.empty? ? part.location : word.location.to(part.location) - - Word.new(parts: word.parts << part, location: location) - end - - # :call-seq: - # on_word_new: () -> Word - def on_word_new - Word.new(parts: [], location: Location.fixed(line: lineno, char: char_pos)) - end - - # :call-seq: - # on_words_add: (Words words, Word word) -> Words - def on_words_add(words, word) - Words.new( - beginning: words.beginning, - elements: words.elements << word, - location: words.location.to(word.location) - ) - end - - # :call-seq: - # on_words_beg: (String value) -> WordsBeg - def on_words_beg(value) - node = - WordsBeg.new( - value: value, - location: Location.token(line: lineno, char: char_pos, size: value.size) - ) - - tokens << node - node - end - - # :call-seq: - # on_words_new: () -> Words - def on_words_new - beginning = find_token(WordsBeg) - - Words.new(beginning: beginning, elements: [], location: beginning.location) - end - - # def on_words_sep(value) - # value - # end - - # :call-seq: - # on_xstring_add: ( - # XString xstring, - # (StringEmbExpr | StringDVar | TStringContent) part - # ) -> XString - def on_xstring_add(xstring, part) - XString.new( - parts: xstring.parts << part, - location: xstring.location.to(part.location) - ) - end - - # :call-seq: - # on_xstring_new: () -> XString - def on_xstring_new - heredoc = @heredocs[-1] - - location = - if heredoc && heredoc.beginning.value.include?("`") - heredoc.location - else - find_token(Backtick).location - end - - XString.new(parts: [], location: location) - end - - # :call-seq: - # on_xstring_literal: (XString xstring) -> Heredoc | XStringLiteral - def on_xstring_literal(xstring) - heredoc = @heredocs[-1] - - if heredoc && heredoc.beginning.value.include?("`") - Heredoc.new( - beginning: heredoc.beginning, - ending: heredoc.ending, - parts: xstring.parts, - location: heredoc.location - ) - else - ending = find_token(TStringEnd, location: xstring.location) - - XStringLiteral.new( - parts: xstring.parts, - location: xstring.location.to(ending.location) - ) - end - end - - # :call-seq: - # on_yield: ((Args | Paren) arguments) -> Yield - def on_yield(arguments) - keyword = find_token(Kw, "yield") - - Yield.new( - arguments: arguments, - location: keyword.location.to(arguments.location) - ) - end - - # :call-seq: - # on_yield0: () -> Yield0 - def on_yield0 - keyword = find_token(Kw, "yield") - - Yield0.new(value: keyword.value, location: keyword.location) - end - - # :call-seq: - # on_zsuper: () -> ZSuper - def on_zsuper - keyword = find_token(Kw, "super") - - ZSuper.new(value: keyword.value, location: keyword.location) - end end diff --git a/lib/syntax_tree/cli.rb b/lib/syntax_tree/cli.rb index 91fa4cb6..644c9cad 100644 --- a/lib/syntax_tree/cli.rb +++ b/lib/syntax_tree/cli.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class SyntaxTree +module SyntaxTree module CLI # A utility wrapper around colored strings in the output. class Color diff --git a/lib/syntax_tree/code_actions.rb b/lib/syntax_tree/code_actions.rb index b8f69c42..b6650cd7 100644 --- a/lib/syntax_tree/code_actions.rb +++ b/lib/syntax_tree/code_actions.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class SyntaxTree +module SyntaxTree class CodeActions class DisasmAction attr_reader :line, :node diff --git a/lib/syntax_tree/implicits.rb b/lib/syntax_tree/implicits.rb index 3f751e29..548e8480 100644 --- a/lib/syntax_tree/implicits.rb +++ b/lib/syntax_tree/implicits.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class SyntaxTree +module SyntaxTree class Implicits attr_reader :before, :after diff --git a/lib/syntax_tree/language_server.rb b/lib/syntax_tree/language_server.rb index b9dd4bcb..559247df 100644 --- a/lib/syntax_tree/language_server.rb +++ b/lib/syntax_tree/language_server.rb @@ -7,7 +7,7 @@ require_relative "code_actions" require_relative "implicits" -class SyntaxTree +module SyntaxTree class LanguageServer attr_reader :input, :output diff --git a/lib/syntax_tree/node.rb b/lib/syntax_tree/node.rb index 538c085e..ee7ecfac 100644 --- a/lib/syntax_tree/node.rb +++ b/lib/syntax_tree/node.rb @@ -1,6 +1,54 @@ # frozen_string_literal: true -class SyntaxTree < Ripper +module SyntaxTree + # Represents the location of a node in the tree from the source code. + class Location + attr_reader :start_line, :start_char, :end_line, :end_char + + def initialize(start_line:, start_char:, end_line:, end_char:) + @start_line = start_line + @start_char = start_char + @end_line = end_line + @end_char = end_char + end + + def lines + start_line..end_line + end + + def ==(other) + other.is_a?(Location) && start_line == other.start_line && + start_char == other.start_char && end_line == other.end_line && + end_char == other.end_char + end + + def to(other) + Location.new( + start_line: start_line, + start_char: start_char, + end_line: [end_line, other.end_line].max, + end_char: other.end_char + ) + end + + def to_json(*opts) + [start_line, start_char, end_line, end_char].to_json(*opts) + end + + def self.token(line:, char:, size:) + new( + start_line: line, + start_char: char, + end_line: line, + end_char: char + size + ) + end + + def self.fixed(line:, char:) + new(start_line: line, start_char: char, end_line: line, end_char: char) + end + end + # This is the parent node of all of the syntax tree nodes. It's pretty much # exclusively here to make it easier to operate with the tree in cases where # you're trying to monkey-patch or strictly type. diff --git a/lib/syntax_tree/parser.rb b/lib/syntax_tree/parser.rb new file mode 100644 index 00000000..33eeef8b --- /dev/null +++ b/lib/syntax_tree/parser.rb @@ -0,0 +1,3098 @@ +# frozen_string_literal: true + +module SyntaxTree + class Parser < Ripper + # A special parser error so that we can get nice syntax displays on the + # error message when prettier prints out the results. + class ParseError < StandardError + attr_reader :lineno, :column + + def initialize(error, lineno, column) + super(error) + @lineno = lineno + @column = column + end + end + + # Represents a line in the source. If this class is being used, it means + # that every character in the string is 1 byte in length, so we can just + # return the start of the line + the index. + class SingleByteString + attr_reader :start + + def initialize(start) + @start = start + end + + def [](byteindex) + start + byteindex + end + end + + # Represents a line in the source. If this class is being used, it means + # that there are characters in the string that are multi-byte, so we will + # build up an array of indices, such that array[byteindex] will be equal to + # the index of the character within the string. + class MultiByteString + attr_reader :start, :indices + + def initialize(start, line) + @start = start + @indices = [] + + line.each_char.with_index(start) do |char, index| + char.bytesize.times { @indices << index } + end + end + + # Technically it's possible for the column index to be a negative value if + # there's a BOM at the beginning of the file, which is the reason we need + # to compare it to 0 here. + def [](byteindex) + indices[byteindex < 0 ? 0 : byteindex] + end + end + + # [String] the source being parsed + attr_reader :source + + # [Array[ String ]] the list of lines in the source + attr_reader :lines + + # [Array[ SingleByteString | MultiByteString ]] the list of objects that + # represent the start of each line in character offsets + attr_reader :line_counts + + # [Array[ untyped ]] a running list of tokens that have been found in the + # source. This list changes a lot as certain nodes will "consume" these + # tokens to determine their bounds. + attr_reader :tokens + + # [Array[ Comment | EmbDoc ]] the list of comments that have been found + # while parsing the source. + attr_reader :comments + + def initialize(source, *) + super + + # We keep the source around so that we can refer back to it when we're + # generating the AST. Sometimes it's easier to just reference the source + # string when you want to check if it contains a certain character, for + # example. + @source = source + + # Similarly, we keep the lines of the source string around to be able to + # check if certain lines contain certain characters. For example, we'll + # use this to generate the content that goes after the __END__ keyword. + # Or we'll use this to check if a comment has other content on its line. + @lines = source.split(/\r?\n/) + + # This is the full set of comments that have been found by the parser. + # It's a running list. At the end of every block of statements, they will + # go in and attempt to grab any comments that are on their own line and + # turn them into regular statements. So at the end of parsing the only + # comments left in here will be comments on lines that also contain code. + @comments = [] + + # This is the current embdoc (comments that start with =begin and end with + # =end). Since they can't be nested, there's no need for a stack here, as + # there can only be one active. These end up getting dumped into the + # comments list before getting picked up by the statements that surround + # them. + @embdoc = nil + + # This is an optional node that can be present if the __END__ keyword is + # used in the file. In that case, this will represent the content after + # that keyword. + @__end__ = nil + + # Heredocs can actually be nested together if you're using interpolation, + # so this is a stack of heredoc nodes that are currently being created. + # When we get to the token that finishes off a heredoc node, we pop the + # top one off. If there are others surrounding it, then the body events + # will now be added to the correct nodes. + @heredocs = [] + + # This is a running list of tokens that have fired. It's useful mostly for + # maintaining location information. For example, if you're inside the + # handle of a def event, then in order to determine where the AST node + # started, you need to look backward in the tokens to find a def keyword. + # Most of the time, when a parser event consumes one of these events, it + # will be deleted from the list. So ideally, this list stays pretty short + # over the course of parsing a source string. + @tokens = [] + + # Here we're going to build up a list of SingleByteString or + # MultiByteString objects. They're each going to represent a string in the + # source. They are used by the `char_pos` method to determine where we are + # in the source string. + @line_counts = [] + last_index = 0 + + @source.lines.each do |line| + if line.size == line.bytesize + @line_counts << SingleByteString.new(last_index) + else + @line_counts << MultiByteString.new(last_index, line) + end + + last_index += line.size + end + + # Make sure line counts is filled out with the first and last line at + # minimum so that it has something to compare against if the parser is in + # a lineno=2 state for an empty file. + @line_counts << SingleByteString.new(0) if @line_counts.empty? + @line_counts << SingleByteString.new(last_index) + end + + private + + # -------------------------------------------------------------------------- + # :section: Helper methods + # The following methods are used by the ripper event handlers to either + # determine their bounds or query other nodes. + # -------------------------------------------------------------------------- + + # This represents the current place in the source string that we've gotten + # to so far. We have a memoized line_counts object that we can use to get + # the number of characters that we've had to go through to get to the + # beginning of this line, then we add the number of columns into this line + # that we've gone through. + def char_pos + line_counts[lineno - 1][column] + end + + # As we build up a list of tokens, we'll periodically need to go backwards + # and find the ones that we've already hit in order to determine the + # location information for nodes that use them. For example, if you have a + # module node then you'll look backward for a kw token to determine your + # start location. + # + # This works with nesting since we're deleting tokens from the list once + # they've been used up. For example if you had nested module declarations + # then the innermost declaration would grab the last kw node that matches + # "module" (which would happen to be the innermost keyword). Then the outer + # one would only be able to grab the first one. In this way all of the + # tokens act as their own stack. + def find_token(type, value = :any, consume: true, location: nil) + index = + tokens.rindex do |token| + token.is_a?(type) && (value == :any || (token.value == value)) + end + + if consume + # If we're expecting to be able to find a token and consume it, but + # can't actually find it, then we need to raise an error. This is + # _usually_ caused by a syntax error in the source that we're printing. + # It could also be caused by accidentally attempting to consume a token + # twice by two different parser event handlers. + unless index + token = value == :any ? type.name.split("::", 2).last : value + message = "Cannot find expected #{token}" + + if location + lineno = location.start_line + column = location.start_char - line_counts[lineno - 1].start + raise ParseError.new(message, lineno, column) + else + raise ParseError.new(message, lineno, column) + end + end + + tokens.delete_at(index) + elsif index + tokens[index] + end + end + + # A helper function to find a :: operator. We do special handling instead of + # using find_token here because we don't pop off all of the :: operators so + # you could end up getting the wrong information if you have for instance + # ::X::Y::Z. + def find_colon2_before(const) + index = + tokens.rindex do |token| + token.is_a?(Op) && token.value == "::" && + token.location.start_char < const.location.start_char + end + + tokens[index] + end + + # Finds the next position in the source string that begins a statement. This + # is used to bind statements lists and make sure they don't include a + # preceding comment. For example, we want the following comment to be + # attached to the class node and not the statement node: + # + # class Foo # :nodoc: + # ... + # end + # + # By finding the next non-space character, we can make sure that the bounds + # of the statement list are correct. + def find_next_statement_start(position) + remaining = source[position..-1] + + if remaining.sub(/\A +/, "")[0] == "#" + return position + remaining.index("\n") + end + + position + end + + # -------------------------------------------------------------------------- + # :section: Ripper event handlers + # The following methods all handle a dispatched ripper event. + # -------------------------------------------------------------------------- + + # :call-seq: + # on_BEGIN: (Statements statements) -> BEGINBlock + def on_BEGIN(statements) + lbrace = find_token(LBrace) + rbrace = find_token(RBrace) + + statements.bind( + find_next_statement_start(lbrace.location.end_char), + rbrace.location.start_char + ) + + keyword = find_token(Kw, "BEGIN") + + BEGINBlock.new( + lbrace: lbrace, + statements: statements, + location: keyword.location.to(rbrace.location) + ) + end + + # :call-seq: + # on_CHAR: (String value) -> CHAR + def on_CHAR(value) + CHAR.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) + end + + # :call-seq: + # on_END: (Statements statements) -> ENDBlock + def on_END(statements) + lbrace = find_token(LBrace) + rbrace = find_token(RBrace) + + statements.bind( + find_next_statement_start(lbrace.location.end_char), + rbrace.location.start_char + ) + + keyword = find_token(Kw, "END") + + ENDBlock.new( + lbrace: lbrace, + statements: statements, + location: keyword.location.to(rbrace.location) + ) + end + + # :call-seq: + # on___end__: (String value) -> EndContent + def on___end__(value) + @__end__ = + EndContent.new( + value: source[(char_pos + value.length)..-1], + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) + end + + # :call-seq: + # on_alias: ( + # (DynaSymbol | SymbolLiteral) left, + # (DynaSymbol | SymbolLiteral) right + # ) -> Alias + def on_alias(left, right) + keyword = find_token(Kw, "alias") + + Alias.new( + left: left, + right: right, + location: keyword.location.to(right.location) + ) + end + + # :call-seq: + # on_aref: (untyped collection, (nil | Args) index) -> ARef + def on_aref(collection, index) + find_token(LBracket) + rbracket = find_token(RBracket) + + ARef.new( + collection: collection, + index: index, + location: collection.location.to(rbracket.location) + ) + end + + # :call-seq: + # on_aref_field: ( + # untyped collection, + # (nil | Args) index + # ) -> ARefField + def on_aref_field(collection, index) + find_token(LBracket) + rbracket = find_token(RBracket) + + ARefField.new( + collection: collection, + index: index, + location: collection.location.to(rbracket.location) + ) + end + + # def on_arg_ambiguous(value) + # value + # end + + # :call-seq: + # on_arg_paren: ( + # (nil | Args | ArgsForward) arguments + # ) -> ArgParen + def on_arg_paren(arguments) + lparen = find_token(LParen) + rparen = find_token(RParen) + + # If the arguments exceed the ending of the parentheses, then we know we + # have a heredoc in the arguments, and we need to use the bounds of the + # arguments to determine how large the arg_paren is. + ending = + if arguments && arguments.location.end_line > rparen.location.end_line + arguments + else + rparen + end + + ArgParen.new( + arguments: arguments, + location: lparen.location.to(ending.location) + ) + end + + # :call-seq: + # on_args_add: (Args arguments, untyped argument) -> Args + def on_args_add(arguments, argument) + if arguments.parts.empty? + # If this is the first argument being passed into the list of arguments, + # then we're going to use the bounds of the argument to override the + # parent node's location since this will be more accurate. + Args.new(parts: [argument], location: argument.location) + else + # Otherwise we're going to update the existing list with the argument + # being added as well as the new end bounds. + Args.new( + parts: arguments.parts << argument, + location: arguments.location.to(argument.location) + ) + end + end + + # :call-seq: + # on_args_add_block: ( + # Args arguments, + # (false | untyped) block + # ) -> Args + def on_args_add_block(arguments, block) + operator = find_token(Op, "&", consume: false) + + # If we can't find the & operator, then there's no block to add to the + # list, so we're just going to return the arguments as-is. + return arguments unless operator + + # Now we know we have an & operator, so we're going to delete it from the + # list of tokens to make sure it doesn't get confused with anything else. + tokens.delete(operator) + + # Construct the location that represents the block argument. + location = operator.location + location = operator.location.to(block.location) if block + + # If there are any arguments and the operator we found from the list is + # not after them, then we're going to return the arguments as-is because + # we're looking at an & that occurs before the arguments are done. + if arguments.parts.any? && location.start_char < arguments.location.end_char + return arguments + end + + # Otherwise, we're looking at an actual block argument (with or without a + # block, which could be missing because it could be a bare & since 3.1.0). + arg_block = ArgBlock.new(value: block, location: location) + + Args.new( + parts: arguments.parts << arg_block, + location: arguments.location.to(location) + ) + end + + # :call-seq: + # on_args_add_star: (Args arguments, untyped star) -> Args + def on_args_add_star(arguments, argument) + beginning = find_token(Op, "*") + ending = argument || beginning + + location = + if arguments.parts.empty? + ending.location + else + arguments.location.to(ending.location) + end + + arg_star = + ArgStar.new( + value: argument, + location: beginning.location.to(ending.location) + ) + + Args.new(parts: arguments.parts << arg_star, location: location) + end + + # :call-seq: + # on_args_forward: () -> ArgsForward + def on_args_forward + op = find_token(Op, "...") + + ArgsForward.new(value: op.value, location: op.location) + end + + # :call-seq: + # on_args_new: () -> Args + def on_args_new + Args.new(parts: [], location: Location.fixed(line: lineno, char: char_pos)) + end + + # :call-seq: + # on_array: ((nil | Args) contents) -> + # ArrayLiteral | QSymbols | QWords | Symbols | Words + def on_array(contents) + if !contents || contents.is_a?(Args) + lbracket = find_token(LBracket) + rbracket = find_token(RBracket) + + ArrayLiteral.new( + lbracket: lbracket, + contents: contents, + location: lbracket.location.to(rbracket.location) + ) + else + tstring_end = + find_token(TStringEnd, location: contents.beginning.location) + + contents.class.new( + beginning: contents.beginning, + elements: contents.elements, + location: contents.location.to(tstring_end.location) + ) + end + end + + # :call-seq: + # on_aryptn: ( + # (nil | VarRef) constant, + # (nil | Array[untyped]) requireds, + # (nil | VarField) rest, + # (nil | Array[untyped]) posts + # ) -> AryPtn + def on_aryptn(constant, requireds, rest, posts) + parts = [constant, *requireds, rest, *posts].compact + + AryPtn.new( + constant: constant, + requireds: requireds || [], + rest: rest, + posts: posts || [], + location: parts[0].location.to(parts[-1].location) + ) + end + + # :call-seq: + # on_assign: ( + # (ARefField | ConstPathField | Field | TopConstField | VarField) target, + # untyped value + # ) -> Assign + def on_assign(target, value) + Assign.new( + target: target, + value: value, + location: target.location.to(value.location) + ) + end + + # :call-seq: + # on_assoc_new: (untyped key, untyped value) -> Assoc + def on_assoc_new(key, value) + location = key.location + location = location.to(value.location) if value + + Assoc.new(key: key, value: value, location: location) + end + + # :call-seq: + # on_assoc_splat: (untyped value) -> AssocSplat + def on_assoc_splat(value) + operator = find_token(Op, "**") + + AssocSplat.new(value: value, location: operator.location.to(value.location)) + end + + # def on_assoclist_from_args(assocs) + # assocs + # end + + # :call-seq: + # on_backref: (String value) -> Backref + def on_backref(value) + Backref.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) + end + + # :call-seq: + # on_backtick: (String value) -> Backtick + def on_backtick(value) + node = + Backtick.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) + + tokens << node + node + end + + # :call-seq: + # on_bare_assoc_hash: (Array[AssocNew | AssocSplat] assocs) -> BareAssocHash + def on_bare_assoc_hash(assocs) + BareAssocHash.new( + assocs: assocs, + location: assocs[0].location.to(assocs[-1].location) + ) + end + + # :call-seq: + # on_begin: (untyped bodystmt) -> Begin | PinnedBegin + def on_begin(bodystmt) + pin = find_token(Op, "^", consume: false) + + if pin && pin.location.start_char < bodystmt.location.start_char + tokens.delete(pin) + find_token(LParen) + + rparen = find_token(RParen) + location = pin.location.to(rparen.location) + + PinnedBegin.new(statement: bodystmt, location: location) + else + keyword = find_token(Kw, "begin") + end_char = + if bodystmt.rescue_clause || bodystmt.ensure_clause || + bodystmt.else_clause + bodystmt.location.end_char + else + find_token(Kw, "end").location.end_char + end + + bodystmt.bind(keyword.location.end_char, end_char) + location = keyword.location.to(bodystmt.location) + + Begin.new(bodystmt: bodystmt, location: location) + end + end + + # :call-seq: + # on_binary: (untyped left, (Op | Symbol) operator, untyped right) -> Binary + def on_binary(left, operator, right) + if operator.is_a?(Symbol) + # Here, we're going to search backward for the token that's between the + # two operands that matches the operator so we can delete it from the + # list. + index = + tokens.rindex do |token| + location = token.location + + token.is_a?(Op) && token.value == operator.to_s && + location.start_char > left.location.end_char && + location.end_char < right.location.start_char + end + + tokens.delete_at(index) if index + else + # On most Ruby implementations, operator is a Symbol that represents + # that operation being performed. For instance in the example `1 < 2`, + # the `operator` object would be `:<`. However, on JRuby, it's an `@op` + # node, so here we're going to explicitly convert it into the same + # normalized form. + operator = tokens.delete(operator).value + end + + Binary.new( + left: left, + operator: operator, + right: right, + location: left.location.to(right.location) + ) + end + + # :call-seq: + # on_block_var: (Params params, (nil | Array[Ident]) locals) -> BlockVar + def on_block_var(params, locals) + index = + tokens.rindex do |node| + node.is_a?(Op) && %w[| ||].include?(node.value) && + node.location.start_char < params.location.start_char + end + + beginning = tokens[index] + ending = tokens[-1] + + BlockVar.new( + params: params, + locals: locals || [], + location: beginning.location.to(ending.location) + ) + end + + # :call-seq: + # on_blockarg: (Ident name) -> BlockArg + def on_blockarg(name) + operator = find_token(Op, "&") + + location = operator.location + location = location.to(name.location) if name + + BlockArg.new(name: name, location: location) + end + + # :call-seq: + # on_bodystmt: ( + # Statements statements, + # (nil | Rescue) rescue_clause, + # (nil | Statements) else_clause, + # (nil | Ensure) ensure_clause + # ) -> BodyStmt + def on_bodystmt(statements, rescue_clause, else_clause, ensure_clause) + BodyStmt.new( + statements: statements, + rescue_clause: rescue_clause, + else_clause: else_clause, + ensure_clause: ensure_clause, + location: Location.fixed(line: lineno, char: char_pos) + ) + end + + # :call-seq: + # on_brace_block: ( + # (nil | BlockVar) block_var, + # Statements statements + # ) -> BraceBlock + def on_brace_block(block_var, statements) + lbrace = find_token(LBrace) + rbrace = find_token(RBrace) + + statements.bind( + find_next_statement_start((block_var || lbrace).location.end_char), + rbrace.location.start_char + ) + + location = + Location.new( + start_line: lbrace.location.start_line, + start_char: lbrace.location.start_char, + end_line: [rbrace.location.end_line, statements.location.end_line].max, + end_char: rbrace.location.end_char + ) + + BraceBlock.new( + lbrace: lbrace, + block_var: block_var, + statements: statements, + location: location + ) + end + + # :call-seq: + # on_break: (Args arguments) -> Break + def on_break(arguments) + keyword = find_token(Kw, "break") + + location = keyword.location + location = location.to(arguments.location) if arguments.parts.any? + + Break.new(arguments: arguments, location: location) + end + + # :call-seq: + # on_call: ( + # untyped receiver, + # (:"::" | Op | Period) operator, + # (:call | Backtick | Const | Ident | Op) message + # ) -> Call + def on_call(receiver, operator, message) + ending = message + ending = operator if message == :call + + Call.new( + receiver: receiver, + operator: operator, + message: message, + arguments: nil, + location: receiver.location.to(ending.location) + ) + end + + # :call-seq: + # on_case: (untyped value, untyped consequent) -> Case | RAssign + def on_case(value, consequent) + if keyword = find_token(Kw, "case", consume: false) + tokens.delete(keyword) + + Case.new( + keyword: keyword, + value: value, + consequent: consequent, + location: keyword.location.to(consequent.location) + ) + else + operator = find_token(Kw, "in", consume: false) || find_token(Op, "=>") + + RAssign.new( + value: value, + operator: operator, + pattern: consequent, + location: value.location.to(consequent.location) + ) + end + end + + # :call-seq: + # on_class: ( + # (ConstPathRef | ConstRef | TopConstRef) constant, + # untyped superclass, + # BodyStmt bodystmt + # ) -> ClassDeclaration + def on_class(constant, superclass, bodystmt) + beginning = find_token(Kw, "class") + ending = find_token(Kw, "end") + + bodystmt.bind( + find_next_statement_start((superclass || constant).location.end_char), + ending.location.start_char + ) + + ClassDeclaration.new( + constant: constant, + superclass: superclass, + bodystmt: bodystmt, + location: beginning.location.to(ending.location) + ) + end + + # :call-seq: + # on_comma: (String value) -> Comma + def on_comma(value) + node = + Comma.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) + + tokens << node + node + end + + # :call-seq: + # on_command: ((Const | Ident) message, Args arguments) -> Command + def on_command(message, arguments) + Command.new( + message: message, + arguments: arguments, + location: message.location.to(arguments.location) + ) + end + + # :call-seq: + # on_command_call: ( + # untyped receiver, + # (:"::" | Op | Period) operator, + # (Const | Ident | Op) message, + # (nil | Args) arguments + # ) -> CommandCall + def on_command_call(receiver, operator, message, arguments) + ending = arguments || message + + CommandCall.new( + receiver: receiver, + operator: operator, + message: message, + arguments: arguments, + location: receiver.location.to(ending.location) + ) + end + + # :call-seq: + # on_comment: (String value) -> Comment + def on_comment(value) + line = lineno + comment = + Comment.new( + value: value.chomp, + inline: value.strip != lines[line - 1].strip, + location: + Location.token(line: line, char: char_pos, size: value.size - 1) + ) + + @comments << comment + comment + end + + # :call-seq: + # on_const: (String value) -> Const + def on_const(value) + Const.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) + end + + # :call-seq: + # on_const_path_field: (untyped parent, Const constant) -> ConstPathField + def on_const_path_field(parent, constant) + ConstPathField.new( + parent: parent, + constant: constant, + location: parent.location.to(constant.location) + ) + end + + # :call-seq: + # on_const_path_ref: (untyped parent, Const constant) -> ConstPathRef + def on_const_path_ref(parent, constant) + ConstPathRef.new( + parent: parent, + constant: constant, + location: parent.location.to(constant.location) + ) + end + + # :call-seq: + # on_const_ref: (Const constant) -> ConstRef + def on_const_ref(constant) + ConstRef.new(constant: constant, location: constant.location) + end + + # :call-seq: + # on_cvar: (String value) -> CVar + def on_cvar(value) + CVar.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) + end + + # :call-seq: + # on_def: ( + # (Backtick | Const | Ident | Kw | Op) name, + # (nil | Params | Paren) params, + # untyped bodystmt + # ) -> Def | DefEndless + def on_def(name, params, bodystmt) + # Make sure to delete this token in case you're defining something like + # def class which would lead to this being a kw and causing all kinds of + # trouble + tokens.delete(name) + + # Find the beginning of the method definition, which works for single-line + # and normal method definitions. + beginning = find_token(Kw, "def") + + # If there aren't any params then we need to correct the params node + # location information + if params.is_a?(Params) && params.empty? + end_char = name.location.end_char + location = + Location.new( + start_line: params.location.start_line, + start_char: end_char, + end_line: params.location.end_line, + end_char: end_char + ) + + params = Params.new(location: location) + end + + ending = find_token(Kw, "end", consume: false) + + if ending + tokens.delete(ending) + bodystmt.bind( + find_next_statement_start(params.location.end_char), + ending.location.start_char + ) + + Def.new( + name: name, + params: params, + bodystmt: bodystmt, + location: beginning.location.to(ending.location) + ) + else + # In Ruby >= 3.1.0, this is a BodyStmt that wraps a single statement in + # the statements list. Before, it was just the individual statement. + statement = bodystmt.is_a?(BodyStmt) ? bodystmt.statements : bodystmt + + DefEndless.new( + target: nil, + operator: nil, + name: name, + paren: params, + statement: statement, + location: beginning.location.to(bodystmt.location) + ) + end + end + + # :call-seq: + # on_defined: (untyped value) -> Defined + def on_defined(value) + beginning = find_token(Kw, "defined?") + ending = value + + range = beginning.location.end_char...value.location.start_char + if source[range].include?("(") + find_token(LParen) + ending = find_token(RParen) + end + + Defined.new(value: value, location: beginning.location.to(ending.location)) + end + + # :call-seq: + # on_defs: ( + # untyped target, + # (Op | Period) operator, + # (Backtick | Const | Ident | Kw | Op) name, + # (Params | Paren) params, + # BodyStmt bodystmt + # ) -> Defs + def on_defs(target, operator, name, params, bodystmt) + # Make sure to delete this token in case you're defining something + # like def class which would lead to this being a kw and causing all kinds + # of trouble + tokens.delete(name) + + # If there aren't any params then we need to correct the params node + # location information + if params.is_a?(Params) && params.empty? + end_char = name.location.end_char + location = + Location.new( + start_line: params.location.start_line, + start_char: end_char, + end_line: params.location.end_line, + end_char: end_char + ) + + params = Params.new(location: location) + end + + beginning = find_token(Kw, "def") + ending = find_token(Kw, "end", consume: false) + + if ending + tokens.delete(ending) + bodystmt.bind( + find_next_statement_start(params.location.end_char), + ending.location.start_char + ) + + Defs.new( + target: target, + operator: operator, + name: name, + params: params, + bodystmt: bodystmt, + location: beginning.location.to(ending.location) + ) + else + # In Ruby >= 3.1.0, this is a BodyStmt that wraps a single statement in + # the statements list. Before, it was just the individual statement. + statement = bodystmt.is_a?(BodyStmt) ? bodystmt.statements : bodystmt + + DefEndless.new( + target: target, + operator: operator, + name: name, + paren: params, + statement: statement, + location: beginning.location.to(bodystmt.location) + ) + end + end + + # :call-seq: + # on_do_block: (BlockVar block_var, BodyStmt bodystmt) -> DoBlock + def on_do_block(block_var, bodystmt) + beginning = find_token(Kw, "do") + ending = find_token(Kw, "end") + + bodystmt.bind( + find_next_statement_start((block_var || beginning).location.end_char), + ending.location.start_char + ) + + DoBlock.new( + keyword: beginning, + block_var: block_var, + bodystmt: bodystmt, + location: beginning.location.to(ending.location) + ) + end + + # :call-seq: + # on_dot2: ((nil | untyped) left, (nil | untyped) right) -> Dot2 + def on_dot2(left, right) + operator = find_token(Op, "..") + + beginning = left || operator + ending = right || operator + + Dot2.new( + left: left, + right: right, + location: beginning.location.to(ending.location) + ) + end + + # :call-seq: + # on_dot3: ((nil | untyped) left, (nil | untyped) right) -> Dot3 + def on_dot3(left, right) + operator = find_token(Op, "...") + + beginning = left || operator + ending = right || operator + + Dot3.new( + left: left, + right: right, + location: beginning.location.to(ending.location) + ) + end + + # :call-seq: + # on_dyna_symbol: (StringContent string_content) -> DynaSymbol + def on_dyna_symbol(string_content) + if find_token(SymBeg, consume: false) + # A normal dynamic symbol + symbeg = find_token(SymBeg) + tstring_end = find_token(TStringEnd, location: symbeg.location) + + DynaSymbol.new( + quote: symbeg.value, + parts: string_content.parts, + location: symbeg.location.to(tstring_end.location) + ) + else + # A dynamic symbol as a hash key + tstring_beg = find_token(TStringBeg) + label_end = find_token(LabelEnd) + + DynaSymbol.new( + parts: string_content.parts, + quote: label_end.value[0], + location: tstring_beg.location.to(label_end.location) + ) + end + end + + # :call-seq: + # on_else: (Statements statements) -> Else + def on_else(statements) + beginning = find_token(Kw, "else") + + # else can either end with an end keyword (in which case we'll want to + # consume that event) or it can end with an ensure keyword (in which case + # we'll leave that to the ensure to handle). + index = + tokens.rindex do |token| + token.is_a?(Kw) && %w[end ensure].include?(token.value) + end + + node = tokens[index] + ending = node.value == "end" ? tokens.delete_at(index) : node + # ending = node + + statements.bind(beginning.location.end_char, ending.location.start_char) + + Else.new( + statements: statements, + location: beginning.location.to(ending.location) + ) + end + + # :call-seq: + # on_elsif: ( + # untyped predicate, + # Statements statements, + # (nil | Elsif | Else) consequent + # ) -> Elsif + def on_elsif(predicate, statements, consequent) + beginning = find_token(Kw, "elsif") + ending = consequent || find_token(Kw, "end") + + statements.bind(predicate.location.end_char, ending.location.start_char) + + Elsif.new( + predicate: predicate, + statements: statements, + consequent: consequent, + location: beginning.location.to(ending.location) + ) + end + + # :call-seq: + # on_embdoc: (String value) -> EmbDoc + def on_embdoc(value) + @embdoc.value << value + @embdoc + end + + # :call-seq: + # on_embdoc_beg: (String value) -> EmbDoc + def on_embdoc_beg(value) + @embdoc = + EmbDoc.new( + value: value, + location: Location.fixed(line: lineno, char: char_pos) + ) + end + + # :call-seq: + # on_embdoc_end: (String value) -> EmbDoc + def on_embdoc_end(value) + location = @embdoc.location + embdoc = + EmbDoc.new( + value: @embdoc.value << value.chomp, + location: + Location.new( + start_line: location.start_line, + start_char: location.start_char, + end_line: lineno, + end_char: char_pos + value.length - 1 + ) + ) + + @comments << embdoc + @embdoc = nil + + embdoc + end + + # :call-seq: + # on_embexpr_beg: (String value) -> EmbExprBeg + def on_embexpr_beg(value) + node = + EmbExprBeg.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) + + tokens << node + node + end + + # :call-seq: + # on_embexpr_end: (String value) -> EmbExprEnd + def on_embexpr_end(value) + node = + EmbExprEnd.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) + + tokens << node + node + end + + # :call-seq: + # on_embvar: (String value) -> EmbVar + def on_embvar(value) + node = + EmbVar.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) + + tokens << node + node + end + + # :call-seq: + # on_ensure: (Statements statements) -> Ensure + def on_ensure(statements) + keyword = find_token(Kw, "ensure") + + # We don't want to consume the :@kw event, because that would break + # def..ensure..end chains. + ending = find_token(Kw, "end", consume: false) + statements.bind( + find_next_statement_start(keyword.location.end_char), + ending.location.start_char + ) + + Ensure.new( + keyword: keyword, + statements: statements, + location: keyword.location.to(ending.location) + ) + end + + # The handler for this event accepts no parameters (though in previous + # versions of Ruby it accepted a string literal with a value of ","). + # + # :call-seq: + # on_excessed_comma: () -> ExcessedComma + def on_excessed_comma(*) + comma = find_token(Comma) + + ExcessedComma.new(value: comma.value, location: comma.location) + end + + # :call-seq: + # on_fcall: ((Const | Ident) value) -> FCall + def on_fcall(value) + FCall.new(value: value, arguments: nil, location: value.location) + end + + # :call-seq: + # on_field: ( + # untyped parent, + # (:"::" | Op | Period) operator + # (Const | Ident) name + # ) -> Field + def on_field(parent, operator, name) + Field.new( + parent: parent, + operator: operator, + name: name, + location: parent.location.to(name.location) + ) + end + + # :call-seq: + # on_float: (String value) -> FloatLiteral + def on_float(value) + FloatLiteral.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) + end + + # :call-seq: + # on_fndptn: ( + # (nil | untyped) constant, + # VarField left, + # Array[untyped] values, + # VarField right + # ) -> FndPtn + def on_fndptn(constant, left, values, right) + beginning = constant || find_token(LBracket) + ending = find_token(RBracket) + + FndPtn.new( + constant: constant, + left: left, + values: values, + right: right, + location: beginning.location.to(ending.location) + ) + end + + # :call-seq: + # on_for: ( + # (MLHS | VarField) value, + # untyped collection, + # Statements statements + # ) -> For + def on_for(index, collection, statements) + beginning = find_token(Kw, "for") + in_keyword = find_token(Kw, "in") + ending = find_token(Kw, "end") + + # Consume the do keyword if it exists so that it doesn't get confused for + # some other block + keyword = find_token(Kw, "do", consume: false) + if keyword && keyword.location.start_char > collection.location.end_char && + keyword.location.end_char < ending.location.start_char + tokens.delete(keyword) + end + + statements.bind( + (keyword || collection).location.end_char, + ending.location.start_char + ) + + if index.is_a?(MLHS) + comma_range = index.location.end_char...in_keyword.location.start_char + index.comma = true if source[comma_range].strip.start_with?(",") + end + + For.new( + index: index, + collection: collection, + statements: statements, + location: beginning.location.to(ending.location) + ) + end + + # :call-seq: + # on_gvar: (String value) -> GVar + def on_gvar(value) + GVar.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) + end + + # :call-seq: + # on_hash: ((nil | Array[AssocNew | AssocSplat]) assocs) -> HashLiteral + def on_hash(assocs) + lbrace = find_token(LBrace) + rbrace = find_token(RBrace) + + HashLiteral.new( + lbrace: lbrace, + assocs: assocs || [], + location: lbrace.location.to(rbrace.location) + ) + end + + # :call-seq: + # on_heredoc_beg: (String value) -> HeredocBeg + def on_heredoc_beg(value) + location = + Location.token(line: lineno, char: char_pos, size: value.size + 1) + + # Here we're going to artificially create an extra node type so that if + # there are comments after the declaration of a heredoc, they get printed. + beginning = HeredocBeg.new(value: value, location: location) + @heredocs << Heredoc.new(beginning: beginning, location: location) + + beginning + end + + # :call-seq: + # on_heredoc_dedent: (StringContent string, Integer width) -> Heredoc + def on_heredoc_dedent(string, width) + heredoc = @heredocs[-1] + + @heredocs[-1] = Heredoc.new( + beginning: heredoc.beginning, + ending: heredoc.ending, + parts: string.parts, + location: heredoc.location + ) + end + + # :call-seq: + # on_heredoc_end: (String value) -> Heredoc + def on_heredoc_end(value) + heredoc = @heredocs[-1] + + @heredocs[-1] = Heredoc.new( + beginning: heredoc.beginning, + ending: value.chomp, + parts: heredoc.parts, + location: + Location.new( + start_line: heredoc.location.start_line, + start_char: heredoc.location.start_char, + end_line: lineno, + end_char: char_pos + ) + ) + end + + # :call-seq: + # on_hshptn: ( + # (nil | untyped) constant, + # Array[[Label, untyped]] keywords, + # (nil | VarField) keyword_rest + # ) -> HshPtn + def on_hshptn(constant, keywords, keyword_rest) + parts = [constant, keywords, keyword_rest].flatten(2).compact + + HshPtn.new( + constant: constant, + keywords: keywords, + keyword_rest: keyword_rest, + location: parts[0].location.to(parts[-1].location) + ) + end + + # :call-seq: + # on_ident: (String value) -> Ident + def on_ident(value) + Ident.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) + end + + # :call-seq: + # on_if: ( + # untyped predicate, + # Statements statements, + # (nil | Elsif | Else) consequent + # ) -> If + def on_if(predicate, statements, consequent) + beginning = find_token(Kw, "if") + ending = consequent || find_token(Kw, "end") + + statements.bind(predicate.location.end_char, ending.location.start_char) + + If.new( + predicate: predicate, + statements: statements, + consequent: consequent, + location: beginning.location.to(ending.location) + ) + end + + # :call-seq: + # on_ifop: (untyped predicate, untyped truthy, untyped falsy) -> IfOp + def on_ifop(predicate, truthy, falsy) + IfOp.new( + predicate: predicate, + truthy: truthy, + falsy: falsy, + location: predicate.location.to(falsy.location) + ) + end + + # :call-seq: + # on_if_mod: (untyped predicate, untyped statement) -> IfMod + def on_if_mod(predicate, statement) + find_token(Kw, "if") + + IfMod.new( + statement: statement, + predicate: predicate, + location: statement.location.to(predicate.location) + ) + end + + # def on_ignored_nl(value) + # value + # end + + # def on_ignored_sp(value) + # value + # end + + # :call-seq: + # on_imaginary: (String value) -> Imaginary + def on_imaginary(value) + Imaginary.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) + end + + # :call-seq: + # on_in: (RAssign pattern, nil statements, nil consequent) -> RAssign + # | ( + # untyped pattern, + # Statements statements, + # (nil | In | Else) consequent + # ) -> In + def on_in(pattern, statements, consequent) + # Here we have a rightward assignment + return pattern unless statements + + beginning = find_token(Kw, "in") + ending = consequent || find_token(Kw, "end") + + statements_start = pattern + if token = find_token(Kw, "then", consume: false) + tokens.delete(token) + statements_start = token + end + + statements.bind( + find_next_statement_start(statements_start.location.end_char), + ending.location.start_char + ) + + In.new( + pattern: pattern, + statements: statements, + consequent: consequent, + location: beginning.location.to(ending.location) + ) + end + + # :call-seq: + # on_int: (String value) -> Int + def on_int(value) + Int.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) + end + + # :call-seq: + # on_ivar: (String value) -> IVar + def on_ivar(value) + IVar.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) + end + + # :call-seq: + # on_kw: (String value) -> Kw + def on_kw(value) + node = + Kw.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) + + tokens << node + node + end + + # :call-seq: + # on_kwrest_param: ((nil | Ident) name) -> KwRestParam + def on_kwrest_param(name) + location = find_token(Op, "**").location + location = location.to(name.location) if name + + KwRestParam.new(name: name, location: location) + end + + # :call-seq: + # on_label: (String value) -> Label + def on_label(value) + Label.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) + end + + # :call-seq: + # on_label_end: (String value) -> LabelEnd + def on_label_end(value) + node = + LabelEnd.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) + + tokens << node + node + end + + # :call-seq: + # on_lambda: ( + # (Params | Paren) params, + # (BodyStmt | Statements) statements + # ) -> Lambda + def on_lambda(params, statements) + beginning = find_token(TLambda) + + if tokens.any? { |token| + token.is_a?(TLamBeg) && + token.location.start_char > beginning.location.start_char + } + opening = find_token(TLamBeg) + closing = find_token(RBrace) + else + opening = find_token(Kw, "do") + closing = find_token(Kw, "end") + end + + statements.bind(opening.location.end_char, closing.location.start_char) + + Lambda.new( + params: params, + statements: statements, + location: beginning.location.to(closing.location) + ) + end + + # :call-seq: + # on_lbrace: (String value) -> LBrace + def on_lbrace(value) + node = + LBrace.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) + + tokens << node + node + end + + # :call-seq: + # on_lbracket: (String value) -> LBracket + def on_lbracket(value) + node = + LBracket.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) + + tokens << node + node + end + + # :call-seq: + # on_lparen: (String value) -> LParen + def on_lparen(value) + node = + LParen.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) + + tokens << node + node + end + + # def on_magic_comment(key, value) + # [key, value] + # end + + # :call-seq: + # on_massign: ((MLHS | MLHSParen) target, untyped value) -> MAssign + def on_massign(target, value) + comma_range = target.location.end_char...value.location.start_char + target.comma = true if source[comma_range].strip.start_with?(",") + + MAssign.new( + target: target, + value: value, + location: target.location.to(value.location) + ) + end + + # :call-seq: + # on_method_add_arg: ( + # (Call | FCall) call, + # (ArgParen | Args) arguments + # ) -> Call | FCall + def on_method_add_arg(call, arguments) + location = call.location + location = location.to(arguments.location) if arguments.is_a?(ArgParen) + + if call.is_a?(FCall) + FCall.new(value: call.value, arguments: arguments, location: location) + else + Call.new( + receiver: call.receiver, + operator: call.operator, + message: call.message, + arguments: arguments, + location: location + ) + end + end + + # :call-seq: + # on_method_add_block: ( + # (Call | Command | CommandCall | FCall) call, + # (BraceBlock | DoBlock) block + # ) -> MethodAddBlock + def on_method_add_block(call, block) + MethodAddBlock.new( + call: call, + block: block, + location: call.location.to(block.location) + ) + end + + # :call-seq: + # on_mlhs_add: ( + # MLHS mlhs, + # (ARefField | Field | Ident | MLHSParen | VarField) part + # ) -> MLHS + def on_mlhs_add(mlhs, part) + location = + mlhs.parts.empty? ? part.location : mlhs.location.to(part.location) + + MLHS.new(parts: mlhs.parts << part, location: location) + end + + # :call-seq: + # on_mlhs_add_post: (MLHS left, MLHS right) -> MLHS + def on_mlhs_add_post(left, right) + MLHS.new( + parts: left.parts + right.parts, + location: left.location.to(right.location) + ) + end + + # :call-seq: + # on_mlhs_add_star: ( + # MLHS mlhs, + # (nil | ARefField | Field | Ident | VarField) part + # ) -> MLHS + def on_mlhs_add_star(mlhs, part) + beginning = find_token(Op, "*") + ending = part || beginning + + location = beginning.location.to(ending.location) + arg_star = ArgStar.new(value: part, location: location) + + location = mlhs.location.to(location) unless mlhs.parts.empty? + MLHS.new(parts: mlhs.parts << arg_star, location: location) + end + + # :call-seq: + # on_mlhs_new: () -> MLHS + def on_mlhs_new + MLHS.new(parts: [], location: Location.fixed(line: lineno, char: char_pos)) + end + + # :call-seq: + # on_mlhs_paren: ((MLHS | MLHSParen) contents) -> MLHSParen + def on_mlhs_paren(contents) + lparen = find_token(LParen) + rparen = find_token(RParen) + + comma_range = lparen.location.end_char...rparen.location.start_char + contents.comma = true if source[comma_range].strip.end_with?(",") + + MLHSParen.new( + contents: contents, + location: lparen.location.to(rparen.location) + ) + end + + # :call-seq: + # on_module: ( + # (ConstPathRef | ConstRef | TopConstRef) constant, + # BodyStmt bodystmt + # ) -> ModuleDeclaration + def on_module(constant, bodystmt) + beginning = find_token(Kw, "module") + ending = find_token(Kw, "end") + + bodystmt.bind( + find_next_statement_start(constant.location.end_char), + ending.location.start_char + ) + + ModuleDeclaration.new( + constant: constant, + bodystmt: bodystmt, + location: beginning.location.to(ending.location) + ) + end + + # :call-seq: + # on_mrhs_new: () -> MRHS + def on_mrhs_new + MRHS.new(parts: [], location: Location.fixed(line: lineno, char: char_pos)) + end + + # :call-seq: + # on_mrhs_add: (MRHS mrhs, untyped part) -> MRHS + def on_mrhs_add(mrhs, part) + location = + if mrhs.parts.empty? + mrhs.location + else + mrhs.location.to(part.location) + end + + MRHS.new(parts: mrhs.parts << part, location: location) + end + + # :call-seq: + # on_mrhs_add_star: (MRHS mrhs, untyped value) -> MRHS + def on_mrhs_add_star(mrhs, value) + beginning = find_token(Op, "*") + ending = value || beginning + + arg_star = + ArgStar.new( + value: value, + location: beginning.location.to(ending.location) + ) + + location = + if mrhs.parts.empty? + arg_star.location + else + mrhs.location.to(arg_star.location) + end + + MRHS.new(parts: mrhs.parts << arg_star, location: location) + end + + # :call-seq: + # on_mrhs_new_from_args: (Args arguments) -> MRHS + def on_mrhs_new_from_args(arguments) + MRHS.new(parts: arguments.parts, location: arguments.location) + end + + # :call-seq: + # on_next: (Args arguments) -> Next + def on_next(arguments) + keyword = find_token(Kw, "next") + + location = keyword.location + location = location.to(arguments.location) if arguments.parts.any? + + Next.new(arguments: arguments, location: location) + end + + # def on_nl(value) + # value + # end + + # def on_nokw_param(value) + # value + # end + + # :call-seq: + # on_op: (String value) -> Op + def on_op(value) + node = + Op.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) + + tokens << node + node + end + + # :call-seq: + # on_opassign: ( + # (ARefField | ConstPathField | Field | TopConstField | VarField) target, + # Op operator, + # untyped value + # ) -> OpAssign + def on_opassign(target, operator, value) + OpAssign.new( + target: target, + operator: operator, + value: value, + location: target.location.to(value.location) + ) + end + + # def on_operator_ambiguous(value) + # value + # end + + # :call-seq: + # on_params: ( + # (nil | Array[Ident]) requireds, + # (nil | Array[[Ident, untyped]]) optionals, + # (nil | ArgsForward | ExcessedComma | RestParam) rest, + # (nil | Array[Ident]) posts, + # (nil | Array[[Ident, nil | untyped]]) keywords, + # (nil | :nil | ArgsForward | KwRestParam) keyword_rest, + # (nil | :& | BlockArg) block + # ) -> Params + def on_params( + requireds, + optionals, + rest, + posts, + keywords, + keyword_rest, + block + ) + parts = [ + *requireds, + *optionals&.flatten(1), + rest, + *posts, + *keywords&.flat_map { |(key, value)| [key, value || nil] }, + (keyword_rest if keyword_rest != :nil), + (block if block != :&) + ].compact + + location = + if parts.any? + parts[0].location.to(parts[-1].location) + else + Location.fixed(line: lineno, char: char_pos) + end + + Params.new( + requireds: requireds || [], + optionals: optionals || [], + rest: rest, + posts: posts || [], + keywords: keywords || [], + keyword_rest: keyword_rest, + block: (block if block != :&), + location: location + ) + end + + # :call-seq: + # on_paren: (untyped contents) -> Paren + def on_paren(contents) + lparen = find_token(LParen) + rparen = find_token(RParen) + + if contents && contents.is_a?(Params) + location = contents.location + location = + Location.new( + start_line: location.start_line, + start_char: find_next_statement_start(lparen.location.end_char), + end_line: location.end_line, + end_char: rparen.location.start_char + ) + + contents = + Params.new( + requireds: contents.requireds, + optionals: contents.optionals, + rest: contents.rest, + posts: contents.posts, + keywords: contents.keywords, + keyword_rest: contents.keyword_rest, + block: contents.block, + location: location + ) + end + + Paren.new( + lparen: lparen, + contents: contents || nil, + location: lparen.location.to(rparen.location) + ) + end + + # If we encounter a parse error, just immediately bail out so that our + # runner can catch it. + def on_parse_error(error, *) + raise ParseError.new(error, lineno, column) + end + alias on_alias_error on_parse_error + alias on_assign_error on_parse_error + alias on_class_name_error on_parse_error + alias on_param_error on_parse_error + + # :call-seq: + # on_period: (String value) -> Period + def on_period(value) + Period.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) + end + + # :call-seq: + # on_program: (Statements statements) -> Program + def on_program(statements) + location = + Location.new( + start_line: 1, + start_char: 0, + end_line: lines.length, + end_char: source.length + ) + + statements.body << @__end__ if @__end__ + statements.bind(0, source.length) + + program = Program.new(statements: statements, location: location) + attach_comments(program, @comments) + + program + end + + # Attaches comments to the nodes in the tree that most closely correspond to + # the location of the comments. + def attach_comments(program, comments) + comments.each do |comment| + preceding, enclosing, following = nearest_nodes(program, comment) + + if comment.inline? + if preceding + preceding.comments << comment + comment.trailing! + elsif following + following.comments << comment + comment.leading! + elsif enclosing + enclosing.comments << comment + else + program.comments << comment + end + else + # If a comment exists on its own line, prefer a leading comment. + if following + following.comments << comment + comment.leading! + elsif preceding + preceding.comments << comment + comment.trailing! + elsif enclosing + enclosing.comments << comment + else + program.comments << comment + end + end + end + end + + # Responsible for finding the nearest nodes to the given comment within the + # context of the given encapsulating node. + def nearest_nodes(node, comment) + comment_start = comment.location.start_char + comment_end = comment.location.end_char + + child_nodes = node.child_nodes.compact + preceding = nil + following = nil + + left = 0 + right = child_nodes.length + + # This is a custom binary search that finds the nearest nodes to the given + # comment. When it finds a node that completely encapsulates the comment, + # it recursed downward into the tree. + while left < right + middle = (left + right) / 2 + child = child_nodes[middle] + + node_start = child.location.start_char + node_end = child.location.end_char + + if node_start <= comment_start && comment_end <= node_end + # The comment is completely contained by this child node. Abandon the + # binary search at this level. + return nearest_nodes(child, comment) + end + + if node_end <= comment_start + # This child node falls completely before the comment. Because we will + # never consider this node or any nodes before it again, this node + # must be the closest preceding node we have encountered so far. + preceding = child + left = middle + 1 + next + end + + if comment_end <= node_start + # This child node falls completely after the comment. Because we will + # never consider this node or any nodes after it again, this node must + # be the closest following node we have encountered so far. + following = child + right = middle + next + end + + # This should only happen if there is a bug in this parser. + raise "Comment location overlaps with node location" + end + + [preceding, node, following] + end + + # :call-seq: + # on_qsymbols_add: (QSymbols qsymbols, TStringContent element) -> QSymbols + def on_qsymbols_add(qsymbols, element) + QSymbols.new( + beginning: qsymbols.beginning, + elements: qsymbols.elements << element, + location: qsymbols.location.to(element.location) + ) + end + + # :call-seq: + # on_qsymbols_beg: (String value) -> QSymbolsBeg + def on_qsymbols_beg(value) + node = + QSymbolsBeg.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) + + tokens << node + node + end + + # :call-seq: + # on_qsymbols_new: () -> QSymbols + def on_qsymbols_new + beginning = find_token(QSymbolsBeg) + + QSymbols.new( + beginning: beginning, + elements: [], + location: beginning.location + ) + end + + # :call-seq: + # on_qwords_add: (QWords qwords, TStringContent element) -> QWords + def on_qwords_add(qwords, element) + QWords.new( + beginning: qwords.beginning, + elements: qwords.elements << element, + location: qwords.location.to(element.location) + ) + end + + # :call-seq: + # on_qwords_beg: (String value) -> QWordsBeg + def on_qwords_beg(value) + node = + QWordsBeg.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) + + tokens << node + node + end + + # :call-seq: + # on_qwords_new: () -> QWords + def on_qwords_new + beginning = find_token(QWordsBeg) + + QWords.new(beginning: beginning, elements: [], location: beginning.location) + end + + # :call-seq: + # on_rational: (String value) -> RationalLiteral + def on_rational(value) + RationalLiteral.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) + end + + # :call-seq: + # on_rbrace: (String value) -> RBrace + def on_rbrace(value) + node = + RBrace.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) + + tokens << node + node + end + + # :call-seq: + # on_rbracket: (String value) -> RBracket + def on_rbracket(value) + node = + RBracket.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) + + tokens << node + node + end + + # :call-seq: + # on_redo: () -> Redo + def on_redo + keyword = find_token(Kw, "redo") + + Redo.new(value: keyword.value, location: keyword.location) + end + + # :call-seq: + # on_regexp_add: ( + # RegexpContent regexp_content, + # (StringDVar | StringEmbExpr | TStringContent) part + # ) -> RegexpContent + def on_regexp_add(regexp_content, part) + RegexpContent.new( + beginning: regexp_content.beginning, + parts: regexp_content.parts << part, + location: regexp_content.location.to(part.location) + ) + end + + # :call-seq: + # on_regexp_beg: (String value) -> RegexpBeg + def on_regexp_beg(value) + node = + RegexpBeg.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) + + tokens << node + node + end + + # :call-seq: + # on_regexp_end: (String value) -> RegexpEnd + def on_regexp_end(value) + RegexpEnd.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) + end + + # :call-seq: + # on_regexp_literal: ( + # RegexpContent regexp_content, + # RegexpEnd ending + # ) -> RegexpLiteral + def on_regexp_literal(regexp_content, ending) + RegexpLiteral.new( + beginning: regexp_content.beginning, + ending: ending.value, + parts: regexp_content.parts, + location: regexp_content.location.to(ending.location) + ) + end + + # :call-seq: + # on_regexp_new: () -> RegexpContent + def on_regexp_new + regexp_beg = find_token(RegexpBeg) + + RegexpContent.new( + beginning: regexp_beg.value, + parts: [], + location: regexp_beg.location + ) + end + + # :call-seq: + # on_rescue: ( + # (nil | [untyped] | MRHS | MRHSAddStar) exceptions, + # (nil | Field | VarField) variable, + # Statements statements, + # (nil | Rescue) consequent + # ) -> Rescue + def on_rescue(exceptions, variable, statements, consequent) + keyword = find_token(Kw, "rescue") + exceptions = exceptions[0] if exceptions.is_a?(Array) + + last_node = variable || exceptions || keyword + statements.bind( + find_next_statement_start(last_node.location.end_char), + char_pos + ) + + # We add an additional inner node here that ripper doesn't provide so that + # we have a nice place to attach inline comments. But we only need it if + # we have an exception or a variable that we're rescuing. + rescue_ex = + if exceptions || variable + RescueEx.new( + exceptions: exceptions, + variable: variable, + location: + Location.new( + start_line: keyword.location.start_line, + start_char: keyword.location.end_char + 1, + end_line: last_node.location.end_line, + end_char: last_node.location.end_char + ) + ) + end + + Rescue.new( + exception: rescue_ex, + statements: statements, + consequent: consequent, + location: + Location.new( + start_line: keyword.location.start_line, + start_char: keyword.location.start_char, + end_line: lineno, + end_char: char_pos + ) + ) + end + + # :call-seq: + # on_rescue_mod: (untyped statement, untyped value) -> RescueMod + def on_rescue_mod(statement, value) + find_token(Kw, "rescue") + + RescueMod.new( + statement: statement, + value: value, + location: statement.location.to(value.location) + ) + end + + # :call-seq: + # on_rest_param: ((nil | Ident) name) -> RestParam + def on_rest_param(name) + location = find_token(Op, "*").location + location = location.to(name.location) if name + + RestParam.new(name: name, location: location) + end + + # :call-seq: + # on_retry: () -> Retry + def on_retry + keyword = find_token(Kw, "retry") + + Retry.new(value: keyword.value, location: keyword.location) + end + + # :call-seq: + # on_return: (Args arguments) -> Return + def on_return(arguments) + keyword = find_token(Kw, "return") + + Return.new( + arguments: arguments, + location: keyword.location.to(arguments.location) + ) + end + + # :call-seq: + # on_return0: () -> Return0 + def on_return0 + keyword = find_token(Kw, "return") + + Return0.new(value: keyword.value, location: keyword.location) + end + + # :call-seq: + # on_rparen: (String value) -> RParen + def on_rparen(value) + node = + RParen.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) + + tokens << node + node + end + + # :call-seq: + # on_sclass: (untyped target, BodyStmt bodystmt) -> SClass + def on_sclass(target, bodystmt) + beginning = find_token(Kw, "class") + ending = find_token(Kw, "end") + + bodystmt.bind( + find_next_statement_start(target.location.end_char), + ending.location.start_char + ) + + SClass.new( + target: target, + bodystmt: bodystmt, + location: beginning.location.to(ending.location) + ) + end + + # def on_semicolon(value) + # value + # end + + # def on_sp(value) + # value + # end + + # stmts_add is a parser event that represents a single statement inside a + # list of statements within any lexical block. It accepts as arguments the + # parent stmts node as well as an stmt which can be any expression in + # Ruby. + def on_stmts_add(statements, statement) + location = + if statements.body.empty? + statement.location + else + statements.location.to(statement.location) + end + + Statements.new(self, body: statements.body << statement, location: location) + end + + # :call-seq: + # on_stmts_new: () -> Statements + def on_stmts_new + Statements.new( + self, + body: [], + location: Location.fixed(line: lineno, char: char_pos) + ) + end + + # :call-seq: + # on_string_add: ( + # String string, + # (StringEmbExpr | StringDVar | TStringContent) part + # ) -> StringContent + def on_string_add(string, part) + location = + string.parts.any? ? string.location.to(part.location) : part.location + + StringContent.new(parts: string.parts << part, location: location) + end + + # :call-seq: + # on_string_concat: ( + # (StringConcat | StringLiteral) left, + # StringLiteral right + # ) -> StringConcat + def on_string_concat(left, right) + StringConcat.new( + left: left, + right: right, + location: left.location.to(right.location) + ) + end + + # :call-seq: + # on_string_content: () -> StringContent + def on_string_content + StringContent.new( + parts: [], + location: Location.fixed(line: lineno, char: char_pos) + ) + end + + # :call-seq: + # on_string_dvar: ((Backref | VarRef) variable) -> StringDVar + def on_string_dvar(variable) + embvar = find_token(EmbVar) + + StringDVar.new( + variable: variable, + location: embvar.location.to(variable.location) + ) + end + + # :call-seq: + # on_string_embexpr: (Statements statements) -> StringEmbExpr + def on_string_embexpr(statements) + embexpr_beg = find_token(EmbExprBeg) + embexpr_end = find_token(EmbExprEnd) + + statements.bind( + embexpr_beg.location.end_char, + embexpr_end.location.start_char + ) + + location = + Location.new( + start_line: embexpr_beg.location.start_line, + start_char: embexpr_beg.location.start_char, + end_line: [ + embexpr_end.location.end_line, + statements.location.end_line + ].max, + end_char: embexpr_end.location.end_char + ) + + StringEmbExpr.new(statements: statements, location: location) + end + + # :call-seq: + # on_string_literal: (String string) -> Heredoc | StringLiteral + def on_string_literal(string) + heredoc = @heredocs[-1] + + if heredoc && heredoc.ending + heredoc = @heredocs.pop + + Heredoc.new( + beginning: heredoc.beginning, + ending: heredoc.ending, + parts: string.parts, + location: heredoc.location + ) + else + tstring_beg = find_token(TStringBeg) + tstring_end = find_token(TStringEnd, location: tstring_beg.location) + + location = + Location.new( + start_line: tstring_beg.location.start_line, + start_char: tstring_beg.location.start_char, + end_line: [ + tstring_end.location.end_line, + string.location.end_line + ].max, + end_char: tstring_end.location.end_char + ) + + StringLiteral.new( + parts: string.parts, + quote: tstring_beg.value, + location: location + ) + end + end + + # :call-seq: + # on_super: ((ArgParen | Args) arguments) -> Super + def on_super(arguments) + keyword = find_token(Kw, "super") + + Super.new( + arguments: arguments, + location: keyword.location.to(arguments.location) + ) + end + + # symbeg is a token that represents the beginning of a symbol literal. In + # most cases it will contain just ":" as in the value, but if its a dynamic + # symbol being defined it will contain ":'" or ":\"". + def on_symbeg(value) + node = + SymBeg.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) + + tokens << node + node + end + + # :call-seq: + # on_symbol: ( + # (Backtick | Const | CVar | GVar | Ident | IVar | Kw | Op) value + # ) -> SymbolContent + def on_symbol(value) + tokens.delete(value) + + SymbolContent.new(value: value, location: value.location) + end + + # :call-seq: + # on_symbol_literal: ( + # ( + # Backtick | Const | CVar | GVar | Ident | + # IVar | Kw | Op | SymbolContent + # ) value + # ) -> SymbolLiteral + def on_symbol_literal(value) + if value.is_a?(SymbolContent) + symbeg = find_token(SymBeg) + + SymbolLiteral.new( + value: value.value, + location: symbeg.location.to(value.location) + ) + else + tokens.delete(value) + SymbolLiteral.new(value: value, location: value.location) + end + end + + # :call-seq: + # on_symbols_add: (Symbols symbols, Word word) -> Symbols + def on_symbols_add(symbols, word) + Symbols.new( + beginning: symbols.beginning, + elements: symbols.elements << word, + location: symbols.location.to(word.location) + ) + end + + # :call-seq: + # on_symbols_beg: (String value) -> SymbolsBeg + def on_symbols_beg(value) + node = + SymbolsBeg.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) + + tokens << node + node + end + + # :call-seq: + # on_symbols_new: () -> Symbols + def on_symbols_new + beginning = find_token(SymbolsBeg) + + Symbols.new( + beginning: beginning, + elements: [], + location: beginning.location + ) + end + + # :call-seq: + # on_tlambda: (String value) -> TLambda + def on_tlambda(value) + node = + TLambda.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) + + tokens << node + node + end + + # :call-seq: + # on_tlambeg: (String value) -> TLamBeg + def on_tlambeg(value) + node = + TLamBeg.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) + + tokens << node + node + end + + # :call-seq: + # on_top_const_field: (Const constant) -> TopConstRef + def on_top_const_field(constant) + operator = find_colon2_before(constant) + + TopConstField.new( + constant: constant, + location: operator.location.to(constant.location) + ) + end + + # :call-seq: + # on_top_const_ref: (Const constant) -> TopConstRef + def on_top_const_ref(constant) + operator = find_colon2_before(constant) + + TopConstRef.new( + constant: constant, + location: operator.location.to(constant.location) + ) + end + + # :call-seq: + # on_tstring_beg: (String value) -> TStringBeg + def on_tstring_beg(value) + node = + TStringBeg.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) + + tokens << node + node + end + + # :call-seq: + # on_tstring_content: (String value) -> TStringContent + def on_tstring_content(value) + TStringContent.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) + end + + # :call-seq: + # on_tstring_end: (String value) -> TStringEnd + def on_tstring_end(value) + node = + TStringEnd.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) + + tokens << node + node + end + + # :call-seq: + # on_unary: (:not operator, untyped statement) -> Not + # | (Symbol operator, untyped statement) -> Unary + def on_unary(operator, statement) + if operator == :not + # We have somewhat special handling of the not operator since if it has + # parentheses they don't get reported as a paren node for some reason. + + beginning = find_token(Kw, "not") + ending = statement + + range = beginning.location.end_char...statement.location.start_char + paren = source[range].include?("(") + + if paren + find_token(LParen) + ending = find_token(RParen) + end + + Not.new( + statement: statement, + parentheses: paren, + location: beginning.location.to(ending.location) + ) + else + # Special case instead of using find_token here. It turns out that + # if you have a range that goes from a negative number to a negative + # number then you can end up with a .. or a ... that's higher in the + # stack. So we need to explicitly disallow those operators. + index = + tokens.rindex do |token| + token.is_a?(Op) && + token.location.start_char < statement.location.start_char && + !%w[.. ...].include?(token.value) + end + + beginning = tokens.delete_at(index) + + Unary.new( + operator: operator[0], # :+@ -> "+" + statement: statement, + location: beginning.location.to(statement.location) + ) + end + end + + # :call-seq: + # on_undef: (Array[DynaSymbol | SymbolLiteral] symbols) -> Undef + def on_undef(symbols) + keyword = find_token(Kw, "undef") + + Undef.new( + symbols: symbols, + location: keyword.location.to(symbols.last.location) + ) + end + + # :call-seq: + # on_unless: ( + # untyped predicate, + # Statements statements, + # ((nil | Elsif | Else) consequent) + # ) -> Unless + def on_unless(predicate, statements, consequent) + beginning = find_token(Kw, "unless") + ending = consequent || find_token(Kw, "end") + + statements.bind(predicate.location.end_char, ending.location.start_char) + + Unless.new( + predicate: predicate, + statements: statements, + consequent: consequent, + location: beginning.location.to(ending.location) + ) + end + + # :call-seq: + # on_unless_mod: (untyped predicate, untyped statement) -> UnlessMod + def on_unless_mod(predicate, statement) + find_token(Kw, "unless") + + UnlessMod.new( + statement: statement, + predicate: predicate, + location: statement.location.to(predicate.location) + ) + end + + # :call-seq: + # on_until: (untyped predicate, Statements statements) -> Until + def on_until(predicate, statements) + beginning = find_token(Kw, "until") + ending = find_token(Kw, "end") + + # Consume the do keyword if it exists so that it doesn't get confused for + # some other block + keyword = find_token(Kw, "do", consume: false) + if keyword && keyword.location.start_char > predicate.location.end_char && + keyword.location.end_char < ending.location.start_char + tokens.delete(keyword) + end + + # Update the Statements location information + statements.bind(predicate.location.end_char, ending.location.start_char) + + Until.new( + predicate: predicate, + statements: statements, + location: beginning.location.to(ending.location) + ) + end + + # :call-seq: + # on_until_mod: (untyped predicate, untyped statement) -> UntilMod + def on_until_mod(predicate, statement) + find_token(Kw, "until") + + UntilMod.new( + statement: statement, + predicate: predicate, + location: statement.location.to(predicate.location) + ) + end + + # :call-seq: + # on_var_alias: (GVar left, (Backref | GVar) right) -> VarAlias + def on_var_alias(left, right) + keyword = find_token(Kw, "alias") + + VarAlias.new( + left: left, + right: right, + location: keyword.location.to(right.location) + ) + end + + # :call-seq: + # on_var_field: ( + # (nil | Const | CVar | GVar | Ident | IVar) value + # ) -> VarField + def on_var_field(value) + location = + if value + value.location + else + # You can hit this pattern if you're assigning to a splat using + # pattern matching syntax in Ruby 2.7+ + Location.fixed(line: lineno, char: char_pos) + end + + VarField.new(value: value, location: location) + end + + # :call-seq: + # on_var_ref: ((Const | CVar | GVar | Ident | IVar | Kw) value) -> VarRef + def on_var_ref(value) + pin = find_token(Op, "^", consume: false) + + if pin && pin.location.start_char == value.location.start_char - 1 + tokens.delete(pin) + PinnedVarRef.new(value: value, location: pin.location.to(value.location)) + else + VarRef.new(value: value, location: value.location) + end + end + + # :call-seq: + # on_vcall: (Ident ident) -> VCall + def on_vcall(ident) + VCall.new(value: ident, location: ident.location) + end + + # :call-seq: + # on_void_stmt: () -> VoidStmt + def on_void_stmt + VoidStmt.new(location: Location.fixed(line: lineno, char: char_pos)) + end + + # :call-seq: + # on_when: ( + # Args arguments, + # Statements statements, + # (nil | Else | When) consequent + # ) -> When + def on_when(arguments, statements, consequent) + beginning = find_token(Kw, "when") + ending = consequent || find_token(Kw, "end") + + statements_start = arguments + if token = find_token(Kw, "then", consume: false) + tokens.delete(token) + statements_start = token + end + + statements.bind( + find_next_statement_start(statements_start.location.end_char), + ending.location.start_char + ) + + When.new( + arguments: arguments, + statements: statements, + consequent: consequent, + location: beginning.location.to(ending.location) + ) + end + + # :call-seq: + # on_while: (untyped predicate, Statements statements) -> While + def on_while(predicate, statements) + beginning = find_token(Kw, "while") + ending = find_token(Kw, "end") + + # Consume the do keyword if it exists so that it doesn't get confused for + # some other block + keyword = find_token(Kw, "do", consume: false) + if keyword && keyword.location.start_char > predicate.location.end_char && + keyword.location.end_char < ending.location.start_char + tokens.delete(keyword) + end + + # Update the Statements location information + statements.bind(predicate.location.end_char, ending.location.start_char) + + While.new( + predicate: predicate, + statements: statements, + location: beginning.location.to(ending.location) + ) + end + + # :call-seq: + # on_while_mod: (untyped predicate, untyped statement) -> WhileMod + def on_while_mod(predicate, statement) + find_token(Kw, "while") + + WhileMod.new( + statement: statement, + predicate: predicate, + location: statement.location.to(predicate.location) + ) + end + + # :call-seq: + # on_word_add: ( + # Word word, + # (StringEmbExpr | StringDVar | TStringContent) part + # ) -> Word + def on_word_add(word, part) + location = + word.parts.empty? ? part.location : word.location.to(part.location) + + Word.new(parts: word.parts << part, location: location) + end + + # :call-seq: + # on_word_new: () -> Word + def on_word_new + Word.new(parts: [], location: Location.fixed(line: lineno, char: char_pos)) + end + + # :call-seq: + # on_words_add: (Words words, Word word) -> Words + def on_words_add(words, word) + Words.new( + beginning: words.beginning, + elements: words.elements << word, + location: words.location.to(word.location) + ) + end + + # :call-seq: + # on_words_beg: (String value) -> WordsBeg + def on_words_beg(value) + node = + WordsBeg.new( + value: value, + location: Location.token(line: lineno, char: char_pos, size: value.size) + ) + + tokens << node + node + end + + # :call-seq: + # on_words_new: () -> Words + def on_words_new + beginning = find_token(WordsBeg) + + Words.new(beginning: beginning, elements: [], location: beginning.location) + end + + # def on_words_sep(value) + # value + # end + + # :call-seq: + # on_xstring_add: ( + # XString xstring, + # (StringEmbExpr | StringDVar | TStringContent) part + # ) -> XString + def on_xstring_add(xstring, part) + XString.new( + parts: xstring.parts << part, + location: xstring.location.to(part.location) + ) + end + + # :call-seq: + # on_xstring_new: () -> XString + def on_xstring_new + heredoc = @heredocs[-1] + + location = + if heredoc && heredoc.beginning.value.include?("`") + heredoc.location + else + find_token(Backtick).location + end + + XString.new(parts: [], location: location) + end + + # :call-seq: + # on_xstring_literal: (XString xstring) -> Heredoc | XStringLiteral + def on_xstring_literal(xstring) + heredoc = @heredocs[-1] + + if heredoc && heredoc.beginning.value.include?("`") + Heredoc.new( + beginning: heredoc.beginning, + ending: heredoc.ending, + parts: xstring.parts, + location: heredoc.location + ) + else + ending = find_token(TStringEnd, location: xstring.location) + + XStringLiteral.new( + parts: xstring.parts, + location: xstring.location.to(ending.location) + ) + end + end + + # :call-seq: + # on_yield: ((Args | Paren) arguments) -> Yield + def on_yield(arguments) + keyword = find_token(Kw, "yield") + + Yield.new( + arguments: arguments, + location: keyword.location.to(arguments.location) + ) + end + + # :call-seq: + # on_yield0: () -> Yield0 + def on_yield0 + keyword = find_token(Kw, "yield") + + Yield0.new(value: keyword.value, location: keyword.location) + end + + # :call-seq: + # on_zsuper: () -> ZSuper + def on_zsuper + keyword = find_token(Kw, "super") + + ZSuper.new(value: keyword.value, location: keyword.location) + end + end +end diff --git a/lib/syntax_tree/version.rb b/lib/syntax_tree/version.rb index ed245aa7..42a3beee 100644 --- a/lib/syntax_tree/version.rb +++ b/lib/syntax_tree/version.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require "ripper" - -class SyntaxTree < Ripper +module SyntaxTree VERSION = "1.2.0" end diff --git a/test/behavior_test.rb b/test/behavior_test.rb index 81308464..707cdd9b 100644 --- a/test/behavior_test.rb +++ b/test/behavior_test.rb @@ -2,7 +2,7 @@ require_relative "test_helper" -class SyntaxTree +module SyntaxTree class BehaviorTest < Minitest::Test def test_empty void_stmt = SyntaxTree.parse("").statements.body.first @@ -15,7 +15,7 @@ def test_multibyte end def test_parse_error - assert_raises(ParseError) { SyntaxTree.parse("<>") } + assert_raises(Parser::ParseError) { SyntaxTree.parse("<>") } end def test_next_statement_start diff --git a/test/formatting_test.rb b/test/formatting_test.rb index 8fcb6ae2..f7c3b3f7 100644 --- a/test/formatting_test.rb +++ b/test/formatting_test.rb @@ -2,7 +2,7 @@ require_relative "test_helper" -class SyntaxTree +module SyntaxTree class FormattingTest < Minitest::Test delimiter = /%(?: # (.+?))?\n/ diff --git a/test/idempotency_test.rb b/test/idempotency_test.rb index ba2bc1c0..fde141f5 100644 --- a/test/idempotency_test.rb +++ b/test/idempotency_test.rb @@ -3,7 +3,7 @@ return if ENV["FAST"] require_relative "test_helper" -class SyntaxTree +module SyntaxTree class IdempotencyTest < Minitest::Test Dir[File.join(RbConfig::CONFIG["libdir"], "**/*.rb")].each do |filepath| define_method(:"test_#{filepath}") do diff --git a/test/node_test.rb b/test/node_test.rb index 0622397a..1382ed79 100644 --- a/test/node_test.rb +++ b/test/node_test.rb @@ -2,7 +2,7 @@ require_relative "test_helper" -class SyntaxTree +module SyntaxTree class NodeTest < Minitest::Test def self.guard_version(version) yield if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new(version) @@ -729,7 +729,7 @@ def test_period end def test_program - parser = SyntaxTree.new("variable") + parser = SyntaxTree::Parser.new("variable") program = parser.parse refute(parser.error?) @@ -1013,7 +1013,7 @@ def assert_node(kind, type, source, at: nil) # Parse the example, get the outputted parse tree, and assert that it was # able to successfully parse. - parser = SyntaxTree.new(source) + parser = SyntaxTree::Parser.new(source) program = parser.parse refute(parser.error?) From 8e73f5036e796e665b3d32fa29c7b835b064db89 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Wed, 30 Mar 2022 22:49:22 -0400 Subject: [PATCH 21/25] Move formatter into its own file --- CHANGELOG.md | 7 ++++ lib/syntax_tree.rb | 77 +++--------------------------------- lib/syntax_tree/formatter.rb | 71 +++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 72 deletions(-) create mode 100644 lib/syntax_tree/formatter.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b049bd1..d6907ea6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a ## [Unreleased] +## [2.0.0] - 2022-03-30 + +### Changed + +- Changed `SyntaxTree` from being a class to being a module. The parser functionality is moved into `SyntaxTree::Parser`. +- There is now a parent class for all of the nodes named `SyntaxTree::Node`. + ## [1.2.0] - 2022-01-09 ### Added diff --git a/lib/syntax_tree.rb b/lib/syntax_tree.rb index cdb9bb68..14db905c 100644 --- a/lib/syntax_tree.rb +++ b/lib/syntax_tree.rb @@ -5,6 +5,7 @@ require "ripper" require "stringio" +require_relative "syntax_tree/formatter" require_relative "syntax_tree/node" require_relative "syntax_tree/parser" require_relative "syntax_tree/version" @@ -24,88 +25,20 @@ end module SyntaxTree - # A slightly enhanced PP that knows how to format recursively including - # comments. - class Formatter < PP - COMMENT_PRIORITY = 1 - HEREDOC_PRIORITY = 2 - - attr_reader :source, :stack, :quote - - def initialize(source, ...) - super(...) - - @source = source - @stack = [] - @quote = "\"" - end - - def format(node, stackable: true) - stack << node if stackable - doc = nil - - # If there are comments, then we're going to format them around the node - # so that they get printed properly. - if node.comments.any? - leading, trailing = node.comments.partition(&:leading?) - - # Print all comments that were found before the node. - leading.each do |comment| - comment.format(self) - breakable(force: true) - end - - # If the node has a stree-ignore comment right before it, then we're - # going to just print out the node as it was seen in the source. - if leading.last&.ignore? - doc = text(source[node.location.start_char...node.location.end_char]) - else - doc = node.format(self) - end - - # Print all comments that were found after the node. - trailing.each do |comment| - line_suffix(priority: COMMENT_PRIORITY) do - text(" ") - comment.format(self) - break_parent - end - end - else - doc = node.format(self) - end - - stack.pop if stackable - doc - end - - def format_each(nodes) - nodes.each { |node| format(node) } - end - - def parent - stack[-2] - end - - def parents - stack[0...-1].reverse_each - end - end - + # Parses the given source and returns the syntax tree. def self.parse(source) parser = Parser.new(source) response = parser.parse response unless parser.error? end + # Parses the given source and returns the formatted source. def self.format(source) - output = [] - - formatter = Formatter.new(source, output) + formatter = Formatter.new(source, []) parse(source).format(formatter) formatter.flush - output.join + formatter.output.join end # Returns the source from the given filepath taking into account any potential diff --git a/lib/syntax_tree/formatter.rb b/lib/syntax_tree/formatter.rb new file mode 100644 index 00000000..643030ac --- /dev/null +++ b/lib/syntax_tree/formatter.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +module SyntaxTree + # A slightly enhanced PP that knows how to format recursively including + # comments. + class Formatter < PP + COMMENT_PRIORITY = 1 + HEREDOC_PRIORITY = 2 + + attr_reader :source, :stack, :quote + + def initialize(source, ...) + super(...) + + @source = source + @stack = [] + @quote = "\"" + end + + def format(node, stackable: true) + stack << node if stackable + doc = nil + + # If there are comments, then we're going to format them around the node + # so that they get printed properly. + if node.comments.any? + leading, trailing = node.comments.partition(&:leading?) + + # Print all comments that were found before the node. + leading.each do |comment| + comment.format(self) + breakable(force: true) + end + + # If the node has a stree-ignore comment right before it, then we're + # going to just print out the node as it was seen in the source. + if leading.last&.ignore? + doc = text(source[node.location.start_char...node.location.end_char]) + else + doc = node.format(self) + end + + # Print all comments that were found after the node. + trailing.each do |comment| + line_suffix(priority: COMMENT_PRIORITY) do + text(" ") + comment.format(self) + break_parent + end + end + else + doc = node.format(self) + end + + stack.pop if stackable + doc + end + + def format_each(nodes) + nodes.each { |node| format(node) } + end + + def parent + stack[-2] + end + + def parents + stack[0...-1].reverse_each + end + end +end From fc93026e74a9194f818b9073396fb208cc3da2ed Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Wed, 30 Mar 2022 22:56:41 -0400 Subject: [PATCH 22/25] Rename implicits to inlay hints --- CHANGELOG.md | 5 + lib/syntax_tree/code_actions.rb | 89 ------------------ lib/syntax_tree/implicits.rb | 91 ------------------ lib/syntax_tree/language_server.rb | 53 ++--------- .../language_server/inlay_hints.rb | 93 +++++++++++++++++++ 5 files changed, 106 insertions(+), 225 deletions(-) delete mode 100644 lib/syntax_tree/code_actions.rb delete mode 100644 lib/syntax_tree/implicits.rb create mode 100644 lib/syntax_tree/language_server/inlay_hints.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index d6907ea6..b714e762 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,11 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a - Changed `SyntaxTree` from being a class to being a module. The parser functionality is moved into `SyntaxTree::Parser`. - There is now a parent class for all of the nodes named `SyntaxTree::Node`. +- The `Implicits` class has been renamed to `InlayHints` to match the new LSP spec. + +### Removed + +- The disassembly code action has been removed to limit the scope of this project overall. ## [1.2.0] - 2022-01-09 diff --git a/lib/syntax_tree/code_actions.rb b/lib/syntax_tree/code_actions.rb deleted file mode 100644 index b6650cd7..00000000 --- a/lib/syntax_tree/code_actions.rb +++ /dev/null @@ -1,89 +0,0 @@ -# frozen_string_literal: true - -module SyntaxTree - class CodeActions - class DisasmAction - attr_reader :line, :node - - def initialize(line, node) - @line = line - @node = node - end - - def as_json - { - title: "Disasm #{node.name.value}", - command: "syntaxTree.disasm", - arguments: [line, node.name.value] - } - end - end - - attr_reader :line, :actions - - def initialize(line) - @line = line - @actions = [] - end - - def disasm(node) - @actions << DisasmAction.new(line, node) - end - - def self.find(program, line) - code_actions = new(line) - queue = [program] - - until queue.empty? - node = queue.shift - - # If we're found a method definition that starts on the given line, then - # we're going to add the disasm code action for that node. - if [Def, DefEndless, Defs].include?(node.class) && node.location.start_line == line - code_actions.disasm(node) - end - - # Check if the node covers the given line. If it doesn't, then we just - # bail out and go to the next node in the queue. - next unless node.location.lines.cover?(line) - - # Get a list of child nodes and binary search over them to find the - # first child node that covers the given line. It's possible that there - # are multiple, but at least we'll have a handle on one of them. - child_nodes = node.child_nodes.compact - index = - child_nodes.bsearch_index do |child| - if child.location.lines.cover?(line) - 0 - else - line - child.location.start_line - end - end - - # If no valid child was found, then just continue on to the next node in - # the queue. - next unless index - - # First, we're going to go backward in our list until we find the start - # of the subset of child nodes that cover the given line. We're going to - # add each one to the queue. - current = index - 1 - while current > 0 && child_nodes[current].location.lines.cover?(line) - queue << child_nodes[current] - current -= 1 - end - - # Next, we're going to go forward in our list until we find the end of - # the subset of child nodes that cover the given line. We'll add each of - # those to the queue as well. - current = index - while current < child_nodes.length && child_nodes[current].location.lines.cover?(line) - queue << child_nodes[current] - current += 1 - end - end - - code_actions - end - end -end diff --git a/lib/syntax_tree/implicits.rb b/lib/syntax_tree/implicits.rb deleted file mode 100644 index 548e8480..00000000 --- a/lib/syntax_tree/implicits.rb +++ /dev/null @@ -1,91 +0,0 @@ -# frozen_string_literal: true - -module SyntaxTree - class Implicits - attr_reader :before, :after - - def initialize - @before = Hash.new { |hash, key| hash[key] = +"" } - @after = Hash.new { |hash, key| hash[key] = +"" } - end - - # Adds the implicitly rescued StandardError into a bare rescue clause. For - # example, - # - # begin - # rescue - # end - # - # becomes - # - # begin - # rescue StandardError - # end - # - def bare_rescue(location) - after[location.start_char + "rescue".length] << " StandardError" - end - - # Adds the implicitly referenced value (local variable or method call) that - # is added into a hash when the value of a key-value pair is omitted. For - # example, - # - # { value: } - # - # becomes - # - # { value: value } - # - def missing_hash_value(key, location) - after[location.end_char] << " #{key}" - end - - # Adds implicit parentheses around certain expressions to make it clear - # which subexpression will be evaluated first. For example, - # - # a + b * c - # - # becomes - # - # a + ₍b * c₎ - # - def precedence_parentheses(location) - before[location.start_char] << "₍" - after[location.end_char] << "₎" - end - - def self.find(program) - implicits = new - queue = [[nil, program]] - - until queue.empty? - parent_node, child_node = queue.shift - - child_node.child_nodes.each do |grand_child_node| - queue << [child_node, grand_child_node] if grand_child_node - end - - case [parent_node, child_node] - in _, Rescue[exception: nil, location:] - implicits.bare_rescue(location) - in _, Assoc[key: Label[value: key], value: nil, location:] - implicits.missing_hash_value(key[0...-1], location) - in Assign | Binary | IfOp | OpAssign, IfOp[location:] - implicits.precedence_parentheses(location) - in Assign | OpAssign, Binary[location:] - implicits.precedence_parentheses(location) - in Binary[operator: parent_oper], Binary[operator: child_oper, location:] if parent_oper != child_oper - implicits.precedence_parentheses(location) - in Binary, Unary[operator: "-", location:] - implicits.precedence_parentheses(location) - in Params, Assign[location:] - implicits.precedence_parentheses(location) - else - # do nothing - end - end - - implicits - end - end -end diff --git a/lib/syntax_tree/language_server.rb b/lib/syntax_tree/language_server.rb index 559247df..177a39a1 100644 --- a/lib/syntax_tree/language_server.rb +++ b/lib/syntax_tree/language_server.rb @@ -4,8 +4,7 @@ require "json" require "uri" -require_relative "code_actions" -require_relative "implicits" +require_relative "language_server/inlay_hints" module SyntaxTree class LanguageServer @@ -35,8 +34,6 @@ def run in { method: "shutdown" } store.clear return - in { method: "textDocument/codeAction", id:, params: { textDocument: { uri: }, range: { start: { line: } } } } - write(id: id, result: code_actions(store[uri], line + 1)) in { method: "textDocument/didChange", params: { textDocument: { uri: }, contentChanges: [{ text: }, *] } } store[uri] = text in { method: "textDocument/didOpen", params: { textDocument: { uri:, text: } } } @@ -45,10 +42,8 @@ def run store.delete(uri) in { method: "textDocument/formatting", id:, params: { textDocument: { uri: } } } write(id: id, result: [format(store[uri])]) - in { method: "syntaxTree/disasm", id:, params: { textDocument: { uri:, query: { line:, name: } } } } - write(id: id, result: disasm(store[uri], line.to_i, name)) - in { method: "syntaxTree/implicits", id:, params: { textDocument: { uri: } } } - write(id: id, result: implicits(store[uri])) + in { method: "textDocument/inlayHints", id:, params: { textDocument: { uri: } } } + write(id: id, result: inlay_hints(store[uri])) in { method: "syntaxTree/visualizing", id:, params: { textDocument: { uri: } } } output = [] PP.pp(SyntaxTree.parse(store[uri]), output) @@ -65,43 +60,11 @@ def run def capabilities { - codeActionProvider: { codeActionsKinds: ["disasm"] }, documentFormattingProvider: true, textDocumentSync: { change: 1, openClose: true } } end - def code_actions(source, line) - actions = CodeActions.find(SyntaxTree.parse(source), line).actions - log("Found #{actions.length} actions on line #{line}") - - actions.map(&:as_json) - end - - def disasm(source, line, name) - actions = CodeActions.find(SyntaxTree.parse(source), line).actions - log("Disassembling #{name.inspect} on line #{line.inspect}") - - matched = actions.detect { |action| action.is_a?(CodeActions::DisasmAction) && action.node.name.value == name } - return "Unable to find method: #{name}" unless matched - - # First, get an instruction sequence that encompasses the method that - # we're going to disassemble. It will include the method declaration, - # which will be the top instruction sequence. - location = matched.node.location - iseq = RubyVM::InstructionSequence.new(source[location.start_char...location.end_char]) - - # Next, get the first child. We do this because the parent instruction - # sequence is the method declaration, whereas the first child is the body - # of the method, which is what we're interested in. - method = nil - iseq.each_child { |child| method = child } - - # Finally, return the disassembly as a string to the server, which will - # serialize it to JSON and return it back to the client. - method.disasm - end - def format(source) { range: { @@ -116,15 +79,15 @@ def log(message) write(method: "window/logMessage", params: { type: 4, message: message }) end - def implicits(source) - implicits = Implicits.find(SyntaxTree.parse(source)) + def inlay_hints(source) + inlay_hints = InlayHints.find(SyntaxTree.parse(source)) serialize = ->(position, text) { { position: position, text: text } } { - before: implicits.before.map(&serialize), - after: implicits.after.map(&serialize) + before: inlay_hints.before.map(&serialize), + after: inlay_hints.after.map(&serialize) } - rescue ParseError + rescue Parser::ParseError end def write(value) diff --git a/lib/syntax_tree/language_server/inlay_hints.rb b/lib/syntax_tree/language_server/inlay_hints.rb new file mode 100644 index 00000000..5e43439c --- /dev/null +++ b/lib/syntax_tree/language_server/inlay_hints.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +module SyntaxTree + class LanguageServer + class InlayHints + attr_reader :before, :after + + def initialize + @before = Hash.new { |hash, key| hash[key] = +"" } + @after = Hash.new { |hash, key| hash[key] = +"" } + end + + # Adds the implicitly rescued StandardError into a bare rescue clause. For + # example, + # + # begin + # rescue + # end + # + # becomes + # + # begin + # rescue StandardError + # end + # + def bare_rescue(location) + after[location.start_char + "rescue".length] << " StandardError" + end + + # Adds the implicitly referenced value (local variable or method call) + # that is added into a hash when the value of a key-value pair is omitted. + # For example, + # + # { value: } + # + # becomes + # + # { value: value } + # + def missing_hash_value(key, location) + after[location.end_char] << " #{key}" + end + + # Adds implicit parentheses around certain expressions to make it clear + # which subexpression will be evaluated first. For example, + # + # a + b * c + # + # becomes + # + # a + ₍b * c₎ + # + def precedence_parentheses(location) + before[location.start_char] << "₍" + after[location.end_char] << "₎" + end + + def self.find(program) + inlay_hints = new + queue = [[nil, program]] + + until queue.empty? + parent_node, child_node = queue.shift + + child_node.child_nodes.each do |grand_child_node| + queue << [child_node, grand_child_node] if grand_child_node + end + + case [parent_node, child_node] + in _, Rescue[exception: nil, location:] + inlay_hints.bare_rescue(location) + in _, Assoc[key: Label[value: key], value: nil, location:] + inlay_hints.missing_hash_value(key[0...-1], location) + in Assign | Binary | IfOp | OpAssign, IfOp[location:] + inlay_hints.precedence_parentheses(location) + in Assign | OpAssign, Binary[location:] + inlay_hints.precedence_parentheses(location) + in Binary[operator: parent_oper], Binary[operator: child_oper, location:] if parent_oper != child_oper + inlay_hints.precedence_parentheses(location) + in Binary, Unary[operator: "-", location:] + inlay_hints.precedence_parentheses(location) + in Params, Assign[location:] + inlay_hints.precedence_parentheses(location) + else + # do nothing + end + end + + inlay_hints + end + end + end +end From bf0b3432a1f2649864ba777b5e3c24bd61c81461 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Wed, 30 Mar 2022 23:03:50 -0400 Subject: [PATCH 23/25] The new `SyntaxTree.register_handler` hook for plugins. --- CHANGELOG.md | 4 ++++ lib/syntax_tree/cli.rb | 43 +++++++++++++++++++++++++++--------------- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b714e762..1b2b81e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a ## [2.0.0] - 2022-03-30 +### Added + +- The new `SyntaxTree.register_handler` hook for plugins. + ### Changed - Changed `SyntaxTree` from being a class to being a module. The parser functionality is moved into `SyntaxTree::Parser`. diff --git a/lib/syntax_tree/cli.rb b/lib/syntax_tree/cli.rb index 644c9cad..f2ad252d 100644 --- a/lib/syntax_tree/cli.rb +++ b/lib/syntax_tree/cli.rb @@ -2,6 +2,17 @@ module SyntaxTree module CLI + # This holds references to objects that respond to both #parse and #format + # so that we can use them in the CLI. + HANDLERS = {} + HANDLERS.default = SyntaxTree + + # This is a hook provided so that plugins can register themselves as the + # handler for a particular file type. + def self.register_handler(extension, handler) + HANDLERS[extension] = handler + end + # A utility wrapper around colored strings in the output. class Color attr_reader :value, :code @@ -34,7 +45,7 @@ def self.yellow(value) # The parent action class for the CLI that implements the basics. class Action - def run(filepath, source) + def run(handler, filepath, source) end def success @@ -46,8 +57,8 @@ def failure # An action of the CLI that prints out the AST for the given source. class AST < Action - def run(filepath, source) - pp SyntaxTree.parse(source) + def run(handler, filepath, source) + pp handler.parse(source) end end @@ -57,8 +68,8 @@ class Check < Action class UnformattedError < StandardError end - def run(filepath, source) - raise UnformattedError if source != SyntaxTree.format(source) + def run(handler, filepath, source) + raise UnformattedError if source != handler.format(source) rescue StandardError warn("[#{Color.yellow("warn")}] #{filepath}") raise @@ -79,11 +90,11 @@ class Debug < Action class NonIdempotentFormatError < StandardError end - def run(filepath, source) + def run(handler, filepath, source) warning = "[#{Color.yellow("warn")}] #{filepath}" - formatted = SyntaxTree.format(source) + formatted = handler.format(source) - if formatted != SyntaxTree.format(formatted) + if formatted != handler.format(formatted) raise NonIdempotentFormatError end rescue StandardError @@ -102,28 +113,28 @@ def failure # An action of the CLI that prints out the doc tree IR for the given source. class Doc < Action - def run(filepath, source) + def run(handler, filepath, source) formatter = Formatter.new([]) - SyntaxTree.parse(source).format(formatter) + handler.parse(source).format(formatter) pp formatter.groups.first end end # An action of the CLI that formats the input source and prints it out. class Format < Action - def run(filepath, source) - puts SyntaxTree.format(source) + def run(handler, filepath, source) + puts handler.format(source) end end # An action of the CLI that formats the input source and writes the # formatted output back to the file. class Write < Action - def run(filepath, source) + def run(handler, filepath, source) print filepath start = Time.now - formatted = SyntaxTree.format(source) + formatted = handler.format(source) File.write(filepath, formatted) color = source == formatted ? Color.gray(filepath) : filepath @@ -214,10 +225,12 @@ def run(argv) patterns.each do |pattern| Dir.glob(pattern).each do |filepath| next unless File.file?(filepath) + + handler = HANDLERS[File.extname(filepath)] source = SyntaxTree.read(filepath) begin - action.run(filepath, source) + action.run(handler, filepath, source) rescue ParseError => error warn("Error: #{error.message}") From d0f13fdf174f5a593a414804db6c73be4316000c Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Thu, 31 Mar 2022 10:03:56 -0400 Subject: [PATCH 24/25] New --plugins option on the CLI --- CHANGELOG.md | 3 ++- lib/syntax_tree/cli.rb | 45 +++++++++++++++++++++++++++----------- lib/syntax_tree/version.rb | 2 +- 3 files changed, 35 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b2b81e0..2d8ceaa2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,12 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) a ## [Unreleased] -## [2.0.0] - 2022-03-30 +## [2.0.0] - 2022-03-31 ### Added - The new `SyntaxTree.register_handler` hook for plugins. +- The new `--plugins=` option on the CLI. ### Changed diff --git a/lib/syntax_tree/cli.rb b/lib/syntax_tree/cli.rb index f2ad252d..1f6f6468 100644 --- a/lib/syntax_tree/cli.rb +++ b/lib/syntax_tree/cli.rb @@ -150,19 +150,19 @@ def run(handler, filepath, source) # The help message displayed if the input arguments are not correctly # ordered or formatted. HELP = <<~HELP - #{Color.bold("stree ast [FILE]")} + #{Color.bold("stree ast [OPTIONS] [FILE]")} Print out the AST corresponding to the given files - #{Color.bold("stree check [FILE]")} + #{Color.bold("stree check [OPTIONS] [FILE]")} Check that the given files are formatted as syntax tree would format them - #{Color.bold("stree debug [FILE]")} + #{Color.bold("stree debug [OPTIONS] [FILE]")} Check that the given files can be formatted idempotently - #{Color.bold("stree doc [FILE]")} + #{Color.bold("stree doc [OPTIONS] [FILE]")} Print out the doc tree that would be used to format the given files - #{Color.bold("stree format [FILE]")} + #{Color.bold("stree format [OPTIONS] [FILE]")} Print out the formatted version of the given files #{Color.bold("stree help")} @@ -174,17 +174,22 @@ def run(handler, filepath, source) #{Color.bold("stree version")} Output the current version of syntax tree - #{Color.bold("stree write [FILE]")} + #{Color.bold("stree write [OPTIONS] [FILE]")} Read, format, and write back the source of the given files + + [OPTIONS] + + --plugins=... + A comma-separated list of plugins to load. HELP class << self # Run the CLI over the given array of strings that make up the arguments # passed to the invocation. def run(argv) - arg, *patterns = argv + name, *arguments = argv - case arg + case name when "help" puts HELP return 0 @@ -197,13 +202,13 @@ def run(argv) return 0 end - if patterns.empty? + if arguments.empty? warn(HELP) return 1 end action = - case arg + case name when "a", "ast" AST.new when "c", "check" @@ -221,17 +226,31 @@ def run(argv) return 1 end + # If there are any plugins specified on the command line, then load them + # by requiring them here. We do this by transforming something like + # + # stree format --plugins=haml template.haml + # + # into + # + # require "syntax_tree/haml" + # + if arguments.first.start_with?("--plugins=") + plugins = arguments.shift[/^--plugins=(.*)$/, 1] + plugins.split(",").each { |plugin| require "syntax_tree/#{plugin}" } + end + errored = false - patterns.each do |pattern| + arguments.each do |pattern| Dir.glob(pattern).each do |filepath| next unless File.file?(filepath) handler = HANDLERS[File.extname(filepath)] - source = SyntaxTree.read(filepath) + source = handler.read(filepath) begin action.run(handler, filepath, source) - rescue ParseError => error + rescue Parser::ParseError => error warn("Error: #{error.message}") if error.lineno diff --git a/lib/syntax_tree/version.rb b/lib/syntax_tree/version.rb index 42a3beee..ddee0eb0 100644 --- a/lib/syntax_tree/version.rb +++ b/lib/syntax_tree/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module SyntaxTree - VERSION = "1.2.0" + VERSION = "2.0.0" end From 50d4da202c40cc68105ed9b5b76790ff0efe8264 Mon Sep 17 00:00:00 2001 From: Kevin Newton Date: Thu, 31 Mar 2022 10:04:17 -0400 Subject: [PATCH 25/25] Bump Gemfile.lock --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index d3e0d67c..e6eceae8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - syntax_tree (1.2.0) + syntax_tree (2.0.0) GEM remote: https://rubygems.org/