/* * Copyright (C) 2016 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. 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 APPLE INC. AND ITS 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 APPLE INC. OR ITS 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. */ // There is no standard for tokenizer types in JavaScript ASTs. // This currently assumes Esprima tokens, ranges, and comments. // EsprimaFormatter = class EsprimaFormatter { constructor(sourceText, sourceType, indentString = " ") { this._success = false; let tree = (function() { try { return esprima.parse(sourceText, {attachComment: true, range: true, tokens: true, sourceType}); } catch (error) { return null; } })(); if (!tree) return; this._sourceText = sourceText; this._builder = null; let walker = new ESTreeWalker(this._before.bind(this), this._after.bind(this)); this._tokens = tree.tokens; this._tokensLength = this._tokens.length; this._tokenIndex = 0; this._lineEndings = sourceText.lineEndings(); this._lineEndingsIndex = 0; this._builder = new FormatterContentBuilder(indentString); this._builder.setOriginalLineEndings(this._lineEndings.slice()); walker.walk(tree); this._afterProgram(tree); this._builder.appendNewline(); this._success = true; } // Static static isWhitespace(ch) { return isECMAScriptWhitespace(ch) || isECMAScriptLineTerminator(ch); } // Public get success() { return this._success; } get formattedText() { if (!this._success) return null; return this._builder.formattedContent; } get sourceMapData() { if (!this._success) return null; return this._builder.sourceMapData; } // Private _insertNewlinesBeforeToken(token) { let force = false; while (token.range[0] > this._lineEndings[this._lineEndingsIndex]) { this._builder.appendNewline(force); this._lineEndingsIndex++; force = true; } } _insertComment(comment) { if (comment.type === "Line") this._builder.appendToken("//" + comment.value, comment.range[0]); else if (comment.type === "Block") this._builder.appendToken("/*" + comment.value + "*/", comment.range[0]); this._builder.appendNewline(); comment.__handled = true; } _insertSameLineTrailingComments(node) { let endOfLine = this._lineEndings[this._lineEndingsIndex]; for (let comment of node.trailingComments) { if (comment.range[0] > endOfLine) break; this._builder.removeLastNewline(); this._builder.appendSpace(); this._insertComment(comment); } } _insertCommentsAndNewlines(comments) { for (let comment of comments) { // A previous node may have handled this as a trailing comment. if (comment.__handled) continue; // We expect the comment to be ahead of the last line. // But if it is ahead of the next line ending, then it // was preceded by an empty line. So include that. if (comment.range[0] > this._lineEndings[this._lineEndingsIndex + 1]) this._builder.appendNewline(true); this._insertComment(comment); // Remove line endings for this comment. while (comment.range[1] > this._lineEndings[this._lineEndingsIndex]) this._lineEndingsIndex++; } } _before(node) { if (!node.parent) return; // Handle the tokens before this node, so in the context of our parent node. while (this._tokenIndex < this._tokensLength && this._tokens[this._tokenIndex].range[0] < node.range[0]) { let token = this._tokens[this._tokenIndex++]; this._insertNewlinesBeforeToken(token); this._handleTokenAtNode(token, node.parent); } if (node.leadingComments) this._insertCommentsAndNewlines(node.leadingComments); } _after(node) { // Handle any other tokens inside of this node before exiting. while (this._tokenIndex < this._tokensLength && this._tokens[this._tokenIndex].range[0] < node.range[1]) { let token = this._tokens[this._tokenIndex++]; this._insertNewlinesBeforeToken(token); this._handleTokenAtNode(token, node); } this._exitNode(node); if (node.trailingComments) this._insertSameLineTrailingComments(node); } _isInForHeader(node) { let parent = node.parent; if (!parent) return false; return (parent.type === "ForStatement" || parent.type === "ForInStatement" || parent.type === "ForOfStatement") && node !== parent.body; } _isRangeWhitespace(from, to) { let substring = this._sourceText.substring(from, to); for (let i = 0; i < substring.length; ++i) { if (!EsprimaFormatter.isWhitespace(substring.charCodeAt(i))) return false; } return true; } _handleTokenAtNode(token, node) { let builder = this._builder; let nodeType = node.type; let tokenType = token.type; let tokenValue = token.value; let tokenOffset = token.range[0]; // Very common types that just pass through. if (nodeType === "MemberExpression" || nodeType === "Literal" || nodeType === "ThisExpression" || nodeType === "UpdateExpression") { builder.appendToken(tokenValue, tokenOffset); return; } // Most identifiers just pass through, but a few are special. if (nodeType === "Identifier") { builder.appendToken(tokenValue, tokenOffset); if (tokenValue === "async" && node.parent.type === "Property" && node.parent.value.async && token.range[1] !== node.range[1]) builder.appendSpace(); return; } // Most newline handling is done with semicolons. However, we preserve // newlines so code relying on automatic semicolon insertion should // continue to work. if (tokenValue === ";") { // Avoid newlines for for loop header semicolons. if (nodeType === "ForStatement") { builder.appendToken(tokenValue, tokenOffset); // Do not include spaces in empty for loop header sections: for(;;) if (node.test || node.update) { if (node.test && this._isRangeWhitespace(token.range[1], node.test.range[0])) builder.appendSpace(); else if (node.update && this._isRangeWhitespace(token.range[1], node.update.range[0])) builder.appendSpace(); } return; } // Sometimes more specific nodes gets the semicolon inside a for loop header. // Avoid newlines. Example is a VariableDeclaration in: for (var a, b; ...; ...). if (this._isInForHeader(node)) { builder.appendToken(tokenValue, tokenOffset); builder.appendSpace(); return; } // Avoid newline for single statement arrow functions with semicolons. if (node.parent.type === "BlockStatement" && node.parent.body.length === 1 && node.parent.parent && node.parent.parent.type === "ArrowFunctionExpression") { builder.appendToken(tokenValue, tokenOffset); return; } builder.appendToken(tokenValue, tokenOffset); builder.appendNewline(); return; } if (nodeType === "CallExpression" || nodeType === "ArrayExpression" || nodeType === "ArrayPattern" || nodeType === "ObjectPattern" || nodeType === "SequenceExpression") { if (tokenValue === ",") { builder.appendToken(tokenValue, tokenOffset); builder.appendSpace(); return; } builder.appendToken(tokenValue, tokenOffset); return; } if (nodeType === "LogicalExpression" || nodeType === "BinaryExpression") { if (tokenValue !== "(" && tokenValue !== ")") { builder.appendSpace(); builder.appendToken(tokenValue, tokenOffset); builder.appendSpace(); return; } if (tokenType === "Keyword") { // in, instanceof, ... builder.appendSpace(); builder.appendToken(tokenValue, tokenOffset); builder.appendSpace(); return; } builder.appendToken(tokenValue, tokenOffset); return; } if (nodeType === "BlockStatement") { let isSingleStatementArrowFunction = node.parent.type === "ArrowFunctionExpression" && node.body.length === 1; if (tokenValue === "{") { // Class methods we put the opening brace on its own line. if (node.parent && node.parent.parent && node.parent.parent.type === "MethodDefinition" && node.body.length) { builder.appendNewline(); builder.appendToken(tokenValue, tokenOffset); builder.appendNewline(); builder.indent(); return; } builder.appendToken(tokenValue, tokenOffset); if (node.body.length && !isSingleStatementArrowFunction) builder.appendNewline(); builder.indent(); return; } if (tokenValue === "}") { if (node.body.length && !isSingleStatementArrowFunction) builder.appendNewline(); builder.dedent(); builder.appendToken(tokenValue, tokenOffset); return; } console.warn("Unexpected BlockStatement token", token); builder.appendToken(tokenValue, tokenOffset); return; } if (nodeType === "VariableDeclaration") { if (tokenValue === ",") { if (this._isInForHeader(node)) { builder.appendToken(tokenValue, tokenOffset); builder.appendSpace(); return; } builder.appendToken(tokenValue, tokenOffset); builder.appendNewline(); return; } builder.appendToken(tokenValue, tokenOffset); builder.appendSpace(); // If this is a multiple variable declaration, indent. if (node.declarations.length > 1 && !node.__autoDedent) { builder.indent(); node.__autoDedent = true; } return; } if (nodeType === "VariableDeclarator" || nodeType === "AssignmentExpression") { if (tokenType === "Punctuator") { let surroundWithSpaces = tokenValue !== "(" && tokenValue !== ")"; if (surroundWithSpaces) builder.appendSpace(); builder.appendToken(tokenValue, tokenOffset); if (surroundWithSpaces) builder.appendSpace(); return; } console.warn("Unexpected " + nodeType + " token", token); builder.appendToken(tokenValue, tokenOffset); return; } if (nodeType === "IfStatement") { if (tokenType === "Keyword") { if (tokenValue === "else") { if (node.__autoDedent) { builder.dedent(); node.__autoDedent = false; } builder.appendSpace(); builder.appendToken(tokenValue, tokenOffset); if (node.alternate && (node.alternate.type !== "BlockStatement" && node.alternate.type !== "IfStatement")) { builder.appendNewline(); builder.indent(); node.__autoDedent = true; } else builder.appendSpace(); return; } console.assert(tokenValue === "if", token); builder.appendToken(tokenValue, tokenOffset); builder.appendSpace(); return; } // The last ')' in if(){}. if (tokenValue === ")" && this._isRangeWhitespace(token.range[1], node.consequent.range[0])) { if (node.consequent.type === "BlockStatement") { // The block will handle indenting. builder.appendToken(tokenValue, tokenOffset); builder.appendSpace(); return; } builder.appendToken(tokenValue, tokenOffset); builder.appendNewline(); builder.indent(); node.__autoDedent = true; return; } builder.appendToken(tokenValue, tokenOffset); return; } if (nodeType === "ReturnStatement") { if (tokenValue === ";") { builder.appendToken(tokenValue, tokenOffset); return; } builder.appendToken(tokenValue, tokenOffset); if (node.argument) { // Multi-line LogicalExpressions (&& and ||) are a common style of return // statement that benefits from indentation. if (node.argument.type === "LogicalExpression" && !node.__autoDedent) { builder.indent(); node.__autoDedent = true; } builder.appendSpace(); } return; } if (nodeType === "FunctionDeclaration" || nodeType === "FunctionExpression") { if (tokenType === "Keyword") { console.assert(tokenValue === "function", token); builder.appendToken(tokenValue, tokenOffset); if (node.id) builder.appendSpace(); return; } if (tokenType === "Punctuator") { if (tokenValue === "*") { builder.removeLastWhitespace(); builder.appendToken(tokenValue, tokenOffset); builder.appendSpace(); return; } builder.appendToken(tokenValue, tokenOffset); if (tokenValue === ")" || tokenValue === ",") builder.appendSpace(); return; } if (tokenType === "Identifier" && tokenValue === "async") { builder.appendToken(tokenValue, tokenOffset); builder.appendSpace(); return; } builder.appendToken(tokenValue, tokenOffset); return; } if (nodeType === "WhileStatement" || nodeType === "WithStatement") { if (tokenValue === "while" || tokenValue === "with") { builder.appendToken(tokenValue, tokenOffset); builder.appendSpace(); return; } // The last ')' in while(){} or with(){}. if (tokenValue === ")" && this._isRangeWhitespace(token.range[1], node.body.range[0])) { if (node.body.type === "BlockStatement") { // The block will handle indenting. builder.appendToken(tokenValue, tokenOffset); builder.appendSpace(); return; } builder.appendToken(tokenValue, tokenOffset); builder.appendNewline(); builder.indent(); node.__autoDedent = true; return; } builder.appendToken(tokenValue, tokenOffset); return; } if (nodeType === "ForStatement" || nodeType === "ForOfStatement" || nodeType === "ForInStatement") { if (tokenValue === "for") { builder.appendToken(tokenValue, tokenOffset); builder.appendSpace(); return; } if (tokenValue === "in" || tokenValue === "of") { builder.appendSpace(); builder.appendToken(tokenValue, tokenOffset); builder.appendSpace(); return; } // The last ')' in for(){}. if (tokenValue === ")" && this._isRangeWhitespace(token.range[1], node.body.range[0])) { if (node.body.type === "BlockStatement") { // The block will handle indenting. builder.appendToken(tokenValue, tokenOffset); builder.appendSpace(); return; } builder.appendToken(tokenValue, tokenOffset); builder.appendNewline(); builder.indent(); node.__autoDedent = true; return; } builder.appendToken(tokenValue, tokenOffset); return; } if (nodeType === "SwitchStatement") { if (tokenValue === "switch") { builder.appendToken(tokenValue, tokenOffset); builder.appendSpace(); return; } if (tokenType === "Punctuator") { if (tokenValue === ")") { // FIXME: Would be nice to only add a space if this the ')' closing the discriminant: switch((1)){} builder.appendToken(tokenValue, tokenOffset); builder.appendSpace(); return; } if (tokenValue === "{") { builder.appendToken(tokenValue, tokenOffset); builder.appendNewline(); return; } if (tokenValue === "}") { builder.appendNewline(); builder.appendToken(tokenValue, tokenOffset); builder.appendNewline(); return; } } builder.appendToken(tokenValue, tokenOffset); return; } if (nodeType === "SwitchCase") { if (tokenType === "Keyword") { if (tokenValue === "case") { builder.appendToken(tokenValue, tokenOffset); builder.appendSpace(); return; } if (tokenValue === "default") { builder.appendToken(tokenValue, tokenOffset); return; } console.warn("Unexpected SwitchCase Keyword token", token); builder.appendToken(tokenValue, tokenOffset); return; } if (tokenValue === ":") { builder.appendToken(tokenValue, tokenOffset); builder.appendNewline(); if (node.consequent.length) { builder.indent(); node.__autoDedent = true; } return; } builder.appendToken(tokenValue, tokenOffset); return; } if (nodeType === "NewExpression") { if (tokenValue === "new") { builder.appendToken(tokenValue, tokenOffset); builder.appendSpace(); return; } if (tokenValue === ",") { builder.appendToken(tokenValue, tokenOffset); builder.appendSpace(); return; } builder.appendToken(tokenValue, tokenOffset); return; } if (nodeType === "DoWhileStatement") { if (tokenValue === "do") { builder.appendToken(tokenValue, tokenOffset); builder.appendSpace(); return; } if (tokenValue === "while") { builder.appendSpace(); builder.appendToken(tokenValue, tokenOffset); builder.appendSpace(); return; } builder.appendToken(tokenValue, tokenOffset); return; } if (nodeType === "ThrowStatement") { if (tokenValue === "throw") { builder.appendToken(tokenValue, tokenOffset); builder.appendSpace(); return; } builder.appendToken(tokenValue, tokenOffset); return; } if (nodeType === "UnaryExpression") { if (tokenType === "Keyword") { // typeof, instanceof, void builder.appendToken(tokenValue, tokenOffset); builder.appendSpace(); return; } builder.appendToken(tokenValue, tokenOffset); return; } if (nodeType === "ConditionalExpression") { if (tokenValue === "?" || tokenValue === ":") { builder.appendSpace(); builder.appendToken(tokenValue, tokenOffset); builder.appendSpace(); return; } builder.appendToken(tokenValue, tokenOffset); return; } if (nodeType === "ObjectExpression") { // FIXME: It would be nice to detect if node.properties is very short // and the node.properties themselves are very small then inline them // instead of always adding newlines. Objects like: {a}, {a:1} but // not objects like {a:function(){1;}}. if (tokenValue === "{") { builder.appendToken(tokenValue, tokenOffset); if (node.properties.length) { builder.appendNewline(); builder.indent(); } return; } if (tokenValue === "}") { if (node.properties.length) { builder.appendNewline(); builder.dedent(); } builder.appendToken(tokenValue, tokenOffset); return; } if (tokenValue === ",") { builder.appendToken(tokenValue, tokenOffset); if (node.properties.length) builder.appendNewline(); else builder.appendSpace(); return; } builder.appendToken(tokenValue, tokenOffset); return; } if (nodeType === "ArrowFunctionExpression") { if (tokenType === "Punctuator") { if (tokenValue === "=>") { builder.appendSpace(); builder.appendToken(tokenValue, tokenOffset); builder.appendSpace(); return; } if (tokenValue === ",") { builder.appendToken(tokenValue, tokenOffset); builder.appendSpace(); return; } } builder.appendToken(tokenValue, tokenOffset); if (tokenType === "Identifier" && tokenValue === "async") builder.appendSpace(); return; } if (nodeType === "AwaitExpression") { builder.appendToken(tokenValue, tokenOffset); if (tokenType === "Identifier" && tokenValue === "await") builder.appendSpace(); return; } if (nodeType === "Property") { console.assert(tokenValue === ":" || tokenValue === "get" || tokenValue === "set" || tokenValue === "async" || tokenValue === "*" || tokenValue === "[" || tokenValue === "]" || tokenValue === "(" || tokenValue === ")", token); builder.appendToken(tokenValue, tokenOffset); if (tokenValue === ":" || tokenValue === "get" || tokenValue === "set" || tokenValue === "async") builder.appendSpace(); return; } if (nodeType === "MethodDefinition") { console.assert(tokenValue === "static" || tokenValue === "get" || tokenValue === "set" || tokenValue === "async" || tokenValue === "*" || tokenValue === "[" || tokenValue === "]" || tokenValue === "(" || tokenValue === ")", token); builder.appendToken(tokenValue, tokenOffset); if (tokenValue === "static" || tokenValue === "get" || tokenValue === "set" || tokenValue === "async") builder.appendSpace(); return; } if (nodeType === "BreakStatement" || nodeType === "ContinueStatement") { builder.appendToken(tokenValue, tokenOffset); if (tokenType === "Keyword" && node.label) builder.appendSpace(); return; } if (nodeType === "LabeledStatement") { console.assert(tokenValue === ":", token); builder.appendToken(tokenValue, tokenOffset); builder.appendNewline(); return; } if (nodeType === "TryStatement") { if (tokenValue === "try") { builder.appendToken(tokenValue, tokenOffset); builder.appendSpace(); return; } if (tokenValue === "finally") { builder.appendSpace(); builder.appendToken(tokenValue, tokenOffset); builder.appendSpace(); return; } console.warn("Unexpected TryStatement token", token); builder.appendToken(tokenValue, tokenOffset); return; } if (nodeType === "CatchClause") { if (tokenValue === "catch") { builder.appendSpace(); builder.appendToken(tokenValue, tokenOffset); builder.appendSpace(); return; } if (tokenValue === ")") { // The block will handle indenting. builder.appendToken(tokenValue, tokenOffset); builder.appendSpace(); return; } builder.appendToken(tokenValue, tokenOffset); return; } if (nodeType === "ClassExpression" || nodeType === "ClassDeclaration") { if (tokenValue === "class") { builder.appendToken(tokenValue, tokenOffset); builder.appendSpace(); return; } if (tokenValue === "extends") { builder.appendSpace(); builder.appendToken(tokenValue, tokenOffset); builder.appendSpace(); return; } builder.appendToken(tokenValue, tokenOffset); return; } if (nodeType === "ClassBody") { if (tokenValue === "{") { if (node.parent.id) builder.appendSpace(); builder.appendToken(tokenValue, tokenOffset); if (node.body.length) builder.appendNewline(); builder.indent(); return; } if (tokenValue === "}") { if (node.body.length) builder.appendNewline(); builder.dedent(); builder.appendToken(tokenValue, tokenOffset); builder.appendNewline(); return; } console.warn("Unexpected ClassBody token", token); builder.appendToken(tokenValue, tokenOffset); return; } if (nodeType === "YieldExpression") { if (tokenType === "Keyword") { console.assert(tokenValue === "yield", token); builder.appendToken(tokenValue, tokenOffset); if (node.argument) builder.appendSpace(); return; } builder.appendToken(tokenValue, tokenOffset); return; } if (nodeType === "ImportDeclaration" || nodeType === "ExportNamedDeclaration") { if (tokenValue === "}" || (tokenType === "Identifier" && tokenValue === "from")) builder.appendSpace(); builder.appendToken(tokenValue, tokenOffset); if (tokenValue !== "}") builder.appendSpace(); return; } if (nodeType === "ExportSpecifier" || nodeType === "ImportSpecifier") { if (tokenType === "Identifier" && tokenValue === "as") builder.appendSpace(); builder.appendToken(tokenValue, tokenOffset); builder.appendSpace(); return; } if (nodeType === "ExportAllDeclaration" || nodeType === "ExportDefaultDeclaration" || nodeType === "ImportDefaultSpecifier" || nodeType === "ImportNamespaceSpecifier") { builder.appendToken(tokenValue, tokenOffset); if (tokenValue !== "(" && tokenValue !== ")") builder.appendSpace(); return; } // Include these here so we get only get warnings about unhandled nodes. if (nodeType === "ExpressionStatement" || nodeType === "SpreadElement" || nodeType === "SpreadProperty" || nodeType === "Super" || nodeType === "Import" || nodeType === "MetaProperty" || nodeType === "RestElement" || nodeType === "RestProperty" || nodeType === "TemplateElement" || nodeType === "TemplateLiteral" || nodeType === "DebuggerStatement" || nodeType === "AssignmentPattern") { builder.appendToken(tokenValue, tokenOffset); return; } // Warn about possible unhandled types. console.warn(nodeType, tokenValue); // Fallback behavior in case there are unhandled types. builder.appendToken(tokenValue, tokenOffset); if (tokenType === "Keyword") builder.appendSpace(); } _exitNode(node) { if (node.__autoDedent) this._builder.dedent(); if (node.type === "BlockStatement") { if (node.parent) { // Newline after if(){} if (node.parent.type === "IfStatement" && (!node.parent.alternate || node.parent.consequent !== node)) { this._builder.appendNewline(); return; } // Newline after for(){} if (node.parent.type === "ForStatement" || node.parent.type === "ForOfStatement" || node.parent.type === "ForInStatement") { this._builder.appendNewline(); return; } // Newline after while(){} if (node.parent.type === "WhileStatement") { this._builder.appendNewline(); return; } // Newline after function(){} if (node.parent.type === "FunctionDeclaration") { this._builder.appendNewline(); return; } // Newline after catch block in try{}catch(e){} if (node.parent.type === "CatchClause" && !node.parent.parent.finalizer) { this._builder.appendNewline(); return; } // Newline after finally block in try{}catch(e){}finally{} if (node.parent.type === "TryStatement" && node.parent.finalizer && node.parent.finalizer === node) { this._builder.appendNewline(); return; } // Newline after class body methods in class {method(){}} if (node.parent.parent && node.parent.parent.type === "MethodDefinition") { this._builder.appendNewline(); return; } // Newline after anonymous block inside a block or program. if (node.parent.type === "BlockStatement" || node.parent.type === "Program") { this._builder.appendNewline(); return; } } return; } } _afterProgram(programNode) { if (!programNode) return; console.assert(programNode.type === "Program"); // If a program ends with comments, Esprima puts those // comments on the last node of the body. However, if // a program is entirely comments, then they are // leadingComments on the program node. if (programNode.body.length) { let lastNode = programNode.body[programNode.body.length - 1]; if (lastNode.trailingComments) this._insertCommentsAndNewlines(lastNode.trailingComments); } else { if (programNode.leadingComments) this._insertCommentsAndNewlines(programNode.leadingComments); console.assert(!programNode.trailingComments); } } }; EsprimaFormatter.SourceType = { Script: "script", Module: "module", };