summaryrefslogtreecommitdiffstats
path: root/devtools/client/debugger/src/workers/pretty-print
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /devtools/client/debugger/src/workers/pretty-print
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/debugger/src/workers/pretty-print')
-rw-r--r--devtools/client/debugger/src/workers/pretty-print/LICENSE.md23
-rw-r--r--devtools/client/debugger/src/workers/pretty-print/index.js30
-rw-r--r--devtools/client/debugger/src/workers/pretty-print/moz.build10
-rw-r--r--devtools/client/debugger/src/workers/pretty-print/pretty-fast.js1178
-rw-r--r--devtools/client/debugger/src/workers/pretty-print/tests/__snapshots__/prettyFast.spec.js.snap1974
-rw-r--r--devtools/client/debugger/src/workers/pretty-print/tests/prettyFast.spec.js434
-rw-r--r--devtools/client/debugger/src/workers/pretty-print/worker.js98
7 files changed, 3747 insertions, 0 deletions
diff --git a/devtools/client/debugger/src/workers/pretty-print/LICENSE.md b/devtools/client/debugger/src/workers/pretty-print/LICENSE.md
new file mode 100644
index 0000000000..cc8e9a752c
--- /dev/null
+++ b/devtools/client/debugger/src/workers/pretty-print/LICENSE.md
@@ -0,0 +1,23 @@
+Copyright (c) 2013, Nick Fitzgerald
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/devtools/client/debugger/src/workers/pretty-print/index.js b/devtools/client/debugger/src/workers/pretty-print/index.js
new file mode 100644
index 0000000000..4b151b39e4
--- /dev/null
+++ b/devtools/client/debugger/src/workers/pretty-print/index.js
@@ -0,0 +1,30 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+import { WorkerDispatcher } from "devtools/client/shared/worker-utils";
+
+const WORKER_URL =
+ "resource://devtools/client/debugger/dist/pretty-print-worker.js";
+
+export class PrettyPrintDispatcher extends WorkerDispatcher {
+ constructor(jestUrl) {
+ super(jestUrl || WORKER_URL);
+ }
+
+ #prettyPrintTask = this.task("prettyPrint");
+ #prettyPrintInlineScriptTask = this.task("prettyPrintInlineScript");
+ #getSourceMapForTask = this.task("getSourceMapForTask");
+
+ prettyPrint(options) {
+ return this.#prettyPrintTask(options);
+ }
+
+ prettyPrintInlineScript(options) {
+ return this.#prettyPrintInlineScriptTask(options);
+ }
+
+ getSourceMap(taskId) {
+ return this.#getSourceMapForTask(taskId);
+ }
+}
diff --git a/devtools/client/debugger/src/workers/pretty-print/moz.build b/devtools/client/debugger/src/workers/pretty-print/moz.build
new file mode 100644
index 0000000000..b7223ac81a
--- /dev/null
+++ b/devtools/client/debugger/src/workers/pretty-print/moz.build
@@ -0,0 +1,10 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DIRS += []
+
+CompiledModules(
+ "index.js",
+)
diff --git a/devtools/client/debugger/src/workers/pretty-print/pretty-fast.js b/devtools/client/debugger/src/workers/pretty-print/pretty-fast.js
new file mode 100644
index 0000000000..44b07f4eda
--- /dev/null
+++ b/devtools/client/debugger/src/workers/pretty-print/pretty-fast.js
@@ -0,0 +1,1178 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+/* eslint-disable complexity */
+
+var acorn = require("acorn");
+var sourceMap = require("source-map");
+const NEWLINE_CODE = 10;
+
+export function prettyFast(input, options) {
+ return new PrettyFast(options).getPrettifiedCodeAndSourceMap(input);
+}
+
+// If any of these tokens are seen before a "[" token, we know that "[" token
+// is the start of an array literal, rather than a property access.
+//
+// The only exception is "}", which would need to be disambiguated by
+// parsing. The majority of the time, an open bracket following a closing
+// curly is going to be an array literal, so we brush the complication under
+// the rug, and handle the ambiguity by always assuming that it will be an
+// array literal.
+const PRE_ARRAY_LITERAL_TOKENS = new Set([
+ "typeof",
+ "void",
+ "delete",
+ "case",
+ "do",
+ "=",
+ "in",
+ "of",
+ "...",
+ "{",
+ "*",
+ "/",
+ "%",
+ "else",
+ ";",
+ "++",
+ "--",
+ "+",
+ "-",
+ "~",
+ "!",
+ ":",
+ "?",
+ ">>",
+ ">>>",
+ "<<",
+ "||",
+ "&&",
+ "<",
+ ">",
+ "<=",
+ ">=",
+ "instanceof",
+ "&",
+ "^",
+ "|",
+ "==",
+ "!=",
+ "===",
+ "!==",
+ ",",
+ "}",
+]);
+
+// If any of these tokens are seen before a "{" token, we know that "{" token
+// is the start of an object literal, rather than the start of a block.
+const PRE_OBJECT_LITERAL_TOKENS = new Set([
+ "typeof",
+ "void",
+ "delete",
+ "=",
+ "in",
+ "of",
+ "...",
+ "*",
+ "/",
+ "%",
+ "++",
+ "--",
+ "+",
+ "-",
+ "~",
+ "!",
+ ">>",
+ ">>>",
+ "<<",
+ "<",
+ ">",
+ "<=",
+ ">=",
+ "instanceof",
+ "&",
+ "^",
+ "|",
+ "==",
+ "!=",
+ "===",
+ "!==",
+]);
+
+class PrettyFast {
+ /**
+ * @param {Object} options: Provides configurability of the pretty printing.
+ * @param {String} options.url: The URL string of the ugly JS code.
+ * @param {String} options.indent: The string to indent code by.
+ * @param {SourceMapGenerator} options.sourceMapGenerator: An optional sourceMapGenerator
+ * the mappings will be added to.
+ * @param {Boolean} options.prefixWithNewLine: When true, the pretty printed code will start
+ * with a line break
+ * @param {Integer} options.originalStartLine: The line the passed script starts at (1-based).
+ * This is used for inline scripts where we need to account for the lines
+ * before the script tag
+ * @param {Integer} options.originalStartColumn: The column the passed script starts at (1-based).
+ * This is used for inline scripts where we need to account for the position
+ * of the script tag within the line.
+ * @param {Integer} options.generatedStartLine: The line where the pretty printed script
+ * will start at (1-based). This is used for pretty printing HTML file,
+ * where we might have handle previous inline scripts that impact the
+ * position of this script.
+ */
+ constructor(options = {}) {
+ // The level of indents deep we are.
+ this.#indentLevel = 0;
+ this.#indentChar = options.indent;
+
+ // We will handle mappings between ugly and pretty printed code in this SourceMapGenerator.
+ this.#sourceMapGenerator =
+ options.sourceMapGenerator ||
+ new sourceMap.SourceMapGenerator({
+ file: options.url,
+ });
+
+ this.#file = options.url;
+ this.#hasOriginalStartLine = "originalStartLine" in options;
+ this.#hasOriginalStartColumn = "originalStartColumn" in options;
+ this.#hasGeneratedStartLine = "generatedStartLine" in options;
+ this.#originalStartLine = options.originalStartLine;
+ this.#originalStartColumn = options.originalStartColumn;
+ this.#generatedStartLine = options.generatedStartLine;
+ this.#prefixWithNewLine = options.prefixWithNewLine;
+ }
+
+ /* options */
+ #indentChar;
+ #indentLevel;
+ #file;
+ #hasOriginalStartLine;
+ #hasOriginalStartColumn;
+ #hasGeneratedStartLine;
+ #originalStartLine;
+ #originalStartColumn;
+ #prefixWithNewLine;
+ #generatedStartLine;
+ #sourceMapGenerator;
+
+ /* internals */
+
+ // Whether or not we added a newline on after we added the previous token.
+ #addedNewline = false;
+ // Whether or not we added a space after we added the previous token.
+ #addedSpace = false;
+ #currentCode = "";
+ #currentLine = 1;
+ #currentColumn = 0;
+ // The tokens parsed by acorn.
+ #tokenQueue;
+ // The index of the current token in this.#tokenQueue.
+ #currentTokenIndex;
+ // The previous token we added to the pretty printed code.
+ #previousToken;
+ // Stack of token types/keywords that can affect whether we want to add a
+ // newline or a space. We can make that decision based on what token type is
+ // on the top of the stack. For example, a comma in a parameter list should
+ // be followed by a space, while a comma in an object literal should be
+ // followed by a newline.
+ //
+ // Strings that go on the stack:
+ //
+ // - "{"
+ // - "{\n"
+ // - "("
+ // - "(\n"
+ // - "["
+ // - "[\n"
+ // - "do"
+ // - "?"
+ // - "switch"
+ // - "case"
+ // - "default"
+ //
+ // The difference between "[" and "[\n" (as well as "{" and "{\n", and "(" and "(\n")
+ // is that "\n" is used when we are treating (curly) brackets/parens as line delimiters
+ // and should increment and decrement the indent level when we find them.
+ // "[" can represent either a property access (e.g. `x["hi"]`), or an empty array literal
+ // "{" only represents an empty object literals
+ // "(" can represent lots of different things (wrapping expression, if/loop condition, function call, …)
+ #stack = [];
+
+ /**
+ * @param {String} input: The ugly JS code we want to pretty print.
+ * @returns {Object}
+ * An object with the following properties:
+ * - code: The pretty printed code string.
+ * - map: A SourceMapGenerator instance.
+ */
+ getPrettifiedCodeAndSourceMap(input) {
+ // Add the initial new line if needed
+ if (this.#prefixWithNewLine) {
+ this.#write("\n");
+ }
+
+ // Pass through acorn's tokenizer and append tokens and comments into a
+ // single queue to process. For example, the source file:
+ //
+ // foo
+ // // a
+ // // b
+ // bar
+ //
+ // After this process, tokenQueue has the following token stream:
+ //
+ // [ foo, '// a', '// b', bar]
+ this.#tokenQueue = this.#getTokens(input);
+
+ for (let i = 0, len = this.#tokenQueue.length; i < len; i++) {
+ this.#currentTokenIndex = i;
+ const token = this.#tokenQueue[i];
+ const nextToken = this.#tokenQueue[i + 1];
+ this.#handleToken(token, nextToken);
+
+ // Acorn's tokenizer re-uses tokens, so we have to copy the previous token on
+ // every iteration. We follow acorn's lead here, and reuse the previousToken
+ // object the same way that acorn reuses the token object. This allows us
+ // to avoid allocations and minimize GC pauses.
+ if (!this.#previousToken) {
+ this.#previousToken = { loc: { start: {}, end: {} } };
+ }
+ this.#previousToken.start = token.start;
+ this.#previousToken.end = token.end;
+ this.#previousToken.loc.start.line = token.loc.start.line;
+ this.#previousToken.loc.start.column = token.loc.start.column;
+ this.#previousToken.loc.end.line = token.loc.end.line;
+ this.#previousToken.loc.end.column = token.loc.end.column;
+ this.#previousToken.type = token.type;
+ this.#previousToken.value = token.value;
+ }
+
+ return { code: this.#currentCode, map: this.#sourceMapGenerator };
+ }
+
+ /**
+ * Write a pretty printed string to the prettified string and for tokens, add their
+ * mapping to the SourceMapGenerator.
+ *
+ * @param String str
+ * The string to be added to the result.
+ * @param Number line
+ * The line number the string came from in the ugly source.
+ * @param Number column
+ * The column number the string came from in the ugly source.
+ * @param Boolean isToken
+ * Set to true when writing tokens, so we can differentiate them from the
+ * whitespace we add.
+ */
+ #write(str, line, column, isToken) {
+ this.#currentCode += str;
+ if (isToken) {
+ this.#sourceMapGenerator.addMapping({
+ source: this.#file,
+ // We need to swap original and generated locations, as the prettified text should
+ // be seen by the sourcemap service as the "original" one.
+ generated: {
+ // originalStartLine is 1-based, and here we just want to offset by a number of
+ // lines, so we need to decrement it
+ line: this.#hasOriginalStartLine
+ ? line + (this.#originalStartLine - 1)
+ : line,
+ // We only need to adjust the column number if we're looking at the first line, to
+ // account for the html text before the opening <script> tag.
+ column:
+ line == 1 && this.#hasOriginalStartColumn
+ ? column + this.#originalStartColumn
+ : column,
+ },
+ original: {
+ // generatedStartLine is 1-based, and here we just want to offset by a number of
+ // lines, so we need to decrement it.
+ line: this.#hasGeneratedStartLine
+ ? this.#currentLine + (this.#generatedStartLine - 1)
+ : this.#currentLine,
+ column: this.#currentColumn,
+ },
+ name: null,
+ });
+ }
+
+ for (let idx = 0, length = str.length; idx < length; idx++) {
+ if (str.charCodeAt(idx) === NEWLINE_CODE) {
+ this.#currentLine++;
+ this.#currentColumn = 0;
+ } else {
+ this.#currentColumn++;
+ }
+ }
+ }
+
+ /**
+ * Add the given token to the pretty printed results.
+ *
+ * @param Object token
+ * The token to add.
+ */
+ #writeToken(token) {
+ if (token.type.label == "string") {
+ this.#write(
+ `'${sanitize(token.value)}'`,
+ token.loc.start.line,
+ token.loc.start.column,
+ true
+ );
+ } else if (token.type.label == "regexp") {
+ this.#write(
+ String(token.value.value),
+ token.loc.start.line,
+ token.loc.start.column,
+ true
+ );
+ } else {
+ let value;
+ if (token.value != null) {
+ value = token.value;
+ if (token.type.label === "privateId") {
+ value = `#${value}`;
+ }
+ } else {
+ value = token.type.label;
+ }
+ this.#write(
+ String(value),
+ token.loc.start.line,
+ token.loc.start.column,
+ true
+ );
+ }
+ }
+
+ /**
+ * Returns the tokens computed with acorn.
+ *
+ * @param String input
+ * The JS code we want the tokens of.
+ * @returns Array<Object>
+ */
+ #getTokens(input) {
+ const tokens = [];
+
+ const res = acorn.tokenizer(input, {
+ locations: true,
+ ecmaVersion: "latest",
+ onComment(block, text, start, end, startLoc, endLoc) {
+ tokens.push({
+ type: {},
+ comment: true,
+ block,
+ text,
+ loc: { start: startLoc, end: endLoc },
+ });
+ },
+ });
+
+ for (;;) {
+ const token = res.getToken();
+ tokens.push(token);
+ if (token.type.label == "eof") {
+ break;
+ }
+ }
+
+ return tokens;
+ }
+
+ /**
+ * Add the required whitespace before this token, whether that is a single
+ * space, newline, and/or the indent on fresh lines.
+ *
+ * @param Object token
+ * The token we are currently handling.
+ * @param {Object|undefined} nextToken
+ * The next token, might not exist if we're on the last token
+ */
+ #handleToken(token, nextToken) {
+ if (token.comment) {
+ let commentIndentLevel = this.#indentLevel;
+ if (this.#previousToken?.loc?.end?.line == token.loc.start.line) {
+ commentIndentLevel = 0;
+ this.#write(" ");
+ }
+ this.#addComment(
+ commentIndentLevel,
+ token.block,
+ token.text,
+ token.loc.start.line,
+ nextToken
+ );
+ return;
+ }
+
+ // Shorthand for token.type.keyword, so we don't have to repeatedly access
+ // properties.
+ const ttk = token.type.keyword;
+
+ if (ttk && this.#previousToken?.type?.label == ".") {
+ token.type = acorn.tokTypes.name;
+ }
+
+ // Shorthand for token.type.label, so we don't have to repeatedly access
+ // properties.
+ const ttl = token.type.label;
+
+ if (ttl == "eof") {
+ if (!this.#addedNewline) {
+ this.#write("\n");
+ }
+ return;
+ }
+
+ if (belongsOnStack(token)) {
+ let stackEntry;
+
+ if (isArrayLiteral(token, this.#previousToken)) {
+ // Don't add new lines for empty array literals
+ stackEntry = nextToken?.type?.label === "]" ? "[" : "[\n";
+ } else if (isObjectLiteral(token, this.#previousToken)) {
+ // Don't add new lines for empty object literals
+ stackEntry = nextToken?.type?.label === "}" ? "{" : "{\n";
+ } else if (
+ isRoundBracketStartingLongParenthesis(
+ token,
+ this.#tokenQueue,
+ this.#currentTokenIndex
+ )
+ ) {
+ stackEntry = "(\n";
+ } else if (ttl == "{") {
+ // We need to add a line break for "{" which are not empty object literals
+ stackEntry = "{\n";
+ } else {
+ stackEntry = ttl || ttk;
+ }
+
+ this.#stack.push(stackEntry);
+ }
+
+ this.#maybeDecrementIndent(token);
+ this.#prependWhiteSpace(token);
+ this.#writeToken(token);
+ this.#addedSpace = false;
+
+ // If the next token is going to be a comment starting on the same line,
+ // then no need to add a new line here
+ if (
+ !nextToken ||
+ !nextToken.comment ||
+ token.loc.end.line != nextToken.loc.start.line
+ ) {
+ this.#maybeAppendNewline(token);
+ }
+
+ this.#maybePopStack(token);
+ this.#maybeIncrementIndent(token);
+ }
+
+ /**
+ * Returns true if the given token should cause us to pop the stack.
+ */
+ #maybePopStack(token) {
+ const ttl = token.type.label;
+ const ttk = token.type.keyword;
+ const top = this.#stack.at(-1);
+
+ if (
+ ttl == "]" ||
+ ttl == ")" ||
+ ttl == "}" ||
+ (ttl == ":" && (top == "case" || top == "default" || top == "?")) ||
+ (ttk == "while" && top == "do")
+ ) {
+ this.#stack.pop();
+ if (ttl == "}" && this.#stack.at(-1) == "switch") {
+ this.#stack.pop();
+ }
+ }
+ }
+
+ #maybeIncrementIndent(token) {
+ if (
+ // Don't increment indent for empty object literals
+ (token.type.label == "{" && this.#stack.at(-1) === "{\n") ||
+ // Don't increment indent for empty array literals
+ (token.type.label == "[" && this.#stack.at(-1) === "[\n") ||
+ token.type.keyword == "switch" ||
+ (token.type.label == "(" && this.#stack.at(-1) === "(\n")
+ ) {
+ this.#indentLevel++;
+ }
+ }
+
+ #shouldDecrementIndent(token) {
+ const top = this.#stack.at(-1);
+ const ttl = token.type.label;
+ return (
+ (ttl == "}" && top == "{\n") ||
+ (ttl == "]" && top == "[\n") ||
+ (ttl == ")" && top == "(\n")
+ );
+ }
+
+ #maybeDecrementIndent(token) {
+ if (!this.#shouldDecrementIndent(token)) {
+ return;
+ }
+
+ const ttl = token.type.label;
+ this.#indentLevel--;
+ if (ttl == "}" && this.#stack.at(-2) == "switch") {
+ this.#indentLevel--;
+ }
+ }
+
+ /**
+ * Add a comment to the pretty printed code.
+ *
+ * @param Number indentLevel
+ * The number of indents deep we are (might be different from this.#indentLevel).
+ * @param Boolean block
+ * True if the comment is a multiline block style comment.
+ * @param String text
+ * The text of the comment.
+ * @param Number line
+ * The line number to comment appeared on.
+ * @param Object nextToken
+ * The next token if any.
+ */
+ #addComment(indentLevel, block, text, line, nextToken) {
+ const indentString = this.#indentChar.repeat(indentLevel);
+ const needNewLineAfter =
+ !block || !(nextToken && nextToken.loc.start.line == line);
+
+ if (block) {
+ const commentLinesText = text
+ .split(new RegExp(`/\n${indentString}/`, "g"))
+ .join(`\n${indentString}`);
+
+ this.#write(
+ `${indentString}/*${commentLinesText}*/${needNewLineAfter ? "\n" : " "}`
+ );
+ } else {
+ this.#write(`${indentString}//${text}\n`);
+ }
+
+ this.#addedNewline = needNewLineAfter;
+ this.#addedSpace = !needNewLineAfter;
+ }
+
+ /**
+ * Add the required whitespace before this token, whether that is a single
+ * space, newline, and/or the indent on fresh lines.
+ *
+ * @param Object token
+ * The token we are about to add to the pretty printed code.
+ */
+ #prependWhiteSpace(token) {
+ const ttk = token.type.keyword;
+ const ttl = token.type.label;
+ let newlineAdded = this.#addedNewline;
+ let spaceAdded = this.#addedSpace;
+ const ltt = this.#previousToken?.type?.label;
+
+ // Handle whitespace and newlines after "}" here instead of in
+ // `isLineDelimiter` because it is only a line delimiter some of the
+ // time. For example, we don't want to put "else if" on a new line after
+ // the first if's block.
+ if (this.#previousToken && ltt == "}") {
+ if (
+ (ttk == "while" && this.#stack.at(-1) == "do") ||
+ needsSpaceBeforeClosingCurlyBracket(ttk)
+ ) {
+ this.#write(" ");
+ spaceAdded = true;
+ } else if (needsLineBreakBeforeClosingCurlyBracket(ttl)) {
+ this.#write("\n");
+ newlineAdded = true;
+ }
+ }
+
+ if (
+ (ttl == ":" && this.#stack.at(-1) == "?") ||
+ (ttl == "}" && this.#stack.at(-1) == "${")
+ ) {
+ this.#write(" ");
+ spaceAdded = true;
+ }
+
+ if (this.#previousToken && ltt != "}" && ltt != "." && ttk == "else") {
+ this.#write(" ");
+ spaceAdded = true;
+ }
+
+ const ensureNewline = () => {
+ if (!newlineAdded) {
+ this.#write("\n");
+ newlineAdded = true;
+ }
+ };
+
+ if (isASI(token, this.#previousToken)) {
+ ensureNewline();
+ }
+
+ if (this.#shouldDecrementIndent(token)) {
+ ensureNewline();
+ }
+
+ if (newlineAdded) {
+ let indentLevel = this.#indentLevel;
+ if (ttk == "case" || ttk == "default") {
+ indentLevel--;
+ }
+ this.#write(this.#indentChar.repeat(indentLevel));
+ } else if (!spaceAdded && needsSpaceAfter(token, this.#previousToken)) {
+ this.#write(" ");
+ spaceAdded = true;
+ }
+ }
+
+ /**
+ * Append the necessary whitespace to the result after we have added the given
+ * token.
+ *
+ * @param Object token
+ * The token that was just added to the result.
+ */
+ #maybeAppendNewline(token) {
+ if (!isLineDelimiter(token, this.#stack)) {
+ this.#addedNewline = false;
+ return;
+ }
+
+ this.#write("\n");
+ this.#addedNewline = true;
+ }
+}
+
+/**
+ * Determines if we think that the given token starts an array literal.
+ *
+ * @param Object token
+ * The token we want to determine if it is an array literal.
+ * @param Object previousToken
+ * The previous token we added to the pretty printed results.
+ *
+ * @returns Boolean
+ * True if we believe it is an array literal, false otherwise.
+ */
+function isArrayLiteral(token, previousToken) {
+ if (token.type.label != "[") {
+ return false;
+ }
+ if (!previousToken) {
+ return true;
+ }
+ if (previousToken.type.isAssign) {
+ return true;
+ }
+
+ return PRE_ARRAY_LITERAL_TOKENS.has(
+ previousToken.type.keyword ||
+ // Some tokens ('of', 'yield', …) have a `token.type.keyword` of 'name' and their
+ // actual value in `token.value`
+ (previousToken.type.label == "name"
+ ? previousToken.value
+ : previousToken.type.label)
+ );
+}
+
+/**
+ * Determines if we think that the given token starts an object literal.
+ *
+ * @param Object token
+ * The token we want to determine if it is an object literal.
+ * @param Object previousToken
+ * The previous token we added to the pretty printed results.
+ *
+ * @returns Boolean
+ * True if we believe it is an object literal, false otherwise.
+ */
+function isObjectLiteral(token, previousToken) {
+ if (token.type.label != "{") {
+ return false;
+ }
+ if (!previousToken) {
+ return false;
+ }
+ if (previousToken.type.isAssign) {
+ return true;
+ }
+ return PRE_OBJECT_LITERAL_TOKENS.has(
+ previousToken.type.keyword || previousToken.type.label
+ );
+}
+
+/**
+ * Determines if we think that the given token starts a long parenthesis
+ *
+ * @param {Object} token
+ * The token we want to determine if it is the beginning of a long paren.
+ * @param {Array<Object>} tokenQueue
+ * The whole list of tokens parsed by acorn
+ * @param {Integer} currentTokenIndex
+ * The index of `token` in `tokenQueue`
+ * @returns
+ */
+function isRoundBracketStartingLongParenthesis(
+ token,
+ tokenQueue,
+ currentTokenIndex
+) {
+ if (token.type.label !== "(") {
+ return false;
+ }
+
+ // If we're just wrapping an object, we'll have a new line right after
+ if (tokenQueue[currentTokenIndex + 1].type.label == "{") {
+ return false;
+ }
+
+ // We're going to iterate through the following tokens until :
+ // - we find the closing parent
+ // - or we reached the maximum character we think should be in parenthesis
+ const longParentContentLength = 60;
+
+ // Keep track of other parens so we know when we get the closing one for `token`
+ let parenCount = 0;
+ let parenContentLength = 0;
+ for (let i = currentTokenIndex + 1, len = tokenQueue.length; i < len; i++) {
+ const currToken = tokenQueue[i];
+ const ttl = currToken.type.label;
+
+ if (ttl == "(") {
+ parenCount++;
+ } else if (ttl == ")") {
+ if (parenCount == 0) {
+ // Matching closing paren, if we got here, we didn't reach the length limit,
+ // as we return when parenContentLength is greater than the limit.
+ return false;
+ }
+ parenCount--;
+ }
+
+ // Aside block comments, all tokens start and end location are on the same line, so
+ // we can use `start` and `end` to deduce the token length.
+ const tokenLength = currToken.comment
+ ? currToken.text.length
+ : currToken.end - currToken.start;
+ parenContentLength += tokenLength;
+
+ // If we didn't find the matching closing paren yet and the characters from the
+ // tokens we evaluated so far are longer than the limit, so consider the token
+ // a long paren.
+ if (parenContentLength > longParentContentLength) {
+ return true;
+ }
+ }
+
+ // if we get to here, we didn't found a closing paren, which shouldn't happen
+ // (scripts with syntax error are not displayed in the debugger), but just to
+ // be safe, return false.
+ return false;
+}
+
+// If any of these tokens are followed by a token on a new line, we know that
+// ASI cannot happen.
+const PREVENT_ASI_AFTER_TOKENS = new Set([
+ // Binary operators
+ "*",
+ "/",
+ "%",
+ "+",
+ "-",
+ "<<",
+ ">>",
+ ">>>",
+ "<",
+ ">",
+ "<=",
+ ">=",
+ "instanceof",
+ "in",
+ "==",
+ "!=",
+ "===",
+ "!==",
+ "&",
+ "^",
+ "|",
+ "&&",
+ "||",
+ ",",
+ ".",
+ "=",
+ "*=",
+ "/=",
+ "%=",
+ "+=",
+ "-=",
+ "<<=",
+ ">>=",
+ ">>>=",
+ "&=",
+ "^=",
+ "|=",
+ // Unary operators
+ "delete",
+ "void",
+ "typeof",
+ "~",
+ "!",
+ "new",
+ // Function calls and grouped expressions
+ "(",
+]);
+
+// If any of these tokens are on a line after the token before it, we know
+// that ASI cannot happen.
+const PREVENT_ASI_BEFORE_TOKENS = new Set([
+ // Binary operators
+ "*",
+ "/",
+ "%",
+ "<<",
+ ">>",
+ ">>>",
+ "<",
+ ">",
+ "<=",
+ ">=",
+ "instanceof",
+ "in",
+ "==",
+ "!=",
+ "===",
+ "!==",
+ "&",
+ "^",
+ "|",
+ "&&",
+ "||",
+ ",",
+ ".",
+ "=",
+ "*=",
+ "/=",
+ "%=",
+ "+=",
+ "-=",
+ "<<=",
+ ">>=",
+ ">>>=",
+ "&=",
+ "^=",
+ "|=",
+ // Function calls
+ "(",
+]);
+
+/**
+ * Determine if a token can look like an identifier. More precisely,
+ * this determines if the token may end or start with a character from
+ * [A-Za-z0-9_].
+ *
+ * @param Object token
+ * The token we are looking at.
+ *
+ * @returns Boolean
+ * True if identifier-like.
+ */
+function isIdentifierLike(token) {
+ const ttl = token.type.label;
+ return (
+ ttl == "name" || ttl == "num" || ttl == "privateId" || !!token.type.keyword
+ );
+}
+
+/**
+ * Determines if Automatic Semicolon Insertion (ASI) occurs between these
+ * tokens.
+ *
+ * @param Object token
+ * The current token.
+ * @param Object previousToken
+ * The previous token we added to the pretty printed results.
+ *
+ * @returns Boolean
+ * True if we believe ASI occurs.
+ */
+function isASI(token, previousToken) {
+ if (!previousToken) {
+ return false;
+ }
+ if (token.loc.start.line === previousToken.loc.start.line) {
+ return false;
+ }
+ if (
+ previousToken.type.keyword == "return" ||
+ previousToken.type.keyword == "yield" ||
+ (previousToken.type.label == "name" && previousToken.value == "yield")
+ ) {
+ return true;
+ }
+ if (
+ PREVENT_ASI_AFTER_TOKENS.has(
+ previousToken.type.label || previousToken.type.keyword
+ )
+ ) {
+ return false;
+ }
+ if (PREVENT_ASI_BEFORE_TOKENS.has(token.type.label || token.type.keyword)) {
+ return false;
+ }
+ return true;
+}
+
+/**
+ * Determine if we should add a newline after the given token.
+ *
+ * @param Object token
+ * The token we are looking at.
+ * @param Array stack
+ * The stack of open parens/curlies/brackets/etc.
+ *
+ * @returns Boolean
+ * True if we should add a newline.
+ */
+function isLineDelimiter(token, stack) {
+ const ttl = token.type.label;
+ const top = stack.at(-1);
+ return (
+ (ttl == ";" && top != "(") ||
+ // Don't add a new line for empty object literals
+ (ttl == "{" && top == "{\n") ||
+ // Don't add a new line for empty array literals
+ (ttl == "[" && top == "[\n") ||
+ ((ttl == "," || ttl == "||" || ttl == "&&") && top != "(") ||
+ (ttl == ":" && (top == "case" || top == "default")) ||
+ (ttl == "(" && top == "(\n")
+ );
+}
+
+/**
+ * Determines if we need to add a space after the token we are about to add.
+ *
+ * @param Object token
+ * The token we are about to add to the pretty printed code.
+ * @param Object [previousToken]
+ * Optional previous token added to the pretty printed code.
+ */
+function needsSpaceAfter(token, previousToken) {
+ if (previousToken && needsSpaceBetweenTokens(token, previousToken)) {
+ return true;
+ }
+
+ if (token.type.isAssign) {
+ return true;
+ }
+ if (token.type.binop != null && previousToken) {
+ return true;
+ }
+ if (token.type.label == "?") {
+ return true;
+ }
+ if (token.type.label == "=>") {
+ return true;
+ }
+
+ return false;
+}
+
+function needsSpaceBeforePreviousToken(previousToken) {
+ if (previousToken.type.isLoop) {
+ return true;
+ }
+ if (previousToken.type.isAssign) {
+ return true;
+ }
+ if (previousToken.type.binop != null) {
+ return true;
+ }
+ if (previousToken.value == "of") {
+ return true;
+ }
+
+ const previousTokenTypeLabel = previousToken.type.label;
+ if (previousTokenTypeLabel == "?") {
+ return true;
+ }
+ if (previousTokenTypeLabel == ":") {
+ return true;
+ }
+ if (previousTokenTypeLabel == ",") {
+ return true;
+ }
+ if (previousTokenTypeLabel == ";") {
+ return true;
+ }
+ if (previousTokenTypeLabel == "${") {
+ return true;
+ }
+ if (previousTokenTypeLabel == "=>") {
+ return true;
+ }
+ return false;
+}
+
+function isBreakContinueOrReturnStatement(previousTokenKeyword) {
+ return (
+ previousTokenKeyword == "break" ||
+ previousTokenKeyword == "continue" ||
+ previousTokenKeyword == "return"
+ );
+}
+
+function needsSpaceBeforePreviousTokenKeywordAfterNotDot(previousTokenKeyword) {
+ return (
+ previousTokenKeyword != "debugger" &&
+ previousTokenKeyword != "null" &&
+ previousTokenKeyword != "true" &&
+ previousTokenKeyword != "false" &&
+ previousTokenKeyword != "this" &&
+ previousTokenKeyword != "default"
+ );
+}
+
+function needsSpaceBeforeClosingParen(tokenTypeLabel) {
+ return (
+ tokenTypeLabel != ")" &&
+ tokenTypeLabel != "]" &&
+ tokenTypeLabel != ";" &&
+ tokenTypeLabel != "," &&
+ tokenTypeLabel != "."
+ );
+}
+
+/**
+ * Determines if we need to add a space between the previous token we added and
+ * the token we are about to add.
+ *
+ * @param Object token
+ * The token we are about to add to the pretty printed code.
+ * @param Object previousToken
+ * The previous token added to the pretty printed code.
+ */
+function needsSpaceBetweenTokens(token, previousToken) {
+ if (needsSpaceBeforePreviousToken(previousToken)) {
+ return true;
+ }
+
+ const ltt = previousToken.type.label;
+ if (ltt == "num" && token.type.label == ".") {
+ return true;
+ }
+
+ const ltk = previousToken.type.keyword;
+ const ttl = token.type.label;
+ if (ltk != null && ttl != ".") {
+ if (isBreakContinueOrReturnStatement(ltk)) {
+ return ttl != ";";
+ }
+ if (needsSpaceBeforePreviousTokenKeywordAfterNotDot(ltk)) {
+ return true;
+ }
+ }
+
+ if (ltt == ")" && needsSpaceBeforeClosingParen(ttl)) {
+ return true;
+ }
+
+ if (isIdentifierLike(token) && isIdentifierLike(previousToken)) {
+ // We must emit a space to avoid merging the tokens.
+ return true;
+ }
+
+ if (token.type.label == "{" && previousToken.type.label == "name") {
+ return true;
+ }
+
+ return false;
+}
+
+function needsSpaceBeforeClosingCurlyBracket(tokenTypeKeyword) {
+ return (
+ tokenTypeKeyword == "else" ||
+ tokenTypeKeyword == "catch" ||
+ tokenTypeKeyword == "finally"
+ );
+}
+
+function needsLineBreakBeforeClosingCurlyBracket(tokenTypeLabel) {
+ return (
+ tokenTypeLabel != "(" &&
+ tokenTypeLabel != ";" &&
+ tokenTypeLabel != "," &&
+ tokenTypeLabel != ")" &&
+ tokenTypeLabel != "." &&
+ tokenTypeLabel != "template" &&
+ tokenTypeLabel != "`"
+ );
+}
+
+const escapeCharacters = {
+ // Backslash
+ "\\": "\\\\",
+ // Newlines
+ "\n": "\\n",
+ // Carriage return
+ "\r": "\\r",
+ // Tab
+ "\t": "\\t",
+ // Vertical tab
+ "\v": "\\v",
+ // Form feed
+ "\f": "\\f",
+ // Null character
+ "\0": "\\x00",
+ // Line separator
+ "\u2028": "\\u2028",
+ // Paragraph separator
+ "\u2029": "\\u2029",
+ // Single quotes
+ "'": "\\'",
+};
+
+// eslint-disable-next-line prefer-template
+const regExpString = "(" + Object.values(escapeCharacters).join("|") + ")";
+const escapeCharactersRegExp = new RegExp(regExpString, "g");
+
+function sanitizerReplaceFunc(_, c) {
+ return escapeCharacters[c];
+}
+
+/**
+ * Make sure that we output the escaped character combination inside string
+ * literals instead of various problematic characters.
+ */
+function sanitize(str) {
+ return str.replace(escapeCharactersRegExp, sanitizerReplaceFunc);
+}
+
+/**
+ * Returns true if the given token type belongs on the stack.
+ */
+function belongsOnStack(token) {
+ const ttl = token.type.label;
+ const ttk = token.type.keyword;
+ return (
+ ttl == "{" ||
+ ttl == "(" ||
+ ttl == "[" ||
+ ttl == "?" ||
+ ttl == "${" ||
+ ttk == "do" ||
+ ttk == "switch" ||
+ ttk == "case" ||
+ ttk == "default"
+ );
+}
diff --git a/devtools/client/debugger/src/workers/pretty-print/tests/__snapshots__/prettyFast.spec.js.snap b/devtools/client/debugger/src/workers/pretty-print/tests/__snapshots__/prettyFast.spec.js.snap
new file mode 100644
index 0000000000..498bee267e
--- /dev/null
+++ b/devtools/client/debugger/src/workers/pretty-print/tests/__snapshots__/prettyFast.spec.js.snap
@@ -0,0 +1,1974 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ASI return 1`] = `
+"function f() {
+ return
+ {
+ }
+}
+"
+`;
+
+exports[`ASI return 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 9) -> (1, 9)",
+ "(1, 10) -> (1, 10)",
+ "(1, 11) -> (1, 11)",
+ "(1, 13) -> (1, 13)",
+ "(2, 2) -> (2, 13)",
+ "(3, 2) -> (3, 13)",
+ "(4, 2) -> (3, 14)",
+ "(5, 0) -> (4, 11)",
+]
+`;
+
+exports[`Arrays 1`] = `
+"var a = [
+ 1,
+ 2,
+ 3
+];
+"
+`;
+
+exports[`Arrays 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 6) -> (1, 5)",
+ "(1, 8) -> (1, 6)",
+ "(2, 2) -> (1, 7)",
+ "(2, 3) -> (1, 8)",
+ "(3, 2) -> (1, 9)",
+ "(3, 3) -> (1, 10)",
+ "(4, 2) -> (1, 11)",
+ "(5, 0) -> (1, 12)",
+ "(5, 1) -> (1, 13)",
+]
+`;
+
+exports[`Arrays and spread operator 1`] = `
+"var a = [
+ 1,
+ ...[
+ 2,
+ 3
+ ],
+ ...[],
+ 4
+];
+"
+`;
+
+exports[`Arrays and spread operator 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 6) -> (1, 5)",
+ "(1, 8) -> (1, 6)",
+ "(2, 2) -> (1, 7)",
+ "(2, 3) -> (1, 8)",
+ "(3, 2) -> (1, 9)",
+ "(3, 5) -> (1, 12)",
+ "(4, 4) -> (1, 13)",
+ "(4, 5) -> (1, 14)",
+ "(5, 4) -> (1, 15)",
+ "(6, 2) -> (1, 16)",
+ "(6, 3) -> (1, 17)",
+ "(7, 2) -> (1, 18)",
+ "(7, 5) -> (1, 21)",
+ "(7, 6) -> (1, 22)",
+ "(7, 7) -> (1, 23)",
+ "(8, 2) -> (1, 25)",
+ "(9, 0) -> (1, 26)",
+ "(9, 1) -> (1, 27)",
+]
+`;
+
+exports[`Binary operators 1`] = `
+"var a = 5 * 30;
+var b = 5 >> 3;
+"
+`;
+
+exports[`Binary operators 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 6) -> (1, 5)",
+ "(1, 8) -> (1, 6)",
+ "(1, 10) -> (1, 7)",
+ "(1, 12) -> (1, 8)",
+ "(1, 14) -> (1, 10)",
+ "(2, 0) -> (1, 11)",
+ "(2, 4) -> (1, 15)",
+ "(2, 6) -> (1, 16)",
+ "(2, 8) -> (1, 17)",
+ "(2, 10) -> (1, 18)",
+ "(2, 13) -> (1, 20)",
+ "(2, 14) -> (1, 21)",
+]
+`;
+
+exports[`Bug 975477 don't move end of line comments to next line 1`] = `
+"switch (request.action) {
+ case 'show': //$NON-NLS-0$
+ if (localStorage.hideicon !== 'true') { //$NON-NLS-0$
+ chrome.pageAction.show(sender.tab.id);
+ }
+ break;
+ case 'hide': /*Multiline
+ Comment */
+ break;
+ default:
+ console.warn('unknown request'); //$NON-NLS-0$
+ // don't respond if you don't understand the message.
+ return;
+}
+"
+`;
+
+exports[`Bug 975477 don't move end of line comments to next line 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 7) -> (1, 7)",
+ "(1, 8) -> (1, 8)",
+ "(1, 15) -> (1, 15)",
+ "(1, 16) -> (1, 16)",
+ "(1, 22) -> (1, 22)",
+ "(1, 24) -> (1, 24)",
+ "(2, 2) -> (2, 13)",
+ "(2, 7) -> (2, 18)",
+ "(2, 13) -> (2, 24)",
+ "(3, 4) -> (3, 15)",
+ "(3, 7) -> (3, 18)",
+ "(3, 8) -> (3, 19)",
+ "(3, 20) -> (3, 31)",
+ "(3, 21) -> (3, 32)",
+ "(3, 30) -> (3, 41)",
+ "(3, 34) -> (3, 45)",
+ "(3, 40) -> (3, 51)",
+ "(3, 42) -> (3, 53)",
+ "(4, 6) -> (4, 17)",
+ "(4, 12) -> (4, 23)",
+ "(4, 13) -> (4, 24)",
+ "(4, 23) -> (4, 34)",
+ "(4, 24) -> (4, 35)",
+ "(4, 28) -> (4, 39)",
+ "(4, 29) -> (4, 40)",
+ "(4, 35) -> (4, 46)",
+ "(4, 36) -> (4, 47)",
+ "(4, 39) -> (4, 50)",
+ "(4, 40) -> (4, 51)",
+ "(4, 42) -> (4, 53)",
+ "(4, 43) -> (4, 54)",
+ "(5, 4) -> (5, 15)",
+ "(6, 4) -> (6, 15)",
+ "(6, 9) -> (6, 20)",
+ "(7, 2) -> (7, 13)",
+ "(7, 7) -> (7, 18)",
+ "(7, 13) -> (7, 24)",
+ "(9, 4) -> (9, 15)",
+ "(9, 9) -> (9, 20)",
+ "(10, 2) -> (10, 13)",
+ "(10, 9) -> (10, 20)",
+ "(11, 4) -> (11, 15)",
+ "(11, 11) -> (11, 22)",
+ "(11, 12) -> (11, 23)",
+ "(11, 16) -> (11, 27)",
+ "(11, 17) -> (11, 28)",
+ "(11, 34) -> (11, 45)",
+ "(11, 35) -> (11, 46)",
+ "(13, 4) -> (13, 15)",
+ "(13, 10) -> (13, 21)",
+ "(14, 0) -> (14, 11)",
+]
+`;
+
+exports[`Bug 977082 - space between grouping operator and dot notation 1`] = `
+"JSON.stringify(3).length;
+([1,
+2,
+3]).length;
+(new Date()).toLocaleString();
+"
+`;
+
+exports[`Bug 977082 - space between grouping operator and dot notation 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 5) -> (1, 5)",
+ "(1, 14) -> (1, 14)",
+ "(1, 15) -> (1, 15)",
+ "(1, 16) -> (1, 16)",
+ "(1, 17) -> (1, 17)",
+ "(1, 18) -> (1, 18)",
+ "(1, 24) -> (1, 24)",
+ "(2, 0) -> (2, 11)",
+ "(2, 1) -> (2, 12)",
+ "(2, 2) -> (2, 13)",
+ "(2, 3) -> (2, 14)",
+ "(3, 0) -> (2, 15)",
+ "(3, 1) -> (2, 16)",
+ "(4, 0) -> (2, 17)",
+ "(4, 1) -> (2, 18)",
+ "(4, 2) -> (2, 19)",
+ "(4, 3) -> (2, 20)",
+ "(4, 4) -> (2, 21)",
+ "(4, 10) -> (2, 27)",
+ "(5, 0) -> (3, 11)",
+ "(5, 1) -> (3, 12)",
+ "(5, 5) -> (3, 16)",
+ "(5, 9) -> (3, 20)",
+ "(5, 10) -> (3, 21)",
+ "(5, 11) -> (3, 22)",
+ "(5, 12) -> (3, 23)",
+ "(5, 13) -> (3, 24)",
+ "(5, 27) -> (3, 38)",
+ "(5, 28) -> (3, 39)",
+ "(5, 29) -> (3, 40)",
+]
+`;
+
+exports[`Bug 1206633 - spaces in for of 1`] = `
+"for (let tab of tabs) {
+}
+"
+`;
+
+exports[`Bug 1206633 - spaces in for of 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 5) -> (1, 5)",
+ "(1, 9) -> (1, 9)",
+ "(1, 13) -> (1, 13)",
+ "(1, 16) -> (1, 16)",
+ "(1, 20) -> (1, 20)",
+ "(1, 22) -> (1, 22)",
+ "(2, 0) -> (1, 23)",
+]
+`;
+
+exports[`Bug 1261971 - indentation after switch statement 1`] = `
+"{
+ switch (x) {
+ }
+ if (y) {
+ }
+ done();
+}
+"
+`;
+
+exports[`Bug 1261971 - indentation after switch statement 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(2, 2) -> (1, 1)",
+ "(2, 9) -> (1, 7)",
+ "(2, 10) -> (1, 8)",
+ "(2, 11) -> (1, 9)",
+ "(2, 13) -> (1, 10)",
+ "(3, 2) -> (1, 11)",
+ "(4, 2) -> (1, 12)",
+ "(4, 5) -> (1, 14)",
+ "(4, 6) -> (1, 15)",
+ "(4, 7) -> (1, 16)",
+ "(4, 9) -> (1, 17)",
+ "(5, 2) -> (1, 18)",
+ "(6, 2) -> (1, 19)",
+ "(6, 6) -> (1, 23)",
+ "(6, 7) -> (1, 24)",
+ "(6, 8) -> (1, 25)",
+ "(7, 0) -> (1, 26)",
+]
+`;
+
+exports[`Bug pretty-sure-3 - escaping line and paragraph separators 1`] = `
+"x = '\\\\u2029\\\\u2028';
+"
+`;
+
+exports[`Bug pretty-sure-3 - escaping line and paragraph separators 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 2) -> (1, 2)",
+ "(1, 4) -> (1, 4)",
+ "(1, 18) -> (1, 18)",
+]
+`;
+
+exports[`Bug pretty-sure-4 - escaping null character before digit 1`] = `
+"x = '\\\\x001';
+"
+`;
+
+exports[`Bug pretty-sure-4 - escaping null character before digit 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 2) -> (1, 2)",
+ "(1, 4) -> (1, 4)",
+ "(1, 11) -> (1, 13)",
+]
+`;
+
+exports[`Bug pretty-sure-5 - empty multiline comment shouldn't throw exception 1`] = `
+"{
+ /*
+ */
+ return;
+}
+"
+`;
+
+exports[`Bug pretty-sure-5 - empty multiline comment shouldn't throw exception 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(4, 2) -> (4, 13)",
+ "(4, 8) -> (4, 19)",
+ "(5, 0) -> (5, 11)",
+]
+`;
+
+exports[`Bug pretty-sure-6 - inline comment shouldn't move parenthesis to next line 1`] = `
+"return /* inline comment */ (1 + 1);
+"
+`;
+
+exports[`Bug pretty-sure-6 - inline comment shouldn't move parenthesis to next line 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 28) -> (1, 28)",
+ "(1, 29) -> (2, 13)",
+ "(1, 31) -> (2, 14)",
+ "(1, 33) -> (2, 15)",
+ "(1, 34) -> (2, 16)",
+ "(1, 35) -> (2, 17)",
+]
+`;
+
+exports[`Bug pretty-sure-7 - accessing a literal number property requires a space 1`] = `
+"0 .toString() + x.toString();
+"
+`;
+
+exports[`Bug pretty-sure-7 - accessing a literal number property requires a space 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 2) -> (1, 2)",
+ "(1, 3) -> (1, 3)",
+ "(1, 11) -> (1, 11)",
+ "(1, 12) -> (1, 12)",
+ "(1, 14) -> (1, 13)",
+ "(1, 16) -> (1, 14)",
+ "(1, 17) -> (1, 15)",
+ "(1, 18) -> (1, 16)",
+ "(1, 26) -> (1, 24)",
+ "(1, 27) -> (1, 25)",
+ "(1, 28) -> (1, 26)",
+]
+`;
+
+exports[`Bug pretty-sure-8 - return and yield only accept arguments when on the same line 1`] = `
+"{
+ return
+ (x)
+ yield
+ (x)
+ yield
+ * x
+}
+"
+`;
+
+exports[`Bug pretty-sure-8 - return and yield only accept arguments when on the same line 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(2, 2) -> (2, 13)",
+ "(3, 2) -> (3, 13)",
+ "(3, 3) -> (3, 14)",
+ "(3, 4) -> (3, 15)",
+ "(4, 2) -> (4, 13)",
+ "(5, 2) -> (5, 13)",
+ "(5, 3) -> (5, 14)",
+ "(5, 4) -> (5, 15)",
+ "(6, 2) -> (6, 13)",
+ "(7, 2) -> (7, 13)",
+ "(7, 4) -> (7, 14)",
+ "(8, 0) -> (8, 11)",
+]
+`;
+
+exports[`Bug pretty-sure-9 - accept unary operator at start of file 1`] = `
+"+ 0
+"
+`;
+
+exports[`Bug pretty-sure-9 - accept unary operator at start of file 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 2) -> (1, 2)",
+]
+`;
+
+exports[`Class extension within a function 1`] = `
+"(function () {
+ class X extends Y {
+ constructor() {
+ }
+ }
+}) ()
+"
+`;
+
+exports[`Class extension within a function 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 1) -> (1, 1)",
+ "(1, 10) -> (1, 9)",
+ "(1, 11) -> (1, 10)",
+ "(1, 13) -> (1, 12)",
+ "(2, 2) -> (1, 15)",
+ "(2, 8) -> (1, 21)",
+ "(2, 10) -> (1, 23)",
+ "(2, 18) -> (1, 31)",
+ "(2, 20) -> (1, 33)",
+ "(3, 4) -> (1, 35)",
+ "(3, 15) -> (1, 46)",
+ "(3, 16) -> (1, 47)",
+ "(3, 18) -> (1, 49)",
+ "(4, 4) -> (1, 50)",
+ "(5, 2) -> (1, 52)",
+ "(6, 0) -> (1, 55)",
+ "(6, 1) -> (1, 56)",
+ "(6, 3) -> (1, 57)",
+ "(6, 4) -> (1, 58)",
+]
+`;
+
+exports[`Class handling 1`] = `
+"class Class {
+ constructor() {
+ }
+}
+"
+`;
+
+exports[`Class handling 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 6) -> (1, 7)",
+ "(1, 12) -> (1, 12)",
+ "(2, 2) -> (1, 13)",
+ "(2, 13) -> (1, 24)",
+ "(2, 14) -> (1, 25)",
+ "(2, 16) -> (1, 26)",
+ "(3, 2) -> (1, 27)",
+ "(4, 0) -> (1, 28)",
+]
+`;
+
+exports[`Code that relies on ASI 1`] = `
+"var foo = 10
+var bar = 20
+function g() {
+ a()
+ b()
+}
+"
+`;
+
+exports[`Code that relies on ASI 2`] = `
+Array [
+ "(1, 0) -> (2, 11)",
+ "(1, 4) -> (2, 15)",
+ "(1, 8) -> (2, 19)",
+ "(1, 10) -> (2, 21)",
+ "(2, 0) -> (3, 11)",
+ "(2, 4) -> (3, 15)",
+ "(2, 8) -> (3, 19)",
+ "(2, 10) -> (3, 21)",
+ "(3, 0) -> (4, 11)",
+ "(3, 9) -> (4, 20)",
+ "(3, 10) -> (4, 21)",
+ "(3, 11) -> (4, 22)",
+ "(3, 13) -> (4, 24)",
+ "(4, 2) -> (5, 13)",
+ "(4, 3) -> (5, 14)",
+ "(4, 4) -> (5, 15)",
+ "(5, 2) -> (6, 13)",
+ "(5, 3) -> (6, 14)",
+ "(5, 4) -> (6, 15)",
+ "(6, 0) -> (7, 11)",
+]
+`;
+
+exports[`Const handling 1`] = `
+"const d = 'yes';
+"
+`;
+
+exports[`Const handling 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 6) -> (1, 6)",
+ "(1, 8) -> (1, 8)",
+ "(1, 10) -> (1, 10)",
+ "(1, 15) -> (1, 15)",
+]
+`;
+
+exports[`Continue/break statements 1`] = `
+"while (1) {
+ if (x) {
+ continue
+ }
+ if (y) {
+ break
+ }
+ if (z) {
+ break foo
+ }
+}
+"
+`;
+
+exports[`Continue/break statements 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 6) -> (1, 5)",
+ "(1, 7) -> (1, 6)",
+ "(1, 8) -> (1, 7)",
+ "(1, 10) -> (1, 8)",
+ "(2, 2) -> (1, 9)",
+ "(2, 5) -> (1, 11)",
+ "(2, 6) -> (1, 12)",
+ "(2, 7) -> (1, 13)",
+ "(2, 9) -> (1, 14)",
+ "(3, 4) -> (1, 15)",
+ "(4, 2) -> (1, 23)",
+ "(5, 2) -> (1, 24)",
+ "(5, 5) -> (1, 26)",
+ "(5, 6) -> (1, 27)",
+ "(5, 7) -> (1, 28)",
+ "(5, 9) -> (1, 29)",
+ "(6, 4) -> (1, 30)",
+ "(7, 2) -> (1, 35)",
+ "(8, 2) -> (1, 36)",
+ "(8, 5) -> (1, 38)",
+ "(8, 6) -> (1, 39)",
+ "(8, 7) -> (1, 40)",
+ "(8, 9) -> (1, 41)",
+ "(9, 4) -> (1, 42)",
+ "(9, 10) -> (1, 48)",
+ "(10, 2) -> (1, 51)",
+ "(11, 0) -> (1, 52)",
+]
+`;
+
+exports[`Delete 1`] = `
+"delete obj.prop;
+"
+`;
+
+exports[`Delete 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 7) -> (1, 7)",
+ "(1, 10) -> (1, 10)",
+ "(1, 11) -> (1, 11)",
+ "(1, 15) -> (1, 15)",
+]
+`;
+
+exports[`Do/while loop 1`] = `
+"do {
+ x
+} while (y)
+"
+`;
+
+exports[`Do/while loop 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 3) -> (1, 2)",
+ "(2, 2) -> (1, 3)",
+ "(3, 0) -> (1, 4)",
+ "(3, 2) -> (1, 5)",
+ "(3, 8) -> (1, 10)",
+ "(3, 9) -> (1, 11)",
+ "(3, 10) -> (1, 12)",
+]
+`;
+
+exports[`Dot handling with keywords which are identifier name 1`] = `
+"y.await.break.const.delete.else.return.new.yield = 1.23;
+"
+`;
+
+exports[`Dot handling with keywords which are identifier name 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 1) -> (1, 1)",
+ "(1, 2) -> (1, 2)",
+ "(1, 7) -> (1, 7)",
+ "(1, 8) -> (1, 8)",
+ "(1, 13) -> (1, 13)",
+ "(1, 14) -> (1, 14)",
+ "(1, 19) -> (1, 19)",
+ "(1, 20) -> (1, 20)",
+ "(1, 26) -> (1, 26)",
+ "(1, 27) -> (1, 27)",
+ "(1, 31) -> (1, 31)",
+ "(1, 32) -> (1, 32)",
+ "(1, 38) -> (1, 38)",
+ "(1, 39) -> (1, 39)",
+ "(1, 42) -> (1, 42)",
+ "(1, 43) -> (1, 43)",
+ "(1, 49) -> (1, 49)",
+ "(1, 51) -> (1, 51)",
+ "(1, 55) -> (1, 55)",
+]
+`;
+
+exports[`Dot handling with let which is identifier name 1`] = `
+"y.let.let = 1.23;
+"
+`;
+
+exports[`Dot handling with let which is identifier name 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 1) -> (1, 1)",
+ "(1, 2) -> (1, 2)",
+ "(1, 5) -> (1, 5)",
+ "(1, 6) -> (1, 6)",
+ "(1, 10) -> (1, 10)",
+ "(1, 12) -> (1, 12)",
+ "(1, 16) -> (1, 16)",
+]
+`;
+
+exports[`Empty object/array literals 1`] = `
+"let a = [];
+const b = {};
+c = {
+ ...{},
+ d: 42
+};
+for (let x of []) {
+ for (let y in {}) {
+ }
+}
+"
+`;
+
+exports[`Empty object/array literals 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 6) -> (1, 5)",
+ "(1, 8) -> (1, 6)",
+ "(1, 9) -> (1, 7)",
+ "(1, 10) -> (1, 8)",
+ "(2, 0) -> (1, 9)",
+ "(2, 6) -> (1, 15)",
+ "(2, 8) -> (1, 16)",
+ "(2, 10) -> (1, 17)",
+ "(2, 11) -> (1, 18)",
+ "(2, 12) -> (1, 19)",
+ "(3, 0) -> (1, 20)",
+ "(3, 2) -> (1, 21)",
+ "(3, 4) -> (1, 22)",
+ "(4, 2) -> (1, 23)",
+ "(4, 5) -> (1, 26)",
+ "(4, 6) -> (1, 27)",
+ "(4, 7) -> (1, 28)",
+ "(5, 2) -> (1, 29)",
+ "(5, 3) -> (1, 30)",
+ "(5, 5) -> (1, 32)",
+ "(6, 0) -> (1, 34)",
+ "(6, 1) -> (1, 35)",
+ "(7, 0) -> (1, 36)",
+ "(7, 4) -> (1, 39)",
+ "(7, 5) -> (1, 40)",
+ "(7, 9) -> (1, 44)",
+ "(7, 11) -> (1, 46)",
+ "(7, 14) -> (1, 49)",
+ "(7, 15) -> (1, 50)",
+ "(7, 16) -> (1, 51)",
+ "(7, 18) -> (1, 52)",
+ "(8, 2) -> (1, 53)",
+ "(8, 6) -> (1, 56)",
+ "(8, 7) -> (1, 57)",
+ "(8, 11) -> (1, 61)",
+ "(8, 13) -> (1, 63)",
+ "(8, 16) -> (1, 66)",
+ "(8, 17) -> (1, 67)",
+ "(8, 18) -> (1, 68)",
+ "(8, 20) -> (1, 69)",
+ "(9, 2) -> (1, 70)",
+ "(10, 0) -> (1, 71)",
+]
+`;
+
+exports[`Escaping backslashes in strings 1`] = `
+"'\\\\\\\\'
+"
+`;
+
+exports[`Escaping backslashes in strings 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+]
+`;
+
+exports[`Escaping carriage return in strings 1`] = `
+"'\\\\r'
+"
+`;
+
+exports[`Escaping carriage return in strings 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+]
+`;
+
+exports[`Escaping form feed in strings 1`] = `
+"'\\\\f'
+"
+`;
+
+exports[`Escaping form feed in strings 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+]
+`;
+
+exports[`Escaping null character in strings 1`] = `
+"'\\\\x00'
+"
+`;
+
+exports[`Escaping null character in strings 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+]
+`;
+
+exports[`Escaping tab in strings 1`] = `
+"'\\\\t'
+"
+`;
+
+exports[`Escaping tab in strings 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+]
+`;
+
+exports[`Escaping vertical tab in strings 1`] = `
+"'\\\\v'
+"
+`;
+
+exports[`Escaping vertical tab in strings 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+]
+`;
+
+exports[`False assignment 1`] = `
+"var foo = false;
+"
+`;
+
+exports[`False assignment 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 8) -> (1, 7)",
+ "(1, 10) -> (1, 8)",
+ "(1, 15) -> (1, 13)",
+]
+`;
+
+exports[`Fat arrow function 1`] = `
+"const a = () => 42
+addEventListener('click', e => {
+ return false
+});
+const sum = (c, d) => c + d
+"
+`;
+
+exports[`Fat arrow function 2`] = `
+Array [
+ "(1, 0) -> (2, 6)",
+ "(1, 6) -> (2, 12)",
+ "(1, 8) -> (2, 14)",
+ "(1, 10) -> (2, 16)",
+ "(1, 11) -> (2, 17)",
+ "(1, 13) -> (2, 19)",
+ "(1, 16) -> (2, 22)",
+ "(2, 0) -> (3, 6)",
+ "(2, 16) -> (3, 22)",
+ "(2, 17) -> (3, 23)",
+ "(2, 24) -> (3, 30)",
+ "(2, 26) -> (3, 32)",
+ "(2, 28) -> (3, 34)",
+ "(2, 31) -> (3, 37)",
+ "(3, 2) -> (3, 39)",
+ "(3, 9) -> (3, 46)",
+ "(4, 0) -> (3, 52)",
+ "(4, 1) -> (3, 53)",
+ "(4, 2) -> (3, 54)",
+ "(5, 0) -> (4, 6)",
+ "(5, 6) -> (4, 12)",
+ "(5, 10) -> (4, 16)",
+ "(5, 12) -> (4, 18)",
+ "(5, 13) -> (4, 19)",
+ "(5, 14) -> (4, 20)",
+ "(5, 16) -> (4, 21)",
+ "(5, 17) -> (4, 22)",
+ "(5, 19) -> (4, 24)",
+ "(5, 22) -> (4, 27)",
+ "(5, 24) -> (4, 28)",
+ "(5, 26) -> (4, 29)",
+]
+`;
+
+exports[`For loop 1`] = `
+"for (var i = 0; i < n; i++) {
+ console.log(i);
+}
+"
+`;
+
+exports[`For loop 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 5) -> (1, 5)",
+ "(1, 9) -> (1, 9)",
+ "(1, 11) -> (1, 11)",
+ "(1, 13) -> (1, 13)",
+ "(1, 14) -> (1, 14)",
+ "(1, 16) -> (1, 16)",
+ "(1, 18) -> (1, 18)",
+ "(1, 20) -> (1, 20)",
+ "(1, 21) -> (1, 21)",
+ "(1, 23) -> (1, 23)",
+ "(1, 24) -> (1, 24)",
+ "(1, 26) -> (1, 26)",
+ "(1, 28) -> (1, 28)",
+ "(2, 2) -> (1, 30)",
+ "(2, 9) -> (1, 37)",
+ "(2, 10) -> (1, 38)",
+ "(2, 13) -> (1, 41)",
+ "(2, 14) -> (1, 42)",
+ "(2, 15) -> (1, 43)",
+ "(2, 16) -> (1, 44)",
+ "(3, 0) -> (1, 46)",
+]
+`;
+
+exports[`Function calls 1`] = `
+"var result = func(a, b, c, d);
+"
+`;
+
+exports[`Function calls 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 11) -> (1, 10)",
+ "(1, 13) -> (1, 11)",
+ "(1, 17) -> (1, 15)",
+ "(1, 18) -> (1, 16)",
+ "(1, 19) -> (1, 17)",
+ "(1, 21) -> (1, 18)",
+ "(1, 22) -> (1, 19)",
+ "(1, 24) -> (1, 20)",
+ "(1, 25) -> (1, 21)",
+ "(1, 27) -> (1, 22)",
+ "(1, 28) -> (1, 23)",
+ "(1, 29) -> (1, 24)",
+]
+`;
+
+exports[`Getter and setter literals 1`] = `
+"var obj = {
+ get foo() {
+ return this._foo
+ },
+ set foo(v) {
+ this._foo = v
+ }
+}
+"
+`;
+
+exports[`Getter and setter literals 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 8) -> (1, 7)",
+ "(1, 10) -> (1, 8)",
+ "(2, 2) -> (1, 9)",
+ "(2, 6) -> (1, 13)",
+ "(2, 9) -> (1, 16)",
+ "(2, 10) -> (1, 17)",
+ "(2, 12) -> (1, 18)",
+ "(3, 4) -> (1, 19)",
+ "(3, 11) -> (1, 26)",
+ "(3, 15) -> (1, 30)",
+ "(3, 16) -> (1, 31)",
+ "(4, 2) -> (1, 35)",
+ "(4, 3) -> (1, 36)",
+ "(5, 2) -> (1, 37)",
+ "(5, 6) -> (1, 41)",
+ "(5, 9) -> (1, 44)",
+ "(5, 10) -> (1, 45)",
+ "(5, 11) -> (1, 46)",
+ "(5, 13) -> (1, 47)",
+ "(6, 4) -> (1, 48)",
+ "(6, 8) -> (1, 52)",
+ "(6, 9) -> (1, 53)",
+ "(6, 14) -> (1, 57)",
+ "(6, 16) -> (1, 58)",
+ "(7, 2) -> (1, 59)",
+ "(8, 0) -> (1, 60)",
+]
+`;
+
+exports[`If/else statement 1`] = `
+"if (c) {
+ then()
+} else {
+ other()
+}
+"
+`;
+
+exports[`If/else statement 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 3) -> (1, 2)",
+ "(1, 4) -> (1, 3)",
+ "(1, 5) -> (1, 4)",
+ "(1, 7) -> (1, 5)",
+ "(2, 2) -> (1, 6)",
+ "(2, 6) -> (1, 10)",
+ "(2, 7) -> (1, 11)",
+ "(3, 0) -> (1, 12)",
+ "(3, 2) -> (1, 13)",
+ "(3, 7) -> (1, 17)",
+ "(4, 2) -> (1, 18)",
+ "(4, 7) -> (1, 23)",
+ "(4, 8) -> (1, 24)",
+ "(5, 0) -> (1, 25)",
+]
+`;
+
+exports[`If/else without curlies 1`] = `
+"if (c) a else b
+"
+`;
+
+exports[`If/else without curlies 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 3) -> (1, 2)",
+ "(1, 4) -> (1, 3)",
+ "(1, 5) -> (1, 4)",
+ "(1, 7) -> (1, 6)",
+ "(1, 9) -> (1, 8)",
+ "(1, 14) -> (1, 13)",
+]
+`;
+
+exports[`Immediately invoked function expression 1`] = `
+"(function () {
+ thingy()
+}())
+"
+`;
+
+exports[`Immediately invoked function expression 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 1) -> (1, 1)",
+ "(1, 10) -> (1, 9)",
+ "(1, 11) -> (1, 10)",
+ "(1, 13) -> (1, 11)",
+ "(2, 2) -> (1, 12)",
+ "(2, 8) -> (1, 18)",
+ "(2, 9) -> (1, 19)",
+ "(3, 0) -> (1, 20)",
+ "(3, 1) -> (1, 21)",
+ "(3, 2) -> (1, 22)",
+ "(3, 3) -> (1, 23)",
+]
+`;
+
+exports[`In operator 1`] = `
+"if (foo in bar) {
+ doThing()
+}
+"
+`;
+
+exports[`In operator 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 3) -> (1, 2)",
+ "(1, 4) -> (1, 3)",
+ "(1, 8) -> (1, 7)",
+ "(1, 11) -> (1, 10)",
+ "(1, 14) -> (1, 13)",
+ "(1, 16) -> (1, 14)",
+ "(2, 2) -> (1, 15)",
+ "(2, 9) -> (1, 22)",
+ "(2, 10) -> (1, 23)",
+ "(3, 0) -> (1, 24)",
+]
+`;
+
+exports[`Indented multiline comment 1`] = `
+"function foo() {
+ /**
+ * java doc style comment
+ * more comment
+ */
+ bar();
+}
+"
+`;
+
+exports[`Indented multiline comment 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 9) -> (1, 9)",
+ "(1, 12) -> (1, 12)",
+ "(1, 13) -> (1, 13)",
+ "(1, 15) -> (1, 15)",
+ "(6, 2) -> (6, 13)",
+ "(6, 5) -> (6, 16)",
+ "(6, 6) -> (6, 17)",
+ "(6, 7) -> (6, 18)",
+ "(7, 0) -> (7, 11)",
+]
+`;
+
+exports[`Instanceof 1`] = `
+"var a = x instanceof y;
+"
+`;
+
+exports[`Instanceof 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 6) -> (1, 5)",
+ "(1, 8) -> (1, 6)",
+ "(1, 10) -> (1, 8)",
+ "(1, 21) -> (1, 19)",
+ "(1, 22) -> (1, 20)",
+]
+`;
+
+exports[`Let handling with value 1`] = `
+"let d = 'yes';
+"
+`;
+
+exports[`Let handling with value 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 6) -> (1, 6)",
+ "(1, 8) -> (1, 8)",
+ "(1, 13) -> (1, 13)",
+]
+`;
+
+exports[`Let handling without value 1`] = `
+"let d;
+"
+`;
+
+exports[`Let handling without value 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 5) -> (1, 5)",
+]
+`;
+
+exports[`Long parenthesis 1`] = `
+"if (
+ thisIsAVeryLongVariable &&
+ thisIsAnotherOne ||
+ yetAnotherVeryLongVariable
+) {
+ (
+ thisIsAnotherOne = thisMayReturnNull() ||
+ 'hi',
+ thisIsAVeryLongVariable = 42,
+ yetAnotherVeryLongVariable &&
+ doSomething(
+ true /* do it well */ ,
+ thisIsAVeryLongVariable,
+ thisIsAnotherOne,
+ yetAnotherVeryLongVariable
+ )
+ )
+}
+for (
+ let thisIsAnotherVeryLongVariable = 0;
+ i < thisIsAnotherVeryLongVariable.length;
+ thisIsAnotherVeryLongVariable++
+) {
+}
+const x = ({
+ thisIsAnotherVeryLongPropertyName: 'but should not cause the paren to be a line delimiter'
+})
+"
+`;
+
+exports[`Long parenthesis 2`] = `
+Array [
+ "(1, 0) -> (2, 6)",
+ "(1, 3) -> (2, 9)",
+ "(2, 2) -> (2, 10)",
+ "(2, 26) -> (2, 34)",
+ "(3, 2) -> (2, 37)",
+ "(3, 19) -> (2, 54)",
+ "(4, 2) -> (2, 57)",
+ "(5, 0) -> (2, 83)",
+ "(5, 2) -> (2, 85)",
+ "(6, 2) -> (3, 8)",
+ "(7, 4) -> (3, 9)",
+ "(7, 21) -> (3, 26)",
+ "(7, 23) -> (3, 28)",
+ "(7, 40) -> (3, 45)",
+ "(7, 41) -> (3, 46)",
+ "(7, 43) -> (3, 48)",
+ "(8, 4) -> (3, 51)",
+ "(8, 8) -> (3, 55)",
+ "(9, 4) -> (3, 57)",
+ "(9, 28) -> (3, 81)",
+ "(9, 30) -> (3, 83)",
+ "(9, 32) -> (3, 85)",
+ "(10, 4) -> (3, 87)",
+ "(10, 31) -> (3, 114)",
+ "(11, 4) -> (3, 117)",
+ "(11, 15) -> (3, 128)",
+ "(12, 6) -> (3, 129)",
+ "(12, 28) -> (3, 150)",
+ "(13, 6) -> (3, 151)",
+ "(13, 29) -> (3, 174)",
+ "(14, 6) -> (3, 176)",
+ "(14, 22) -> (3, 192)",
+ "(15, 6) -> (3, 194)",
+ "(16, 4) -> (3, 220)",
+ "(17, 2) -> (3, 221)",
+ "(18, 0) -> (4, 6)",
+ "(19, 0) -> (5, 6)",
+ "(19, 4) -> (5, 10)",
+ "(20, 2) -> (5, 11)",
+ "(20, 6) -> (5, 15)",
+ "(20, 36) -> (5, 45)",
+ "(20, 38) -> (5, 47)",
+ "(20, 39) -> (5, 48)",
+ "(21, 2) -> (5, 50)",
+ "(21, 4) -> (5, 52)",
+ "(21, 6) -> (5, 54)",
+ "(21, 35) -> (5, 83)",
+ "(21, 36) -> (5, 84)",
+ "(21, 42) -> (5, 90)",
+ "(22, 2) -> (5, 92)",
+ "(22, 31) -> (5, 121)",
+ "(23, 0) -> (5, 123)",
+ "(23, 2) -> (5, 125)",
+ "(24, 0) -> (5, 126)",
+ "(25, 0) -> (6, 6)",
+ "(25, 6) -> (6, 12)",
+ "(25, 8) -> (6, 14)",
+ "(25, 10) -> (6, 16)",
+ "(25, 11) -> (6, 17)",
+ "(26, 2) -> (6, 18)",
+ "(26, 35) -> (6, 51)",
+ "(26, 37) -> (6, 53)",
+ "(27, 0) -> (6, 108)",
+ "(27, 1) -> (6, 109)",
+]
+`;
+
+exports[`Multi line comment 1`] = `
+"/* Comment
+ more comment */
+function foo() {
+ bar();
+}
+"
+`;
+
+exports[`Multi line comment 2`] = `
+Array [
+ "(3, 0) -> (4, 4)",
+ "(3, 9) -> (4, 13)",
+ "(3, 12) -> (4, 16)",
+ "(3, 13) -> (4, 17)",
+ "(3, 15) -> (4, 19)",
+ "(4, 2) -> (4, 21)",
+ "(4, 5) -> (4, 24)",
+ "(4, 6) -> (4, 25)",
+ "(4, 7) -> (4, 26)",
+ "(5, 0) -> (4, 28)",
+]
+`;
+
+exports[`Multiple single line comments 1`] = `
+"function f() {
+ // a
+ // b
+ // c
+}
+"
+`;
+
+exports[`Multiple single line comments 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 9) -> (1, 9)",
+ "(1, 10) -> (1, 10)",
+ "(1, 11) -> (1, 11)",
+ "(1, 13) -> (1, 13)",
+ "(5, 0) -> (5, 11)",
+]
+`;
+
+exports[`Named class handling 1`] = `
+"let unnamed = class Class {
+ constructor() {
+ }
+}
+"
+`;
+
+exports[`Named class handling 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 12) -> (1, 11)",
+ "(1, 14) -> (1, 12)",
+ "(1, 20) -> (1, 18)",
+ "(1, 26) -> (1, 23)",
+ "(2, 2) -> (1, 24)",
+ "(2, 13) -> (1, 35)",
+ "(2, 14) -> (1, 36)",
+ "(2, 16) -> (1, 37)",
+ "(3, 2) -> (1, 38)",
+ "(4, 0) -> (1, 39)",
+]
+`;
+
+exports[`Nested function 1`] = `
+"function foo() {
+ function bar() {
+ debugger;
+ }
+ bar();
+}
+"
+`;
+
+exports[`Nested function 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 9) -> (1, 9)",
+ "(1, 12) -> (1, 12)",
+ "(1, 13) -> (1, 13)",
+ "(1, 15) -> (1, 15)",
+ "(2, 2) -> (1, 17)",
+ "(2, 11) -> (1, 26)",
+ "(2, 14) -> (1, 29)",
+ "(2, 15) -> (1, 30)",
+ "(2, 17) -> (1, 32)",
+ "(3, 4) -> (1, 34)",
+ "(3, 12) -> (1, 42)",
+ "(4, 2) -> (1, 44)",
+ "(5, 2) -> (1, 46)",
+ "(5, 5) -> (1, 49)",
+ "(5, 6) -> (1, 50)",
+ "(5, 7) -> (1, 51)",
+ "(6, 0) -> (1, 53)",
+]
+`;
+
+exports[`New expression 1`] = `
+"var foo = new Foo();
+"
+`;
+
+exports[`New expression 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 8) -> (1, 7)",
+ "(1, 10) -> (1, 8)",
+ "(1, 14) -> (1, 12)",
+ "(1, 17) -> (1, 15)",
+ "(1, 18) -> (1, 16)",
+ "(1, 19) -> (1, 17)",
+]
+`;
+
+exports[`Non-ASI function call 1`] = `
+"f()
+"
+`;
+
+exports[`Non-ASI function call 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 1) -> (2, 11)",
+ "(1, 2) -> (2, 12)",
+]
+`;
+
+exports[`Non-ASI in 1`] = `
+"'x' in foo
+"
+`;
+
+exports[`Non-ASI in 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (2, 11)",
+ "(1, 7) -> (2, 14)",
+]
+`;
+
+exports[`Non-ASI new 1`] = `
+"new F()
+"
+`;
+
+exports[`Non-ASI new 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (2, 11)",
+ "(1, 5) -> (2, 12)",
+ "(1, 6) -> (2, 13)",
+]
+`;
+
+exports[`Non-ASI property access 1`] = `
+"[
+ 1,
+ 2,
+ 3
+]
+[0]
+"
+`;
+
+exports[`Non-ASI property access 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(2, 2) -> (1, 1)",
+ "(2, 3) -> (1, 2)",
+ "(3, 2) -> (1, 3)",
+ "(3, 3) -> (1, 4)",
+ "(4, 2) -> (1, 5)",
+ "(5, 0) -> (1, 6)",
+ "(6, 0) -> (2, 11)",
+ "(6, 1) -> (2, 12)",
+ "(6, 2) -> (2, 13)",
+]
+`;
+
+exports[`Null assignment 1`] = `
+"var i = null;
+"
+`;
+
+exports[`Null assignment 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 6) -> (1, 5)",
+ "(1, 8) -> (1, 6)",
+ "(1, 12) -> (1, 10)",
+]
+`;
+
+exports[`Objects 1`] = `
+"var o = {
+ a: 1,
+ b: 2
+};
+"
+`;
+
+exports[`Objects 2`] = `
+Array [
+ "(1, 0) -> (2, 6)",
+ "(1, 4) -> (2, 10)",
+ "(1, 6) -> (2, 11)",
+ "(1, 8) -> (2, 12)",
+ "(2, 2) -> (2, 13)",
+ "(2, 3) -> (2, 14)",
+ "(2, 5) -> (2, 15)",
+ "(2, 6) -> (2, 16)",
+ "(3, 2) -> (3, 6)",
+ "(3, 3) -> (3, 7)",
+ "(3, 5) -> (3, 8)",
+ "(4, 0) -> (3, 9)",
+ "(4, 1) -> (3, 10)",
+]
+`;
+
+exports[`Optional chaining parsing support 1`] = `
+"x?.y?.z?.['a']?.check();
+"
+`;
+
+exports[`Optional chaining parsing support 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 1) -> (1, 1)",
+ "(1, 3) -> (1, 3)",
+ "(1, 4) -> (1, 4)",
+ "(1, 6) -> (1, 6)",
+ "(1, 7) -> (1, 7)",
+ "(1, 9) -> (1, 9)",
+ "(1, 10) -> (1, 10)",
+ "(1, 13) -> (1, 13)",
+ "(1, 14) -> (1, 14)",
+ "(1, 16) -> (1, 16)",
+ "(1, 21) -> (1, 21)",
+ "(1, 22) -> (1, 22)",
+ "(1, 23) -> (1, 23)",
+]
+`;
+
+exports[`Private fields parsing support 1`] = `
+"class MyClass {
+ constructor(a) {
+ this.#a = a;
+ this.#b = Math.random();
+ this.ab = this.#getAB();
+ }
+ #a
+ #b = 'default value'
+ static #someStaticPrivate
+ #getA() {
+ return this.#a;
+ }
+ #getAB() {
+ return this.#getA() + this.#b
+ }
+}
+"
+`;
+
+exports[`Private fields parsing support 2`] = `
+Array [
+ "(1, 0) -> (2, 6)",
+ "(1, 6) -> (2, 12)",
+ "(1, 14) -> (2, 20)",
+ "(2, 2) -> (3, 8)",
+ "(2, 13) -> (3, 19)",
+ "(2, 14) -> (3, 20)",
+ "(2, 15) -> (3, 21)",
+ "(2, 17) -> (3, 23)",
+ "(3, 4) -> (4, 10)",
+ "(3, 8) -> (4, 14)",
+ "(3, 9) -> (4, 15)",
+ "(3, 12) -> (4, 18)",
+ "(3, 14) -> (4, 20)",
+ "(3, 15) -> (4, 21)",
+ "(4, 4) -> (4, 22)",
+ "(4, 8) -> (4, 26)",
+ "(4, 9) -> (4, 27)",
+ "(4, 12) -> (4, 30)",
+ "(4, 14) -> (4, 32)",
+ "(4, 18) -> (4, 36)",
+ "(4, 19) -> (4, 37)",
+ "(4, 25) -> (4, 43)",
+ "(4, 26) -> (4, 44)",
+ "(4, 27) -> (4, 45)",
+ "(5, 4) -> (4, 46)",
+ "(5, 8) -> (4, 50)",
+ "(5, 9) -> (4, 51)",
+ "(5, 12) -> (4, 54)",
+ "(5, 14) -> (4, 56)",
+ "(5, 18) -> (4, 60)",
+ "(5, 19) -> (4, 61)",
+ "(5, 25) -> (4, 67)",
+ "(5, 26) -> (4, 68)",
+ "(5, 27) -> (4, 69)",
+ "(6, 2) -> (5, 8)",
+ "(7, 2) -> (6, 8)",
+ "(8, 2) -> (7, 8)",
+ "(8, 5) -> (7, 11)",
+ "(8, 7) -> (7, 13)",
+ "(9, 2) -> (8, 8)",
+ "(9, 9) -> (8, 15)",
+ "(10, 2) -> (9, 8)",
+ "(10, 7) -> (9, 13)",
+ "(10, 8) -> (9, 14)",
+ "(10, 10) -> (9, 16)",
+ "(11, 4) -> (10, 10)",
+ "(11, 11) -> (10, 17)",
+ "(11, 15) -> (10, 21)",
+ "(11, 16) -> (10, 22)",
+ "(11, 18) -> (10, 24)",
+ "(12, 2) -> (11, 8)",
+ "(13, 2) -> (12, 8)",
+ "(13, 8) -> (12, 14)",
+ "(13, 9) -> (12, 15)",
+ "(13, 11) -> (12, 17)",
+ "(14, 4) -> (13, 10)",
+ "(14, 11) -> (13, 17)",
+ "(14, 15) -> (13, 21)",
+ "(14, 16) -> (13, 22)",
+ "(14, 21) -> (13, 27)",
+ "(14, 22) -> (13, 28)",
+ "(14, 24) -> (13, 29)",
+ "(14, 26) -> (13, 30)",
+ "(14, 30) -> (13, 34)",
+ "(14, 31) -> (14, 12)",
+ "(15, 2) -> (15, 8)",
+ "(16, 0) -> (16, 6)",
+]
+`;
+
+exports[`Regexp 1`] = `
+"var r = /foobar/g;
+"
+`;
+
+exports[`Regexp 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 6) -> (1, 5)",
+ "(1, 8) -> (1, 6)",
+ "(1, 17) -> (1, 15)",
+]
+`;
+
+exports[`Simple function 1`] = `
+"function foo() {
+ bar();
+}
+"
+`;
+
+exports[`Simple function 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 9) -> (1, 9)",
+ "(1, 12) -> (1, 12)",
+ "(1, 13) -> (1, 13)",
+ "(1, 15) -> (1, 15)",
+ "(2, 2) -> (1, 17)",
+ "(2, 5) -> (1, 20)",
+ "(2, 6) -> (1, 21)",
+ "(2, 7) -> (1, 22)",
+ "(3, 0) -> (1, 24)",
+]
+`;
+
+exports[`Single line comment 1`] = `
+"// Comment
+function foo() {
+ bar();
+}
+"
+`;
+
+exports[`Single line comment 2`] = `
+Array [
+ "(2, 0) -> (3, 4)",
+ "(2, 9) -> (3, 13)",
+ "(2, 12) -> (3, 16)",
+ "(2, 13) -> (3, 17)",
+ "(2, 15) -> (3, 19)",
+ "(3, 2) -> (3, 21)",
+ "(3, 5) -> (3, 24)",
+ "(3, 6) -> (3, 25)",
+ "(3, 7) -> (3, 26)",
+ "(4, 0) -> (3, 28)",
+]
+`;
+
+exports[`Stack-keyword property access 1`] = `
+"foo.a = 1.1;
+foo.do.switch.case.default = 2.2;
+foo.b = 3.3;
+"
+`;
+
+exports[`Stack-keyword property access 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 3) -> (1, 3)",
+ "(1, 4) -> (1, 4)",
+ "(1, 6) -> (1, 5)",
+ "(1, 8) -> (1, 6)",
+ "(1, 11) -> (1, 9)",
+ "(2, 0) -> (1, 10)",
+ "(2, 3) -> (1, 13)",
+ "(2, 4) -> (1, 14)",
+ "(2, 6) -> (1, 16)",
+ "(2, 7) -> (1, 17)",
+ "(2, 13) -> (1, 23)",
+ "(2, 14) -> (1, 24)",
+ "(2, 18) -> (1, 28)",
+ "(2, 19) -> (1, 29)",
+ "(2, 27) -> (1, 36)",
+ "(2, 29) -> (1, 37)",
+ "(2, 32) -> (1, 40)",
+ "(3, 0) -> (1, 41)",
+ "(3, 3) -> (1, 44)",
+ "(3, 4) -> (1, 45)",
+ "(3, 6) -> (1, 46)",
+ "(3, 8) -> (1, 47)",
+ "(3, 11) -> (1, 50)",
+]
+`;
+
+exports[`String with quote 1`] = `
+"var foo = '\\\\'';
+"
+`;
+
+exports[`String with quote 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 8) -> (1, 8)",
+ "(1, 10) -> (1, 10)",
+ "(1, 14) -> (1, 13)",
+]
+`;
+
+exports[`String with semicolon 1`] = `
+"var foo = ';';
+"
+`;
+
+exports[`String with semicolon 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 8) -> (1, 8)",
+ "(1, 10) -> (1, 10)",
+ "(1, 13) -> (1, 13)",
+]
+`;
+
+exports[`Subclass handling 1`] = `
+"class Class extends Base {
+ constructor() {
+ }
+}
+"
+`;
+
+exports[`Subclass handling 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 6) -> (1, 7)",
+ "(1, 12) -> (1, 14)",
+ "(1, 20) -> (1, 23)",
+ "(1, 25) -> (1, 27)",
+ "(2, 2) -> (1, 28)",
+ "(2, 13) -> (1, 39)",
+ "(2, 14) -> (1, 40)",
+ "(2, 16) -> (1, 41)",
+ "(3, 2) -> (1, 42)",
+ "(4, 0) -> (1, 43)",
+]
+`;
+
+exports[`Switch statements 1`] = `
+"switch (x) {
+ case a:
+ foo();
+ break;
+ default:
+ bar()
+}
+"
+`;
+
+exports[`Switch statements 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 7) -> (1, 6)",
+ "(1, 8) -> (1, 7)",
+ "(1, 9) -> (1, 8)",
+ "(1, 11) -> (1, 9)",
+ "(2, 2) -> (1, 10)",
+ "(2, 7) -> (1, 15)",
+ "(2, 8) -> (1, 16)",
+ "(3, 4) -> (1, 17)",
+ "(3, 7) -> (1, 20)",
+ "(3, 8) -> (1, 21)",
+ "(3, 9) -> (1, 22)",
+ "(4, 4) -> (1, 23)",
+ "(4, 9) -> (1, 28)",
+ "(5, 2) -> (1, 29)",
+ "(5, 9) -> (1, 36)",
+ "(6, 4) -> (1, 37)",
+ "(6, 7) -> (1, 40)",
+ "(6, 8) -> (1, 41)",
+ "(7, 0) -> (1, 42)",
+]
+`;
+
+exports[`Template literals 1`] = `
+"\`abc\${ JSON.stringify({
+ clas: 'testing'
+}) }def\`;
+{
+ a();
+}
+"
+`;
+
+exports[`Template literals 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 1) -> (1, 1)",
+ "(1, 4) -> (1, 4)",
+ "(1, 7) -> (1, 6)",
+ "(1, 11) -> (1, 10)",
+ "(1, 12) -> (1, 11)",
+ "(1, 21) -> (1, 20)",
+ "(1, 22) -> (1, 21)",
+ "(2, 2) -> (1, 22)",
+ "(2, 6) -> (1, 26)",
+ "(2, 8) -> (1, 28)",
+ "(3, 0) -> (1, 37)",
+ "(3, 1) -> (1, 38)",
+ "(3, 3) -> (1, 39)",
+ "(3, 4) -> (1, 40)",
+ "(3, 7) -> (1, 43)",
+ "(3, 8) -> (1, 44)",
+ "(4, 0) -> (1, 45)",
+ "(5, 2) -> (1, 46)",
+ "(5, 3) -> (1, 47)",
+ "(5, 4) -> (1, 48)",
+ "(5, 5) -> (1, 49)",
+ "(6, 0) -> (1, 50)",
+]
+`;
+
+exports[`Ternary operator 1`] = `
+"bar ? baz : bang;
+"
+`;
+
+exports[`Ternary operator 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 3)",
+ "(1, 6) -> (1, 4)",
+ "(1, 10) -> (1, 7)",
+ "(1, 12) -> (1, 8)",
+ "(1, 16) -> (1, 12)",
+]
+`;
+
+exports[`This property access 1`] = `
+"var foo = this.foo;
+"
+`;
+
+exports[`This property access 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 8) -> (1, 7)",
+ "(1, 10) -> (1, 8)",
+ "(1, 14) -> (1, 12)",
+ "(1, 15) -> (1, 13)",
+ "(1, 18) -> (1, 16)",
+]
+`;
+
+exports[`True assignment 1`] = `
+"var foo = true;
+"
+`;
+
+exports[`True assignment 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 8) -> (1, 7)",
+ "(1, 10) -> (1, 8)",
+ "(1, 14) -> (1, 12)",
+]
+`;
+
+exports[`Try/catch/finally statement 1`] = `
+"try {
+ dangerous()
+} catch (e) {
+ handle(e)
+} finally {
+ cleanup()
+}
+"
+`;
+
+exports[`Try/catch/finally statement 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 3)",
+ "(2, 2) -> (1, 4)",
+ "(2, 11) -> (1, 13)",
+ "(2, 12) -> (1, 14)",
+ "(3, 0) -> (1, 15)",
+ "(3, 2) -> (1, 16)",
+ "(3, 8) -> (1, 21)",
+ "(3, 9) -> (1, 22)",
+ "(3, 10) -> (1, 23)",
+ "(3, 12) -> (1, 24)",
+ "(4, 2) -> (1, 25)",
+ "(4, 8) -> (1, 31)",
+ "(4, 9) -> (1, 32)",
+ "(4, 10) -> (1, 33)",
+ "(5, 0) -> (1, 34)",
+ "(5, 2) -> (1, 35)",
+ "(5, 10) -> (1, 42)",
+ "(6, 2) -> (1, 43)",
+ "(6, 9) -> (1, 50)",
+ "(6, 10) -> (1, 51)",
+ "(7, 0) -> (1, 52)",
+]
+`;
+
+exports[`Undefined assignment 1`] = `
+"var i = undefined;
+"
+`;
+
+exports[`Undefined assignment 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 6) -> (1, 5)",
+ "(1, 8) -> (1, 6)",
+ "(1, 17) -> (1, 15)",
+]
+`;
+
+exports[`Unnamed class handling 1`] = `
+"let unnamed = class {
+ constructor() {
+ }
+}
+"
+`;
+
+exports[`Unnamed class handling 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 12) -> (1, 11)",
+ "(1, 14) -> (1, 12)",
+ "(1, 20) -> (1, 17)",
+ "(2, 2) -> (1, 18)",
+ "(2, 13) -> (1, 29)",
+ "(2, 14) -> (1, 30)",
+ "(2, 16) -> (1, 31)",
+ "(3, 2) -> (1, 32)",
+ "(4, 0) -> (1, 33)",
+]
+`;
+
+exports[`Void 0 assignment 1`] = `
+"var i = void 0;
+"
+`;
+
+exports[`Void 0 assignment 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 6) -> (1, 5)",
+ "(1, 8) -> (1, 6)",
+ "(1, 13) -> (1, 11)",
+ "(1, 14) -> (1, 12)",
+]
+`;
+
+exports[`With statement 1`] = `
+"with (obj) {
+ crock()
+}
+"
+`;
+
+exports[`With statement 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 5) -> (1, 4)",
+ "(1, 6) -> (1, 5)",
+ "(1, 9) -> (1, 8)",
+ "(1, 11) -> (1, 9)",
+ "(2, 2) -> (1, 10)",
+ "(2, 7) -> (1, 15)",
+ "(2, 8) -> (1, 16)",
+ "(3, 0) -> (1, 17)",
+]
+`;
+
+exports[`for..of loop 1`] = `
+"for (const x of [
+ 1,
+ 2,
+ 3
+]) {
+ console.log(x)
+}
+"
+`;
+
+exports[`for..of loop 2`] = `
+Array [
+ "(1, 0) -> (1, 0)",
+ "(1, 4) -> (1, 4)",
+ "(1, 5) -> (1, 5)",
+ "(1, 11) -> (1, 11)",
+ "(1, 13) -> (1, 13)",
+ "(1, 16) -> (1, 16)",
+ "(2, 2) -> (1, 17)",
+ "(2, 3) -> (1, 18)",
+ "(3, 2) -> (1, 19)",
+ "(3, 3) -> (1, 20)",
+ "(4, 2) -> (1, 21)",
+ "(5, 0) -> (1, 22)",
+ "(5, 1) -> (1, 23)",
+ "(5, 3) -> (1, 25)",
+ "(6, 2) -> (1, 27)",
+ "(6, 9) -> (1, 34)",
+ "(6, 10) -> (1, 35)",
+ "(6, 13) -> (1, 38)",
+ "(6, 14) -> (1, 39)",
+ "(6, 15) -> (1, 40)",
+ "(7, 0) -> (1, 42)",
+]
+`;
diff --git a/devtools/client/debugger/src/workers/pretty-print/tests/prettyFast.spec.js b/devtools/client/debugger/src/workers/pretty-print/tests/prettyFast.spec.js
new file mode 100644
index 0000000000..f1f7a1c635
--- /dev/null
+++ b/devtools/client/debugger/src/workers/pretty-print/tests/prettyFast.spec.js
@@ -0,0 +1,434 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+/*
+ * Copyright 2013 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE.md or:
+ * http://opensource.org/licenses/BSD-2-Clause
+ */
+import { prettyFast } from "../pretty-fast";
+import { SourceMapConsumer } from "devtools/client/shared/vendor/source-map/source-map";
+
+const cases = [
+ {
+ name: "Simple function",
+ input: "function foo() { bar(); }",
+ },
+ {
+ name: "Nested function",
+ input: "function foo() { function bar() { debugger; } bar(); }",
+ },
+ {
+ name: "Immediately invoked function expression",
+ input: "(function(){thingy()}())",
+ },
+ {
+ name: "Single line comment",
+ input: `
+ // Comment
+ function foo() { bar(); }`,
+ },
+ {
+ name: "Multi line comment",
+ input: `
+ /* Comment
+ more comment */
+ function foo() { bar(); }
+ `,
+ },
+ { name: "Null assignment", input: "var i=null;" },
+ { name: "Undefined assignment", input: "var i=undefined;" },
+ { name: "Void 0 assignment", input: "var i=void 0;" },
+ {
+ name: "This property access",
+ input: "var foo=this.foo;\n",
+ },
+
+ {
+ name: "True assignment",
+ input: "var foo=true;\n",
+ },
+
+ {
+ name: "False assignment",
+ input: "var foo=false;\n",
+ },
+
+ {
+ name: "For loop",
+ input: "for (var i = 0; i < n; i++) { console.log(i); }",
+ },
+
+ {
+ name: "for..of loop",
+ input: "for (const x of [1,2,3]) { console.log(x) }",
+ },
+
+ {
+ name: "String with semicolon",
+ input: "var foo = ';';\n",
+ },
+
+ {
+ name: "String with quote",
+ input: 'var foo = "\'";\n',
+ },
+
+ {
+ name: "Function calls",
+ input: "var result=func(a,b,c,d);",
+ },
+
+ {
+ name: "Regexp",
+ input: "var r=/foobar/g;",
+ },
+
+ {
+ name: "In operator",
+ input: "if(foo in bar){doThing()}",
+ output: "if (foo in bar) {\n" + " doThing()\n" + "}\n",
+ },
+ {
+ name: "With statement",
+ input: "with(obj){crock()}",
+ },
+ {
+ name: "New expression",
+ input: "var foo=new Foo();",
+ },
+ {
+ name: "Continue/break statements",
+ input: "while(1){if(x){continue}if(y){break}if(z){break foo}}",
+ },
+ {
+ name: "Instanceof",
+ input: "var a=x instanceof y;",
+ },
+ {
+ name: "Binary operators",
+ input: "var a=5*30;var b=5>>3;",
+ },
+ {
+ name: "Delete",
+ input: "delete obj.prop;",
+ },
+
+ {
+ name: "Try/catch/finally statement",
+ input: "try{dangerous()}catch(e){handle(e)}finally{cleanup()}",
+ },
+ {
+ name: "If/else statement",
+ input: "if(c){then()}else{other()}",
+ },
+ {
+ name: "If/else without curlies",
+ input: "if(c) a else b",
+ },
+ {
+ name: "Objects",
+ input: `
+ var o={a:1,
+ b:2};`,
+ },
+ {
+ name: "Do/while loop",
+ input: "do{x}while(y)",
+ },
+ {
+ name: "Arrays",
+ input: "var a=[1,2,3];",
+ },
+ {
+ name: "Arrays and spread operator",
+ input: "var a=[1,...[2,3],...[], 4];",
+ },
+ {
+ name: "Empty object/array literals",
+ input: `let a=[];const b={};c={...{},d: 42};for(let x of []){for(let y in {}){}}`,
+ },
+ {
+ name: "Code that relies on ASI",
+ input: `
+ var foo = 10
+ var bar = 20
+ function g() {
+ a()
+ b()
+ }`,
+ },
+ {
+ name: "Ternary operator",
+ input: "bar?baz:bang;",
+ },
+ {
+ name: "Switch statements",
+ input: "switch(x){case a:foo();break;default:bar()}",
+ },
+
+ {
+ name: "Multiple single line comments",
+ input: `function f() {
+ // a
+ // b
+ // c
+ }`,
+ },
+ {
+ name: "Indented multiline comment",
+ input: `function foo() {
+ /**
+ * java doc style comment
+ * more comment
+ */
+ bar();
+ }`,
+ },
+ {
+ name: "ASI return",
+ input: `function f() {
+ return
+ {}
+ }`,
+ },
+ {
+ name: "Non-ASI property access",
+ input: `[1,2,3]
+ [0]`,
+ },
+ {
+ name: "Non-ASI in",
+ input: `'x'
+ in foo`,
+ },
+
+ {
+ name: "Non-ASI function call",
+ input: `f
+ ()`,
+ },
+ {
+ name: "Non-ASI new",
+ input: `new
+ F()`,
+ },
+ {
+ name: "Getter and setter literals",
+ input: "var obj={get foo(){return this._foo},set foo(v){this._foo=v}}",
+ },
+ {
+ name: "Escaping backslashes in strings",
+ input: "'\\\\'\n",
+ },
+ {
+ name: "Escaping carriage return in strings",
+ input: "'\\r'\n",
+ },
+ {
+ name: "Escaping tab in strings",
+ input: "'\\t'\n",
+ },
+ {
+ name: "Escaping vertical tab in strings",
+ input: "'\\v'\n",
+ },
+ {
+ name: "Escaping form feed in strings",
+ input: "'\\f'\n",
+ },
+ {
+ name: "Escaping null character in strings",
+ input: "'\\0'\n",
+ },
+ {
+ name: "Bug 977082 - space between grouping operator and dot notation",
+ input: `JSON.stringify(3).length;
+ ([1,2,3]).length;
+ (new Date()).toLocaleString();`,
+ },
+ {
+ name: "Bug 975477 don't move end of line comments to next line",
+ input: `switch (request.action) {
+ case 'show': //$NON-NLS-0$
+ if (localStorage.hideicon !== 'true') { //$NON-NLS-0$
+ chrome.pageAction.show(sender.tab.id);
+ }
+ break;
+ case 'hide': /*Multiline
+ Comment */
+ break;
+ default:
+ console.warn('unknown request'); //$NON-NLS-0$
+ // don't respond if you don't understand the message.
+ return;
+ }`,
+ },
+ {
+ name: "Const handling",
+ input: "const d = 'yes';\n",
+ },
+ {
+ name: "Let handling without value",
+ input: "let d;\n",
+ },
+ {
+ name: "Let handling with value",
+ input: "let d = 'yes';\n",
+ },
+ {
+ name: "Template literals",
+ // issue in acorn
+ input: "`abc${JSON.stringify({clas: 'testing'})}def`;{a();}",
+ },
+ {
+ name: "Class handling",
+ input: "class Class{constructor(){}}",
+ },
+ {
+ name: "Subclass handling",
+ input: "class Class extends Base{constructor(){}}",
+ },
+ {
+ name: "Unnamed class handling",
+ input: "let unnamed=class{constructor(){}}",
+ },
+ {
+ name: "Named class handling",
+ input: "let unnamed=class Class{constructor(){}}",
+ },
+ {
+ name: "Class extension within a function",
+ input: "(function() { class X extends Y { constructor() {} } })()",
+ },
+ {
+ name: "Bug 1261971 - indentation after switch statement",
+ input: "{switch(x){}if(y){}done();}",
+ },
+ {
+ name: "Bug 1206633 - spaces in for of",
+ input: "for (let tab of tabs) {}",
+ },
+ {
+ name: "Bug pretty-sure-3 - escaping line and paragraph separators",
+ input: "x = '\\u2029\\u2028';",
+ },
+ {
+ name: "Bug pretty-sure-4 - escaping null character before digit",
+ input: "x = '\\u00001';",
+ },
+ {
+ name: "Bug pretty-sure-5 - empty multiline comment shouldn't throw exception",
+ input: `{
+ /*
+ */
+ return;
+ }`,
+ },
+ {
+ name: "Bug pretty-sure-6 - inline comment shouldn't move parenthesis to next line",
+ input: `return /* inline comment */ (
+ 1+1);`,
+ },
+ {
+ name: "Bug pretty-sure-7 - accessing a literal number property requires a space",
+ input: "0..toString()+x.toString();",
+ },
+ {
+ name: "Bug pretty-sure-8 - return and yield only accept arguments when on the same line",
+ input: `{
+ return
+ (x)
+ yield
+ (x)
+ yield
+ *x
+ }`,
+ },
+ {
+ name: "Bug pretty-sure-9 - accept unary operator at start of file",
+ input: "+ 0",
+ },
+ {
+ name: "Stack-keyword property access",
+ input: "foo.a=1.1;foo.do.switch.case.default=2.2;foo.b=3.3;\n",
+ },
+ {
+ name: "Dot handling with let which is identifier name",
+ input: "y.let.let = 1.23;\n",
+ },
+ {
+ name: "Dot handling with keywords which are identifier name",
+ input: "y.await.break.const.delete.else.return.new.yield = 1.23;\n",
+ },
+ {
+ name: "Optional chaining parsing support",
+ input: "x?.y?.z?.['a']?.check();\n",
+ },
+ {
+ name: "Private fields parsing support",
+ input: `
+ class MyClass {
+ constructor(a) {
+ this.#a = a;this.#b = Math.random();this.ab = this.#getAB();
+ }
+ #a
+ #b = "default value"
+ static #someStaticPrivate
+ #getA() {
+ return this.#a;
+ }
+ #getAB() {
+ return this.#getA()+this.
+ #b
+ }
+ }
+ `,
+ },
+ {
+ name: "Long parenthesis",
+ input: `
+ if (thisIsAVeryLongVariable && thisIsAnotherOne || yetAnotherVeryLongVariable) {
+ (thisIsAnotherOne = thisMayReturnNull() || "hi", thisIsAVeryLongVariable = 42, yetAnotherVeryLongVariable && doSomething(true /* do it well */,thisIsAVeryLongVariable, thisIsAnotherOne, yetAnotherVeryLongVariable))
+ }
+ for (let thisIsAnotherVeryLongVariable = 0; i < thisIsAnotherVeryLongVariable.length; thisIsAnotherVeryLongVariable++) {}
+ const x = ({thisIsAnotherVeryLongPropertyName: "but should not cause the paren to be a line delimiter"})
+ `,
+ },
+ {
+ name: "Fat arrow function",
+ input: `
+ const a = () => 42
+ addEventListener("click", e => { return false });
+ const sum = (c,d) => c+d
+ `,
+ },
+];
+
+const includesOnly = cases.find(({ only }) => only);
+
+for (const { name, input, only, skip } of cases) {
+ if ((includesOnly && !only) || skip) {
+ continue;
+ }
+ test(name, async () => {
+ const actual = prettyFast(input, {
+ indent: " ",
+ url: "test.js",
+ });
+
+ expect(actual.code).toMatchSnapshot();
+
+ const smc = await new SourceMapConsumer(actual.map.toJSON());
+ const mappings = [];
+ smc.eachMapping(
+ ({ generatedColumn, generatedLine, originalColumn, originalLine }) => {
+ mappings.push(
+ `(${originalLine}, ${originalColumn}) -> (${generatedLine}, ${generatedColumn})`
+ );
+ }
+ );
+ expect(mappings).toMatchSnapshot();
+ });
+}
diff --git a/devtools/client/debugger/src/workers/pretty-print/worker.js b/devtools/client/debugger/src/workers/pretty-print/worker.js
new file mode 100644
index 0000000000..1c5587db9f
--- /dev/null
+++ b/devtools/client/debugger/src/workers/pretty-print/worker.js
@@ -0,0 +1,98 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
+
+import { workerHandler } from "../../../../shared/worker-utils";
+import { prettyFast } from "./pretty-fast";
+
+var { SourceMapGenerator } = require("source-map");
+
+const sourceMapGeneratorByTaskId = new Map();
+
+function prettyPrint({ url, indent, sourceText }) {
+ const { code, map: sourceMapGenerator } = prettyFast(sourceText, {
+ url,
+ indent,
+ });
+
+ return {
+ code,
+ sourceMap: sourceMapGenerator.toJSON(),
+ };
+}
+
+function prettyPrintInlineScript({
+ taskId,
+ url,
+ indent,
+ sourceText,
+ originalStartLine,
+ originalStartColumn,
+ generatedStartLine,
+}) {
+ let taskSourceMapGenerator;
+ if (!sourceMapGeneratorByTaskId.has(taskId)) {
+ taskSourceMapGenerator = new SourceMapGenerator({ file: url });
+ sourceMapGeneratorByTaskId.set(taskId, taskSourceMapGenerator);
+ } else {
+ taskSourceMapGenerator = sourceMapGeneratorByTaskId.get(taskId);
+ }
+
+ const { code } = prettyFast(sourceText, {
+ url,
+ indent,
+ sourceMapGenerator: taskSourceMapGenerator,
+ /*
+ * By default prettyPrint will trim the text, and we'd have the pretty text displayed
+ * just after the script tag, e.g.:
+ *
+ * ```
+ * <script>if (true) {
+ * something()
+ * }
+ * </script>
+ * ```
+ *
+ * We want the text to start on a new line, so prepend a line break, so we get
+ * something like:
+ *
+ * ```
+ * <script>
+ * if (true) {
+ * something()
+ * }
+ * </script>
+ * ```
+ */
+ prefixWithNewLine: true,
+ originalStartLine,
+ originalStartColumn,
+ generatedStartLine,
+ });
+
+ // When a taskId was passed, we only return the pretty printed text.
+ // The source map should be retrieved with getSourceMapForTask.
+ return code;
+}
+
+/**
+ * Get the source map for a pretty-print task
+ *
+ * @param {Integer} taskId: The taskId that was used to call prettyPrint
+ * @returns {Object} A source map object
+ */
+function getSourceMapForTask(taskId) {
+ if (!sourceMapGeneratorByTaskId.has(taskId)) {
+ return null;
+ }
+
+ const taskSourceMapGenerator = sourceMapGeneratorByTaskId.get(taskId);
+ sourceMapGeneratorByTaskId.delete(taskId);
+ return taskSourceMapGenerator.toJSON();
+}
+
+self.onmessage = workerHandler({
+ prettyPrint,
+ prettyPrintInlineScript,
+ getSourceMapForTask,
+});