diff options
Diffstat (limited to 'devtools/shared/pretty-fast')
-rw-r--r-- | devtools/shared/pretty-fast/UPGRADING.md | 11 | ||||
-rw-r--r-- | devtools/shared/pretty-fast/moz.build | 11 | ||||
-rw-r--r-- | devtools/shared/pretty-fast/pretty-fast.js | 873 | ||||
-rw-r--r-- | devtools/shared/pretty-fast/tests/unit/head_pretty-fast.js | 49 | ||||
-rw-r--r-- | devtools/shared/pretty-fast/tests/unit/test.js | 572 | ||||
-rw-r--r-- | devtools/shared/pretty-fast/tests/unit/xpcshell.ini | 8 |
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] |