summaryrefslogtreecommitdiffstats
path: root/devtools/shared/pretty-fast
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/shared/pretty-fast')
-rw-r--r--devtools/shared/pretty-fast/UPGRADING.md11
-rw-r--r--devtools/shared/pretty-fast/moz.build11
-rw-r--r--devtools/shared/pretty-fast/pretty-fast.js873
-rw-r--r--devtools/shared/pretty-fast/tests/unit/head_pretty-fast.js49
-rw-r--r--devtools/shared/pretty-fast/tests/unit/test.js572
-rw-r--r--devtools/shared/pretty-fast/tests/unit/xpcshell.ini8
6 files changed, 1524 insertions, 0 deletions
diff --git a/devtools/shared/pretty-fast/UPGRADING.md b/devtools/shared/pretty-fast/UPGRADING.md
new file mode 100644
index 000000000..9758096eb
--- /dev/null
+++ b/devtools/shared/pretty-fast/UPGRADING.md
@@ -0,0 +1,11 @@
+# UPGRADING
+
+1. `git clone https://github.com/mozilla/pretty-fast.git`
+
+2. Copy `pretty-fast/pretty-fast.js` to `devtools/shared/pretty-fast/pretty-fast.js`
+
+3. Copy `pretty-fast/test.js` to `devtools/shared/pretty-fast/tests/unit/test.js`
+
+4. If necessary, upgrade acorn (see devtools/shared/acorn/UPGRADING.md)
+
+5. Replace `acorn/dist/` with `acorn/` in pretty-fast.js. \ No newline at end of file
diff --git a/devtools/shared/pretty-fast/moz.build b/devtools/shared/pretty-fast/moz.build
new file mode 100644
index 000000000..3c72c8ff6
--- /dev/null
+++ b/devtools/shared/pretty-fast/moz.build
@@ -0,0 +1,11 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# 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/.
+
+XPCSHELL_TESTS_MANIFESTS += ['tests/unit/xpcshell.ini']
+
+DevToolsModules(
+ 'pretty-fast.js',
+)
diff --git a/devtools/shared/pretty-fast/pretty-fast.js b/devtools/shared/pretty-fast/pretty-fast.js
new file mode 100644
index 000000000..838a7b348
--- /dev/null
+++ b/devtools/shared/pretty-fast/pretty-fast.js
@@ -0,0 +1,873 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2; fill-column: 80 -*- */
+/*
+ * Copyright 2013 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE.md or:
+ * http://opensource.org/licenses/BSD-2-Clause
+ */
+(function (root, factory) {
+ "use strict";
+
+ if (typeof define === "function" && define.amd) {
+ define(factory);
+ } else if (typeof exports === "object") {
+ module.exports = factory();
+ } else {
+ root.prettyFast = factory();
+ }
+}(this, function () {
+ "use strict";
+
+ var acorn = this.acorn || require("acorn/acorn");
+ var sourceMap = this.sourceMap || require("source-map");
+ var SourceNode = sourceMap.SourceNode;
+
+ // 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.
+ var PRE_ARRAY_LITERAL_TOKENS = {
+ "typeof": true,
+ "void": true,
+ "delete": true,
+ "case": true,
+ "do": true,
+ "=": true,
+ "in": true,
+ "{": true,
+ "*": true,
+ "/": true,
+ "%": true,
+ "else": true,
+ ";": true,
+ "++": true,
+ "--": true,
+ "+": true,
+ "-": true,
+ "~": true,
+ "!": true,
+ ":": true,
+ "?": true,
+ ">>": true,
+ ">>>": true,
+ "<<": true,
+ "||": true,
+ "&&": true,
+ "<": true,
+ ">": true,
+ "<=": true,
+ ">=": true,
+ "instanceof": true,
+ "&": true,
+ "^": true,
+ "|": true,
+ "==": true,
+ "!=": true,
+ "===": true,
+ "!==": true,
+ ",": true,
+
+ "}": 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 lastToken
+ * The last token we added to the pretty printed results.
+ *
+ * @returns Boolean
+ * True if we believe it is an array literal, false otherwise.
+ */
+ function isArrayLiteral(token, lastToken) {
+ if (token.type.label != "[") {
+ return false;
+ }
+ if (!lastToken) {
+ return true;
+ }
+ if (lastToken.type.isAssign) {
+ return true;
+ }
+ return !!PRE_ARRAY_LITERAL_TOKENS[
+ lastToken.type.keyword || lastToken.type.label
+ ];
+ }
+
+ // If any of these tokens are followed by a token on a new line, we know that
+ // ASI cannot happen.
+ var PREVENT_ASI_AFTER_TOKENS = {
+ // Binary operators
+ "*": true,
+ "/": true,
+ "%": true,
+ "+": true,
+ "-": true,
+ "<<": true,
+ ">>": true,
+ ">>>": true,
+ "<": true,
+ ">": true,
+ "<=": true,
+ ">=": true,
+ "instanceof": true,
+ "in": true,
+ "==": true,
+ "!=": true,
+ "===": true,
+ "!==": true,
+ "&": true,
+ "^": true,
+ "|": true,
+ "&&": true,
+ "||": true,
+ ",": true,
+ ".": true,
+ "=": true,
+ "*=": true,
+ "/=": true,
+ "%=": true,
+ "+=": true,
+ "-=": true,
+ "<<=": true,
+ ">>=": true,
+ ">>>=": true,
+ "&=": true,
+ "^=": true,
+ "|=": true,
+ // Unary operators
+ "delete": true,
+ "void": true,
+ "typeof": true,
+ "~": true,
+ "!": true,
+ "new": true,
+ // Function calls and grouped expressions
+ "(": true
+ };
+
+ // If any of these tokens are on a line after the token before it, we know
+ // that ASI cannot happen.
+ var PREVENT_ASI_BEFORE_TOKENS = {
+ // Binary operators
+ "*": true,
+ "/": true,
+ "%": true,
+ "<<": true,
+ ">>": true,
+ ">>>": true,
+ "<": true,
+ ">": true,
+ "<=": true,
+ ">=": true,
+ "instanceof": true,
+ "in": true,
+ "==": true,
+ "!=": true,
+ "===": true,
+ "!==": true,
+ "&": true,
+ "^": true,
+ "|": true,
+ "&&": true,
+ "||": true,
+ ",": true,
+ ".": true,
+ "=": true,
+ "*=": true,
+ "/=": true,
+ "%=": true,
+ "+=": true,
+ "-=": true,
+ "<<=": true,
+ ">>=": true,
+ ">>>=": true,
+ "&=": true,
+ "^=": true,
+ "|=": true,
+ // Function calls
+ "(": true
+ };
+
+ /**
+ * Determines if Automatic Semicolon Insertion (ASI) occurs between these
+ * tokens.
+ *
+ * @param Object token
+ * The current token.
+ * @param Object lastToken
+ * The last token we added to the pretty printed results.
+ *
+ * @returns Boolean
+ * True if we believe ASI occurs.
+ */
+ function isASI(token, lastToken) {
+ if (!lastToken) {
+ return false;
+ }
+ if (token.loc.start.line === lastToken.loc.start.line) {
+ return false;
+ }
+ if (PREVENT_ASI_AFTER_TOKENS[
+ lastToken.type.label || lastToken.type.keyword
+ ]) {
+ return false;
+ }
+ if (PREVENT_ASI_BEFORE_TOKENS[token.type.label || token.type.keyword]) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Determine if we have encountered a getter or setter.
+ *
+ * @param Object token
+ * The current token. If this is a getter or setter, it would be the
+ * property name.
+ * @param Object lastToken
+ * The last token we added to the pretty printed results. If this is a
+ * getter or setter, it would be the `get` or `set` keyword
+ * respectively.
+ * @param Array stack
+ * The stack of open parens/curlies/brackets/etc.
+ *
+ * @returns Boolean
+ * True if this is a getter or setter.
+ */
+ function isGetterOrSetter(token, lastToken, stack) {
+ return stack[stack.length - 1] == "{"
+ && lastToken
+ && lastToken.type.label == "name"
+ && (lastToken.value == "get" || lastToken.value == "set")
+ && token.type.label == "name";
+ }
+
+ /**
+ * 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) {
+ if (token.isArrayLiteral) {
+ return true;
+ }
+ var ttl = token.type.label;
+ var top = stack[stack.length - 1];
+ return ttl == ";" && top != "("
+ || ttl == "{"
+ || ttl == "," && top != "("
+ || ttl == ":" && (top == "case" || top == "default");
+ }
+
+ /**
+ * 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.
+ * @param Function write
+ * The function to write to the pretty printed results.
+ * @param Array stack
+ * The stack of open parens/curlies/brackets/etc.
+ *
+ * @returns Boolean
+ * Returns true if we added a newline to result, false in all other
+ * cases.
+ */
+ function appendNewline(token, write, stack) {
+ if (isLineDelimiter(token, stack)) {
+ write("\n", token.loc.start.line, token.loc.start.column);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Determines if we need to add a space between the last 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 lastToken
+ * The last token added to the pretty printed code.
+ */
+ function needsSpaceAfter(token, lastToken) {
+ if (lastToken) {
+ if (lastToken.type.isLoop) {
+ return true;
+ }
+ if (lastToken.type.isAssign) {
+ return true;
+ }
+ if (lastToken.type.binop != null) {
+ return true;
+ }
+
+ var ltt = lastToken.type.label;
+ if (ltt == "?") {
+ return true;
+ }
+ if (ltt == ":") {
+ return true;
+ }
+ if (ltt == ",") {
+ return true;
+ }
+ if (ltt == ";") {
+ return true;
+ }
+
+ var ltk = lastToken.type.keyword;
+ if (ltk != null) {
+ if (ltk == "break" || ltk == "continue" || ltk == "return") {
+ return token.type.label != ";";
+ }
+ if (ltk != "debugger"
+ && ltk != "null"
+ && ltk != "true"
+ && ltk != "false"
+ && ltk != "this"
+ && ltk != "default") {
+ return true;
+ }
+ }
+
+ if (ltt == ")" && (token.type.label != ")"
+ && token.type.label != "]"
+ && token.type.label != ";"
+ && token.type.label != ","
+ && token.type.label != ".")) {
+ return true;
+ }
+ }
+
+ if (token.type.isAssign) {
+ return true;
+ }
+ if (token.type.binop != null) {
+ return true;
+ }
+ if (token.type.label == "?") {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * 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.
+ * @param Object lastToken
+ * The last token we added to the pretty printed code.
+ * @param Boolean addedNewline
+ * Whether we added a newline after adding the last token to the pretty
+ * printed code.
+ * @param Function write
+ * The function to write pretty printed code to the result SourceNode.
+ * @param Object options
+ * The options object.
+ * @param Number indentLevel
+ * The number of indents deep we are.
+ * @param Array stack
+ * The stack of open curlies, brackets, etc.
+ */
+ function prependWhiteSpace(token, lastToken, addedNewline, write, options,
+ indentLevel, stack) {
+ var ttk = token.type.keyword;
+ var ttl = token.type.label;
+ var newlineAdded = addedNewline;
+ var ltt = lastToken ? lastToken.type.label : null;
+
+ // 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 (lastToken && ltt == "}") {
+ if (ttk == "while" && stack[stack.length - 1] == "do") {
+ write(" ",
+ lastToken.loc.start.line,
+ lastToken.loc.start.column);
+ } else if (ttk == "else" ||
+ ttk == "catch" ||
+ ttk == "finally") {
+ write(" ",
+ lastToken.loc.start.line,
+ lastToken.loc.start.column);
+ } else if (ttl != "(" &&
+ ttl != ";" &&
+ ttl != "," &&
+ ttl != ")" &&
+ ttl != ".") {
+ write("\n",
+ lastToken.loc.start.line,
+ lastToken.loc.start.column);
+ newlineAdded = true;
+ }
+ }
+
+ if (isGetterOrSetter(token, lastToken, stack)) {
+ write(" ",
+ lastToken.loc.start.line,
+ lastToken.loc.start.column);
+ }
+
+ if (ttl == ":" && stack[stack.length - 1] == "?") {
+ write(" ",
+ lastToken.loc.start.line,
+ lastToken.loc.start.column);
+ }
+
+ if (lastToken && ltt != "}" && ttk == "else") {
+ write(" ",
+ lastToken.loc.start.line,
+ lastToken.loc.start.column);
+ }
+
+ function ensureNewline() {
+ if (!newlineAdded) {
+ write("\n",
+ lastToken.loc.start.line,
+ lastToken.loc.start.column);
+ newlineAdded = true;
+ }
+ }
+
+ if (isASI(token, lastToken)) {
+ ensureNewline();
+ }
+
+ if (decrementsIndent(ttl, stack)) {
+ ensureNewline();
+ }
+
+ if (newlineAdded) {
+ if (ttk == "case" || ttk == "default") {
+ write(repeat(options.indent, indentLevel - 1),
+ token.loc.start.line,
+ token.loc.start.column);
+ } else {
+ write(repeat(options.indent, indentLevel),
+ token.loc.start.line,
+ token.loc.start.column);
+ }
+ } else if (needsSpaceAfter(token, lastToken)) {
+ write(" ",
+ lastToken.loc.start.line,
+ lastToken.loc.start.column);
+ }
+ }
+
+ /**
+ * Repeat the `str` string `n` times.
+ *
+ * @param String str
+ * The string to be repeated.
+ * @param Number n
+ * The number of times to repeat the string.
+ *
+ * @returns String
+ * The repeated string.
+ */
+ function repeat(str, n) {
+ var result = "";
+ while (n > 0) {
+ if (n & 1) {
+ result += str;
+ }
+ n >>= 1;
+ str += str;
+ }
+ return result;
+ }
+
+ /**
+ * Make sure that we output the escaped character combination inside string
+ * literals instead of various problematic characters.
+ */
+ var sanitize = (function () {
+ var escapeCharacters = {
+ // Backslash
+ "\\": "\\\\",
+ // Newlines
+ "\n": "\\n",
+ // Carriage return
+ "\r": "\\r",
+ // Tab
+ "\t": "\\t",
+ // Vertical tab
+ "\v": "\\v",
+ // Form feed
+ "\f": "\\f",
+ // Null character
+ "\0": "\\0",
+ // Single quotes
+ "'": "\\'"
+ };
+
+ var regExpString = "("
+ + Object.keys(escapeCharacters)
+ .map(function (c) { return escapeCharacters[c]; })
+ .join("|")
+ + ")";
+ var escapeCharactersRegExp = new RegExp(regExpString, "g");
+
+ return function (str) {
+ return str.replace(escapeCharactersRegExp, function (_, c) {
+ return escapeCharacters[c];
+ });
+ };
+ }());
+ /**
+ * Add the given token to the pretty printed results.
+ *
+ * @param Object token
+ * The token to add.
+ * @param Function write
+ * The function to write pretty printed code to the result SourceNode.
+ */
+ function addToken(token, write) {
+ if (token.type.label == "string") {
+ write("'" + sanitize(token.value) + "'",
+ token.loc.start.line,
+ token.loc.start.column);
+ } else if (token.type.label == "regexp") {
+ write(String(token.value.value),
+ token.loc.start.line,
+ token.loc.start.column);
+ } else {
+ write(String(token.value != null ? token.value : token.type.label),
+ token.loc.start.line,
+ token.loc.start.column);
+ }
+ }
+
+ /**
+ * Returns true if the given token type belongs on the stack.
+ */
+ function belongsOnStack(token) {
+ var ttl = token.type.label;
+ var ttk = token.type.keyword;
+ return ttl == "{"
+ || ttl == "("
+ || ttl == "["
+ || ttl == "?"
+ || ttk == "do"
+ || ttk == "switch"
+ || ttk == "case"
+ || ttk == "default";
+ }
+
+ /**
+ * Returns true if the given token should cause us to pop the stack.
+ */
+ function shouldStackPop(token, stack) {
+ var ttl = token.type.label;
+ var ttk = token.type.keyword;
+ var top = stack[stack.length - 1];
+ return ttl == "]"
+ || ttl == ")"
+ || ttl == "}"
+ || (ttl == ":" && (top == "case" || top == "default" || top == "?"))
+ || (ttk == "while" && top == "do");
+ }
+
+ /**
+ * Returns true if the given token type should cause us to decrement the
+ * indent level.
+ */
+ function decrementsIndent(tokenType, stack) {
+ return tokenType == "}"
+ || (tokenType == "]" && stack[stack.length - 1] == "[\n");
+ }
+
+ /**
+ * Returns true if the given token should cause us to increment the indent
+ * level.
+ */
+ function incrementsIndent(token) {
+ return token.type.label == "{"
+ || token.isArrayLiteral
+ || token.type.keyword == "switch";
+ }
+
+ /**
+ * Add a comment to the pretty printed code.
+ *
+ * @param Function write
+ * The function to write pretty printed code to the result SourceNode.
+ * @param Number indentLevel
+ * The number of indents deep we are.
+ * @param Object options
+ * The options object.
+ * @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 Number column
+ * The column number the comment appeared on.
+ */
+ function addComment(write, indentLevel, options, block, text, line, column) {
+ var indentString = repeat(options.indent, indentLevel);
+
+ write(indentString, line, column);
+ if (block) {
+ write("/*");
+ write(text
+ .split(new RegExp("/\n" + indentString + "/", "g"))
+ .join("\n" + indentString));
+ write("*/");
+ } else {
+ write("//");
+ write(text);
+ }
+ write("\n");
+ }
+
+ /**
+ * The main function.
+ *
+ * @param String input
+ * The ugly JS code we want to pretty print.
+ * @param Object options
+ * The options object. Provides configurability of the pretty
+ * printing. Properties:
+ * - url: The URL string of the ugly JS code.
+ * - indent: The string to indent code by.
+ *
+ * @returns Object
+ * An object with the following properties:
+ * - code: The pretty printed code string.
+ * - map: A SourceMapGenerator instance.
+ */
+ return function prettyFast(input, options) {
+ // The level of indents deep we are.
+ var indentLevel = 0;
+
+ // We will accumulate the pretty printed code in this SourceNode.
+ var result = new SourceNode();
+
+ /**
+ * Write a pretty printed string to the result SourceNode.
+ *
+ * We buffer our writes so that we only create one mapping for each line in
+ * the source map. This enhances performance by avoiding extraneous mapping
+ * serialization, and flattening the tree that
+ * `SourceNode#toStringWithSourceMap` will have to recursively walk. When
+ * timing how long it takes to pretty print jQuery, this optimization
+ * brought the time down from ~390 ms to ~190ms!
+ *
+ * @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.
+ */
+ var write = (function () {
+ var buffer = [];
+ var bufferLine = -1;
+ var bufferColumn = -1;
+ return function write(str, line, column) {
+ if (line != null && bufferLine === -1) {
+ bufferLine = line;
+ }
+ if (column != null && bufferColumn === -1) {
+ bufferColumn = column;
+ }
+ buffer.push(str);
+
+ if (str == "\n") {
+ var lineStr = "";
+ for (var i = 0, len = buffer.length; i < len; i++) {
+ lineStr += buffer[i];
+ }
+ result.add(new SourceNode(bufferLine, bufferColumn, options.url,
+ lineStr));
+ buffer.splice(0, buffer.length);
+ bufferLine = -1;
+ bufferColumn = -1;
+ }
+ };
+ }());
+
+ // Whether or not we added a newline on after we added the last token.
+ var addedNewline = false;
+
+ // The current token we will be adding to the pretty printed code.
+ var token;
+
+ // Shorthand for token.type.label, so we don't have to repeatedly access
+ // properties.
+ var ttl;
+
+ // Shorthand for token.type.keyword, so we don't have to repeatedly access
+ // properties.
+ var ttk;
+
+ // The last token we added to the pretty printed code.
+ var lastToken;
+
+ // 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"
+ // - "do"
+ // - "?"
+ // - "switch"
+ // - "case"
+ // - "default"
+ //
+ // The difference between "[" and "[\n" is that "[\n" is used when we are
+ // treating "[" and "]" tokens as line delimiters and should increment and
+ // decrement the indent level when we find them.
+ var stack = [];
+
+ // 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]
+ var tokenQueue = [];
+
+ var tokens = acorn.tokenizer(input, {
+ locations: true,
+ sourceFile: options.url,
+ onComment: function (block, text, start, end, startLoc, endLoc) {
+ tokenQueue.push({
+ type: {},
+ comment: true,
+ block: block,
+ text: text,
+ loc: { start: startLoc, end: endLoc }
+ });
+ }
+ });
+
+ for (;;) {
+ token = tokens.getToken();
+ tokenQueue.push(token);
+ if (token.type.label == "eof") {
+ break;
+ }
+ }
+
+ for (var i = 0; i < tokenQueue.length; i++) {
+ token = tokenQueue[i];
+
+ if (token.comment) {
+ var commentIndentLevel = indentLevel;
+ if (lastToken && (lastToken.loc.end.line == token.loc.start.line)) {
+ commentIndentLevel = 0;
+ write(" ");
+ }
+ addComment(write, commentIndentLevel, options, token.block, token.text,
+ token.loc.start.line, token.loc.start.column);
+ addedNewline = true;
+ continue;
+ }
+
+ ttk = token.type.keyword;
+ ttl = token.type.label;
+
+ if (ttl == "eof") {
+ if (!addedNewline) {
+ write("\n");
+ }
+ break;
+ }
+
+ token.isArrayLiteral = isArrayLiteral(token, lastToken);
+
+ if (belongsOnStack(token)) {
+ if (token.isArrayLiteral) {
+ stack.push("[\n");
+ } else {
+ stack.push(ttl || ttk);
+ }
+ }
+
+ if (decrementsIndent(ttl, stack)) {
+ indentLevel--;
+ if (ttl == "}"
+ && stack.length > 1
+ && stack[stack.length - 2] == "switch") {
+ indentLevel--;
+ }
+ }
+
+ prependWhiteSpace(token, lastToken, addedNewline, write, options,
+ indentLevel, stack);
+ addToken(token, write);
+
+ // If the next token is going to be a comment starting on the same line,
+ // then no need to add one here
+ var nextToken = tokenQueue[i + 1];
+ if (!nextToken || !nextToken.comment || token.loc.end.line != nextToken.loc.start.line) {
+ addedNewline = appendNewline(token, write, stack);
+ }
+
+ if (shouldStackPop(token, stack)) {
+ stack.pop();
+ if (token == "}" && stack.length
+ && stack[stack.length - 1] == "switch") {
+ stack.pop();
+ }
+ }
+
+ if (incrementsIndent(token)) {
+ indentLevel++;
+ }
+
+ // Acorn's tokenizer re-uses tokens, so we have to copy the last token on
+ // every iteration. We follow acorn's lead here, and reuse the lastToken
+ // object the same way that acorn reuses the token object. This allows us
+ // to avoid allocations and minimize GC pauses.
+ if (!lastToken) {
+ lastToken = { loc: { start: {}, end: {} } };
+ }
+ lastToken.start = token.start;
+ lastToken.end = token.end;
+ lastToken.loc.start.line = token.loc.start.line;
+ lastToken.loc.start.column = token.loc.start.column;
+ lastToken.loc.end.line = token.loc.end.line;
+ lastToken.loc.end.column = token.loc.end.column;
+ lastToken.type = token.type;
+ lastToken.value = token.value;
+ lastToken.isArrayLiteral = token.isArrayLiteral;
+ }
+
+ return result.toStringWithSourceMap({ file: options.url });
+ };
+
+}.bind(this)));
diff --git a/devtools/shared/pretty-fast/tests/unit/head_pretty-fast.js b/devtools/shared/pretty-fast/tests/unit/head_pretty-fast.js
new file mode 100644
index 000000000..abde4b197
--- /dev/null
+++ b/devtools/shared/pretty-fast/tests/unit/head_pretty-fast.js
@@ -0,0 +1,49 @@
+"use strict";
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+var Cr = Components.results;
+
+const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
+
+this.sourceMap = require("source-map");
+this.acorn = require("acorn/acorn");
+this.prettyFast = require("devtools/shared/pretty-fast/pretty-fast");
+const { console } = Cu.import("resource://gre/modules/Console.jsm", {});
+
+// Register a console listener, so console messages don't just disappear
+// into the ether.
+var errorCount = 0;
+var listener = {
+ observe: function (aMessage) {
+ errorCount++;
+ try {
+ // If we've been given an nsIScriptError, then we can print out
+ // something nicely formatted, for tools like Emacs to pick up.
+ var scriptError = aMessage.QueryInterface(Ci.nsIScriptError);
+ dump(aMessage.sourceName + ":" + aMessage.lineNumber + ": " +
+ scriptErrorFlagsToKind(aMessage.flags) + ": " +
+ aMessage.errorMessage + "\n");
+ var string = aMessage.errorMessage;
+ } catch (x) {
+ // Be a little paranoid with message, as the whole goal here is to lose
+ // no information.
+ try {
+ var string = "" + aMessage.message;
+ } catch (x) {
+ var string = "<error converting error message to string>";
+ }
+ }
+
+ // Ignored until they are fixed in bug 1242968.
+ if (string.includes("JavaScript Warning")) {
+ return;
+ }
+
+ do_throw("head_pretty-fast.js got console message: " + string + "\n");
+ }
+};
+
+var consoleService = Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService);
+consoleService.registerListener(listener);
+
diff --git a/devtools/shared/pretty-fast/tests/unit/test.js b/devtools/shared/pretty-fast/tests/unit/test.js
new file mode 100644
index 000000000..b462726a2
--- /dev/null
+++ b/devtools/shared/pretty-fast/tests/unit/test.js
@@ -0,0 +1,572 @@
+/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; js-indent-level: 2; fill-column: 80 -*- */
+
+"use strict";
+
+/*
+ * Copyright 2013 Mozilla Foundation and contributors
+ * Licensed under the New BSD license. See LICENSE.md or:
+ * http://opensource.org/licenses/BSD-2-Clause
+ */
+var prettyFast = this.prettyFast || require("./pretty-fast");
+
+var testCases = [
+
+ {
+ name: "Simple function",
+ input: "function foo() { bar(); }",
+ output: "function foo() {\n" +
+ " bar();\n" +
+ "}\n",
+ mappings: [
+ // function foo() {
+ {
+ inputLine: 1,
+ outputLine: 1
+ },
+ // bar();
+ {
+ inputLine: 1,
+ outputLine: 2
+ },
+ // }
+ {
+ inputLine: 1,
+ outputLine: 3
+ },
+ ]
+ },
+
+ {
+ name: "Nested function",
+ input: "function foo() { function bar() { debugger; } bar(); }",
+ output: "function foo() {\n" +
+ " function bar() {\n" +
+ " debugger;\n" +
+ " }\n" +
+ " bar();\n" +
+ "}\n",
+ mappings: [
+ // function bar() {
+ {
+ inputLine: 1,
+ outputLine: 2
+ },
+ // debugger;
+ {
+ inputLine: 1,
+ outputLine: 3
+ },
+ // bar();
+ {
+ inputLine: 1,
+ outputLine: 5
+ },
+ ]
+ },
+
+ {
+ name: "Immediately invoked function expression",
+ input: "(function(){thingy()}())",
+ output: "(function () {\n" +
+ " thingy()\n" +
+ "}())\n"
+ },
+
+ {
+ name: "Single line comment",
+ input: "// Comment\n" +
+ "function foo() { bar(); }\n",
+ output: "// Comment\n" +
+ "function foo() {\n" +
+ " bar();\n" +
+ "}\n",
+ mappings: [
+ // // Comment
+ {
+ inputLine: 1,
+ outputLine: 1
+ }
+ ]
+ },
+
+ {
+ name: "Multi line comment",
+ input: "/* Comment\n" +
+ "more comment */\n" +
+ "function foo() { bar(); }\n",
+ output: "/* Comment\n" +
+ "more comment */\n" +
+ "function foo() {\n" +
+ " bar();\n" +
+ "}\n",
+ mappings: [
+ // /* Comment
+ {
+ inputLine: 1,
+ outputLine: 1
+ },
+ // \nmore comment */
+ {
+ inputLine: 1,
+ outputLine: 2
+ }
+ ]
+ },
+
+ {
+ name: "Null assignment",
+ input: "var i=null;\n",
+ output: "var i = null;\n",
+ mappings: [
+ {
+ inputLine: 1,
+ outputLine: 1
+ }
+ ]
+ },
+
+ {
+ name: "Undefined assignment",
+ input: "var i=undefined;\n",
+ output: "var i = undefined;\n"
+ },
+
+ {
+ name: "Void 0 assignment",
+ input: "var i=void 0;\n",
+ output: "var i = void 0;\n"
+ },
+
+ {
+ name: "This property access",
+ input: "var foo=this.foo;\n",
+ output: "var foo = this.foo;\n"
+ },
+
+ {
+ name: "True assignment",
+ input: "var foo=true;\n",
+ output: "var foo = true;\n"
+ },
+
+ {
+ name: "False assignment",
+ input: "var foo=false;\n",
+ output: "var foo = false;\n"
+ },
+
+ {
+ name: "For loop",
+ input: "for (var i = 0; i < n; i++) { console.log(i); }",
+ output: "for (var i = 0; i < n; i++) {\n" +
+ " console.log(i);\n" +
+ "}\n",
+ mappings: [
+ // for (var i = 0; i < n; i++) {
+ {
+ inputLine: 1,
+ outputLine: 1
+ },
+ // console.log(i);
+ {
+ inputLine: 1,
+ outputLine: 2
+ },
+ ]
+ },
+
+ {
+ name: "String with semicolon",
+ input: "var foo = ';';\n",
+ output: "var foo = ';';\n"
+ },
+
+ {
+ name: "String with quote",
+ input: "var foo = \"'\";\n",
+ output: "var foo = '\\'';\n"
+ },
+
+ {
+ name: "Function calls",
+ input: "var result=func(a,b,c,d);",
+ output: "var result = func(a, b, c, d);\n"
+ },
+
+ {
+ name: "Regexp",
+ input: "var r=/foobar/g;",
+ output: "var r = /foobar/g;\n"
+ },
+
+ {
+ 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()}",
+ output: "with (obj) {\n" +
+ " crock()\n" +
+ "}\n"
+ },
+
+ {
+ name: "New expression",
+ input: "var foo=new Foo();",
+ output: "var foo = new Foo();\n"
+ },
+
+ {
+ name: "Continue/break statements",
+ input: "while(1){if(x){continue}if(y){break}if(z){break foo}}",
+ output: "while (1) {\n" +
+ " if (x) {\n" +
+ " continue\n" +
+ " }\n" +
+ " if (y) {\n" +
+ " break\n" +
+ " }\n" +
+ " if (z) {\n" +
+ " break foo\n" +
+ " }\n" +
+ "}\n"
+ },
+
+ {
+ name: "Instanceof",
+ input: "var a=x instanceof y;",
+ output: "var a = x instanceof y;\n"
+ },
+
+ {
+ name: "Binary operators",
+ input: "var a=5*30;var b=5>>3;",
+ output: "var a = 5 * 30;\n" +
+ "var b = 5 >> 3;\n"
+ },
+
+ {
+ name: "Delete",
+ input: "delete obj.prop;",
+ output: "delete obj.prop;\n"
+ },
+
+ {
+ name: "Try/catch/finally statement",
+ input: "try{dangerous()}catch(e){handle(e)}finally{cleanup()}",
+ output: "try {\n" +
+ " dangerous()\n" +
+ "} catch (e) {\n" +
+ " handle(e)\n" +
+ "} finally {\n" +
+ " cleanup()\n" +
+ "}\n"
+ },
+
+ {
+ name: "If/else statement",
+ input: "if(c){then()}else{other()}",
+ output: "if (c) {\n" +
+ " then()\n" +
+ "} else {\n" +
+ " other()\n" +
+ "}\n"
+ },
+
+ {
+ name: "If/else without curlies",
+ input: "if(c) a else b",
+ output: "if (c) a else b\n"
+ },
+
+ {
+ name: "Objects",
+ input: "var o={a:1,\n" +
+ " b:2};",
+ output: "var o = {\n" +
+ " a: 1,\n" +
+ " b: 2\n" +
+ "};\n",
+ mappings: [
+ // a: 1,
+ {
+ inputLine: 1,
+ outputLine: 2
+ },
+ // b: 2
+ {
+ inputLine: 2,
+ outputLine: 3
+ },
+ ]
+ },
+
+ {
+ name: "Do/while loop",
+ input: "do{x}while(y)",
+ output: "do {\n" +
+ " x\n" +
+ "} while (y)\n"
+ },
+
+ {
+ name: "Arrays",
+ input: "var a=[1,2,3];",
+ output: "var a = [\n" +
+ " 1,\n" +
+ " 2,\n" +
+ " 3\n" +
+ "];\n"
+ },
+
+ {
+ name: "Code that relies on ASI",
+ input: "var foo = 10\n" +
+ "var bar = 20\n" +
+ "function g() {\n" +
+ " a()\n" +
+ " b()\n" +
+ "}",
+ output: "var foo = 10\n" +
+ "var bar = 20\n" +
+ "function g() {\n" +
+ " a()\n" +
+ " b()\n" +
+ "}\n"
+ },
+
+ {
+ name: "Ternary operator",
+ input: "bar?baz:bang;",
+ output: "bar ? baz : bang;\n"
+ },
+
+ {
+ name: "Switch statements",
+ input: "switch(x){case a:foo();break;default:bar()}",
+ output: "switch (x) {\n" +
+ " case a:\n" +
+ " foo();\n" +
+ " break;\n" +
+ " default:\n" +
+ " bar()\n" +
+ "}\n"
+ },
+
+ {
+ name: "Multiple single line comments",
+ input: "function f() {\n" +
+ " // a\n" +
+ " // b\n" +
+ " // c\n" +
+ "}\n",
+ output: "function f() {\n" +
+ " // a\n" +
+ " // b\n" +
+ " // c\n" +
+ "}\n",
+ },
+
+ {
+ name: "Indented multiline comment",
+ input: "function foo() {\n" +
+ " /**\n" +
+ " * java doc style comment\n" +
+ " * more comment\n" +
+ " */\n" +
+ " bar();\n" +
+ "}\n",
+ output: "function foo() {\n" +
+ " /**\n" +
+ " * java doc style comment\n" +
+ " * more comment\n" +
+ " */\n" +
+ " bar();\n" +
+ "}\n",
+ },
+
+ {
+ name: "ASI return",
+ input: "function f() {\n" +
+ " return\n" +
+ " {}\n" +
+ "}\n",
+ output: "function f() {\n" +
+ " return\n" +
+ " {\n" +
+ " }\n" +
+ "}\n",
+ },
+
+ {
+ name: "Non-ASI property access",
+ input: "[1,2,3]\n" +
+ "[0]",
+ output: "[\n" +
+ " 1,\n" +
+ " 2,\n" +
+ " 3\n" +
+ "]\n" +
+ "[0]\n"
+ },
+
+ {
+ name: "Non-ASI in",
+ input: "'x'\n" +
+ "in foo",
+ output: "'x' in foo\n"
+ },
+
+ {
+ name: "Non-ASI function call",
+ input: "f\n" +
+ "()",
+ output: "f()\n"
+ },
+
+ {
+ name: "Non-ASI new",
+ input: "new\n" +
+ "F()",
+ output: "new F()\n"
+ },
+
+ {
+ name: "Getter and setter literals",
+ input: "var obj={get foo(){return this._foo},set foo(v){this._foo=v}}",
+ output: "var obj = {\n" +
+ " get foo() {\n" +
+ " return this._foo\n" +
+ " },\n" +
+ " set foo(v) {\n" +
+ " this._foo = v\n" +
+ " }\n" +
+ "}\n"
+ },
+
+ {
+ name: "Escaping backslashes in strings",
+ input: "'\\\\'\n",
+ output: "'\\\\'\n"
+ },
+
+ {
+ name: "Escaping carriage return in strings",
+ input: "'\\r'\n",
+ output: "'\\r'\n"
+ },
+
+ {
+ name: "Escaping tab in strings",
+ input: "'\\t'\n",
+ output: "'\\t'\n"
+ },
+
+ {
+ name: "Escaping vertical tab in strings",
+ input: "'\\v'\n",
+ output: "'\\v'\n"
+ },
+
+ {
+ name: "Escaping form feed in strings",
+ input: "'\\f'\n",
+ output: "'\\f'\n"
+ },
+
+ {
+ name: "Escaping null character in strings",
+ input: "'\\0'\n",
+ output: "'\\0'\n"
+ },
+
+ {
+ name: "Bug 977082 - space between grouping operator and dot notation",
+ input: "JSON.stringify(3).length;\n" +
+ "([1,2,3]).length;\n" +
+ "(new Date()).toLocaleString();\n",
+ output: "JSON.stringify(3).length;\n" +
+ "([1,\n" +
+ "2,\n" +
+ "3]).length;\n" +
+ "(new Date()).toLocaleString();\n"
+ },
+
+ {
+ name: "Bug 975477 don't move end of line comments to next line",
+ input: "switch (request.action) {\n" +
+ " case 'show': //$NON-NLS-0$\n" +
+ " if (localStorage.hideicon !== 'true') { //$NON-NLS-0$\n" +
+ " chrome.pageAction.show(sender.tab.id);\n" +
+ " }\n" +
+ " break;\n" +
+ " case 'hide': /*Multiline\n" +
+ " Comment */" +
+ " break;\n" +
+ " default:\n" +
+ " console.warn('unknown request'); //$NON-NLS-0$\n" +
+ " // don't respond if you don't understand the message.\n" +
+ " return;\n" +
+ "}\n",
+ output: "switch (request.action) {\n" +
+ " case 'show': //$NON-NLS-0$\n" +
+ " if (localStorage.hideicon !== 'true') { //$NON-NLS-0$\n" +
+ " chrome.pageAction.show(sender.tab.id);\n" +
+ " }\n" +
+ " break;\n" +
+ " case 'hide': /*Multiline\n" +
+ " Comment */\n" +
+ " break;\n" +
+ " default:\n" +
+ " console.warn('unknown request'); //$NON-NLS-0$\n" +
+ " // don't respond if you don't understand the message.\n" +
+ " return;\n" +
+ "}\n"
+ }
+
+];
+
+var sourceMap = this.sourceMap || require("source-map");
+
+function run_test() {
+ testCases.forEach(function (test) {
+ console.log(test.name);
+
+ var actual = prettyFast(test.input, {
+ indent: " ",
+ url: "test.js"
+ });
+
+ if (actual.code !== test.output) {
+ throw new Error("Expected:\n" + test.output
+ + "\nGot:\n" + actual.code);
+ }
+
+ if (test.mappings) {
+ var smc = new sourceMap.SourceMapConsumer(actual.map.toJSON());
+ test.mappings.forEach(function (m) {
+ var query = { line: m.outputLine, column: 0 };
+ var original = smc.originalPositionFor(query);
+ if (original.line != m.inputLine) {
+ throw new Error("Querying:\n" + JSON.stringify(query, null, 2) + "\n"
+ + "Expected line:\n" + m.inputLine + "\n"
+ + "Got:\n" + JSON.stringify(original, null, 2));
+ }
+ });
+ }
+ });
+ console.log("✓ All tests pass!");
+}
+
+// Only run the tests if this is node and we are running this file
+// directly. (Firefox's test runner will import this test file, and then call
+// run_test itself.)
+if (typeof require == "function" && typeof module == "object"
+ && require.main === module) {
+ run_test();
+}
diff --git a/devtools/shared/pretty-fast/tests/unit/xpcshell.ini b/devtools/shared/pretty-fast/tests/unit/xpcshell.ini
new file mode 100644
index 000000000..cb92b1825
--- /dev/null
+++ b/devtools/shared/pretty-fast/tests/unit/xpcshell.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+tags = devtools
+head = head_pretty-fast.js
+tail =
+firefox-appdir = browser
+skip-if = toolkit == 'android'
+
+[test.js]