blob: 562107f08b6e05eb5bc423694bfa1f19bc9414cb [file] [log] [blame]
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
part of js_ast;
class JavaScriptPrintingOptions {
final bool utf8;
final bool shouldCompressOutput;
final bool minifyLocalVariables;
final bool preferSemicolonToNewlineInMinifiedOutput;
const JavaScriptPrintingOptions({
this.utf8 = false,
this.shouldCompressOutput = false,
this.minifyLocalVariables = false,
this.preferSemicolonToNewlineInMinifiedOutput = false,
});
}
/// An environment in which JavaScript printing is done. Provides emitting of
/// text and pre- and post-visit callbacks.
abstract class JavaScriptPrintingContext {
/// Signals an error. This should happen only for serious internal errors.
void error(String message) {
throw message;
}
/// Adds [string] to the output.
void emit(String string);
/// Callback for the start of printing of [node]. [startPosition] is the
/// position of the first non-whitespace character of [node].
///
/// [enterNode] is called in pre-traversal order.
void enterNode(Node node, int startPosition) {}
/// Callback for the end of printing of [node]. [startPosition] is the
/// position of the first non-whitespace character of [node] (also provided
/// in the [enterNode] callback), [endPosition] is the position immediately
/// following the last character of [node]. [closingPosition] is the
/// position of the ending delimiter of [node]. This is only provided for
/// [Fun] nodes and is `null` otherwise.
///
/// [enterNode] is called in post-traversal order.
void exitNode(
Node node, int startPosition, int endPosition, int closingPosition) {}
/// Should return `true` if the printing tolerates unfinalized deferred AST
/// nodes.
bool get isDebugContext => false;
}
/// A simple implementation of [JavaScriptPrintingContext] suitable for tests.
class SimpleJavaScriptPrintingContext extends JavaScriptPrintingContext {
final StringBuffer buffer = StringBuffer();
void emit(String string) {
buffer.write(string);
}
String getText() => buffer.toString();
}
class _DebugJavaScriptPrintingContext extends SimpleJavaScriptPrintingContext {
bool get isDebugContext => true;
}
String DebugPrint(Node node, {bool utf8 = false}) {
JavaScriptPrintingOptions options = JavaScriptPrintingOptions(utf8: utf8);
SimpleJavaScriptPrintingContext context = _DebugJavaScriptPrintingContext();
Printer printer = Printer(options, context);
printer.visit(node);
return context.getText();
}
class Printer implements NodeVisitor {
final JavaScriptPrintingOptions options;
final JavaScriptPrintingContext context;
final bool shouldCompressOutput;
final DanglingElseVisitor danglingElseVisitor;
final LocalNamer localNamer;
final bool isDebugContext;
int _charCount = 0;
bool inForInit = false;
bool atStatementBegin = false;
bool pendingSemicolon = false;
bool pendingSpace = false;
// The current indentation level.
int _indentLevel = 0;
// A cache of all indentation strings used so far.
final List<String> _indentList = [""];
static final identifierCharacterRegExp = RegExp(r'^[a-zA-Z_0-9$]');
static final expressionContinuationRegExp = RegExp(r'^[-+([]');
Printer(JavaScriptPrintingOptions options, JavaScriptPrintingContext context)
: options = options,
context = context,
isDebugContext = context.isDebugContext,
shouldCompressOutput = options.shouldCompressOutput,
danglingElseVisitor = DanglingElseVisitor(context),
localNamer = determineRenamer(
options.shouldCompressOutput, options.minifyLocalVariables);
static LocalNamer determineRenamer(
bool shouldCompressOutput, bool allowVariableMinification) {
return (shouldCompressOutput && allowVariableMinification)
? MinifyRenamer()
: IdentityNamer();
}
// The current indentation string.
String get indentation {
// Lazily add new indentation strings as required.
while (_indentList.length <= _indentLevel) {
_indentList.add(_indentList.last + " ");
}
return _indentList[_indentLevel];
}
void indentMore() {
_indentLevel++;
}
void indentLess() {
_indentLevel--;
}
/// Always emit a newline, even under `enableMinification`.
void forceLine() {
out("\n", isWhitespace: true);
}
/// Emits a newline for readability.
void lineOut() {
if (!shouldCompressOutput) forceLine();
}
void spaceOut() {
if (!shouldCompressOutput) out(" ", isWhitespace: true);
}
String lastAddedString = null;
int get lastCharCode {
if (lastAddedString == null) return 0;
assert(lastAddedString.length != 0);
return lastAddedString.codeUnitAt(lastAddedString.length - 1);
}
void out(String str, {bool isWhitespace = false}) {
if (str != "") {
if (pendingSemicolon) {
if (!shouldCompressOutput) {
_emit(";");
} else if (str != "}") {
// We want to output newline instead of semicolon because it makes
// the raw stack traces much easier to read and it also makes line-
// based tools like diff work much better. JavaScript will
// automatically insert the semicolon at the newline if it means a
// parsing error is avoided, so we can only do this trick if the
// next line is not something that can be glued onto a valid
// expression to make a new valid expression.
// If we're using the new emitter where most pretty printed code
// is escaped in strings, it is a lot easier to deal with semicolons
// than newlines because the former doesn't need escaping.
if (options.preferSemicolonToNewlineInMinifiedOutput ||
expressionContinuationRegExp.hasMatch(str)) {
_emit(";");
} else {
_emit("\n");
}
}
}
if (pendingSpace &&
(!shouldCompressOutput || identifierCharacterRegExp.hasMatch(str))) {
_emit(" ");
}
pendingSpace = false;
pendingSemicolon = false;
if (!isWhitespace) {
enterNode();
}
_emit(str);
lastAddedString = str;
}
}
void outLn(String str) {
out(str);
lineOut();
}
void outSemicolonLn() {
if (shouldCompressOutput) {
pendingSemicolon = true;
} else {
out(";");
forceLine();
}
}
void outIndent(String str) {
indent();
out(str);
}
void outIndentLn(String str) {
indent();
outLn(str);
}
void indent() {
if (!shouldCompressOutput) {
out(indentation, isWhitespace: true);
}
}
EnterExitNode currentNode;
void _emit(String text) {
context.emit(text);
_charCount += text.length;
}
void startNode(Node node) {
currentNode = EnterExitNode(currentNode, node);
if (node is DeferredExpression) {
if (!isDebugContext || node.isFinalized) {
startNode(node.value);
}
}
}
void enterNode() {
currentNode.addToNode(context, _charCount);
}
void endNode(Node node) {
if (node is DeferredExpression) {
if (!isDebugContext || node.isFinalized) {
endNode(node.value);
}
}
assert(currentNode.node == node);
currentNode = currentNode.exitNode(context, _charCount);
}
void visit(Node node) {
startNode(node);
node.accept(this);
endNode(node);
}
void visitCommaSeparated(List<Node> nodes, int hasRequiredType,
{bool newInForInit, bool newAtStatementBegin}) {
for (int i = 0; i < nodes.length; i++) {
if (i != 0) {
atStatementBegin = false;
out(",");
spaceOut();
}
visitNestedExpression(nodes[i], hasRequiredType,
newInForInit: newInForInit, newAtStatementBegin: newAtStatementBegin);
}
}
void visitAll(List<Node> nodes) {
nodes.forEach(visit);
}
Node _undefer(Node node) {
if (isDebugContext && !node.isFinalized) return node;
if (node is DeferredExpression) return _undefer(node.value);
return node;
}
@override
void visitProgram(Program program) {
if (program.body.isNotEmpty) {
visitAll(program.body);
}
}
Statement unwrapBlockIfSingleStatement(Statement body) {
Statement result = body;
while (result is Block) {
Block block = result;
if (block.statements.length != 1) break;
result = block.statements.single;
}
return result;
}
bool blockBody(Statement body, {bool needsSeparation, bool needsNewline}) {
if (body is Block) {
spaceOut();
blockOut(body, shouldIndent: false, needsNewline: needsNewline);
return true;
}
if (shouldCompressOutput && needsSeparation) {
// If [shouldCompressOutput] is false, then the 'lineOut' will insert
// the separation.
out(" ", isWhitespace: true);
} else {
lineOut();
}
indentMore();
visit(body);
indentLess();
return false;
}
/// Elide Blocks and DeferredStatements with Blocks as children.
void blockOutWithoutBraces(Node node) {
if (node is Block) {
startNode(node);
Block block = node;
block.statements.forEach(blockOutWithoutBraces);
endNode(node);
} else if (node is DeferredStatement) {
startNode(node);
blockOutWithoutBraces(node.statement);
endNode(node);
} else {
visit(node);
}
}
int blockOut(Block node, {bool shouldIndent, bool needsNewline}) {
if (shouldIndent) indent();
startNode(node);
out("{");
lineOut();
indentMore();
node.statements.forEach(blockOutWithoutBraces);
indentLess();
indent();
out("}");
int closingPosition = _charCount - 1;
endNode(node);
if (needsNewline) lineOut();
return closingPosition;
}
@override
void visitBlock(Block block) {
blockOut(block, shouldIndent: true, needsNewline: true);
}
@override
void visitExpressionStatement(ExpressionStatement node) {
indent();
visitNestedExpression(node.expression, EXPRESSION,
newInForInit: false, newAtStatementBegin: true);
outSemicolonLn();
}
@override
void visitEmptyStatement(EmptyStatement node) {
outIndentLn(";");
}
void ifOut(If node, bool shouldIndent) {
Statement then = unwrapBlockIfSingleStatement(node.then);
Statement elsePart = node.otherwise;
bool hasElse = node.hasElse;
// Handle dangling elses and a work-around for Android 4.0 stock browser.
// Android 4.0 requires braces for a single do-while in the `then` branch.
// See issue 10923.
if (hasElse) {
bool needsBraces = then.accept(danglingElseVisitor) || then is Do;
if (needsBraces) {
then = Block(<Statement>[then]);
}
}
if (shouldIndent) indent();
out("if");
spaceOut();
out("(");
visitNestedExpression(node.condition, EXPRESSION,
newInForInit: false, newAtStatementBegin: false);
out(")");
bool thenWasBlock =
blockBody(then, needsSeparation: false, needsNewline: !hasElse);
if (hasElse) {
if (thenWasBlock) {
spaceOut();
} else {
indent();
}
out("else");
if (elsePart is If) {
pendingSpace = true;
startNode(elsePart);
ifOut(elsePart, false);
endNode(elsePart);
} else {
blockBody(unwrapBlockIfSingleStatement(elsePart),
needsSeparation: true, needsNewline: true);
}
}
}
@override
void visitIf(If node) {
ifOut(node, true);
}
@override
void visitFor(For loop) {
outIndent("for");
spaceOut();
out("(");
if (loop.init != null) {
visitNestedExpression(loop.init, EXPRESSION,
newInForInit: true, newAtStatementBegin: false);
}
out(";");
if (loop.condition != null) {
spaceOut();
visitNestedExpression(loop.condition, EXPRESSION,
newInForInit: false, newAtStatementBegin: false);
}
out(";");
if (loop.update != null) {
spaceOut();
visitNestedExpression(loop.update, EXPRESSION,
newInForInit: false, newAtStatementBegin: false);
}
out(")");
blockBody(unwrapBlockIfSingleStatement(loop.body),
needsSeparation: false, needsNewline: true);
}
@override
void visitForIn(ForIn loop) {
outIndent("for");
spaceOut();
out("(");
visitNestedExpression(loop.leftHandSide, EXPRESSION,
newInForInit: true, newAtStatementBegin: false);
out(" in");
pendingSpace = true;
visitNestedExpression(loop.object, EXPRESSION,
newInForInit: false, newAtStatementBegin: false);
out(")");
blockBody(unwrapBlockIfSingleStatement(loop.body),
needsSeparation: false, needsNewline: true);
}
@override
void visitWhile(While loop) {
outIndent("while");
spaceOut();
out("(");
visitNestedExpression(loop.condition, EXPRESSION,
newInForInit: false, newAtStatementBegin: false);
out(")");
blockBody(unwrapBlockIfSingleStatement(loop.body),
needsSeparation: false, needsNewline: true);
}
@override
void visitDo(Do loop) {
outIndent("do");
if (blockBody(unwrapBlockIfSingleStatement(loop.body),
needsSeparation: true, needsNewline: false)) {
spaceOut();
} else {
indent();
}
out("while");
spaceOut();
out("(");
visitNestedExpression(loop.condition, EXPRESSION,
newInForInit: false, newAtStatementBegin: false);
out(")");
outSemicolonLn();
}
@override
void visitContinue(Continue node) {
if (node.targetLabel == null) {
outIndent("continue");
} else {
outIndent("continue ${node.targetLabel}");
}
outSemicolonLn();
}
@override
void visitBreak(Break node) {
if (node.targetLabel == null) {
outIndent("break");
} else {
outIndent("break ${node.targetLabel}");
}
outSemicolonLn();
}
@override
void visitReturn(Return node) {
if (node.value == null) {
outIndent("return");
} else {
outIndent("return");
pendingSpace = true;
visitNestedExpression(node.value, EXPRESSION,
newInForInit: false, newAtStatementBegin: false);
}
// Set the closing position to be before the optional semicolon.
currentNode.closingPosition = _charCount;
outSemicolonLn();
}
@override
void visitDartYield(DartYield node) {
if (node.hasStar) {
outIndent("yield*");
} else {
outIndent("yield");
}
pendingSpace = true;
visitNestedExpression(node.expression, EXPRESSION,
newInForInit: false, newAtStatementBegin: false);
outSemicolonLn();
}
@override
void visitThrow(Throw node) {
outIndent("throw");
pendingSpace = true;
visitNestedExpression(node.expression, EXPRESSION,
newInForInit: false, newAtStatementBegin: false);
outSemicolonLn();
}
@override
void visitTry(Try node) {
outIndent("try");
blockBody(node.body, needsSeparation: true, needsNewline: false);
if (node.catchPart != null) {
visit(node.catchPart);
}
if (node.finallyPart != null) {
spaceOut();
out("finally");
blockBody(node.finallyPart, needsSeparation: true, needsNewline: true);
} else {
lineOut();
}
}
@override
void visitCatch(Catch node) {
spaceOut();
out("catch");
spaceOut();
out("(");
visitNestedExpression(node.declaration, EXPRESSION,
newInForInit: false, newAtStatementBegin: false);
out(")");
blockBody(node.body, needsSeparation: false, needsNewline: false);
}
@override
void visitSwitch(Switch node) {
outIndent("switch");
spaceOut();
out("(");
visitNestedExpression(node.key, EXPRESSION,
newInForInit: false, newAtStatementBegin: false);
out(")");
spaceOut();
outLn("{");
indentMore();
visitAll(node.cases);
indentLess();
outIndentLn("}");
}
@override
void visitCase(Case node) {
outIndent("case");
pendingSpace = true;
visitNestedExpression(node.expression, EXPRESSION,
newInForInit: false, newAtStatementBegin: false);
outLn(":");
if (!node.body.statements.isEmpty) {
indentMore();
blockOutWithoutBraces(node.body);
indentLess();
}
}
@override
void visitDefault(Default node) {
outIndentLn("default:");
if (!node.body.statements.isEmpty) {
indentMore();
blockOutWithoutBraces(node.body);
indentLess();
}
}
@override
void visitLabeledStatement(LabeledStatement node) {
Statement body = unwrapBlockIfSingleStatement(node.body);
// `label: break label;`
// Does not work on IE. The statement is a nop, so replace it by an empty
// statement.
// See:
// https://connect.microsoft.com/IE/feedback/details/891889/parser-bugs
if (body is Break && body.targetLabel == node.label) {
visit(EmptyStatement());
return;
}
outIndent("${node.label}:");
blockBody(body, needsSeparation: false, needsNewline: true);
}
int functionOut(Fun fun, Node name, VarCollector vars) {
out("function");
if (name != null) {
out(" ");
// Name must be a [Decl]. Therefore only test for primary expressions.
visitNestedExpression(name, PRIMARY,
newInForInit: false, newAtStatementBegin: false);
}
localNamer.enterScope(vars);
out("(");
if (fun.params != null) {
visitCommaSeparated(fun.params, PRIMARY,
newInForInit: false, newAtStatementBegin: false);
}
out(")");
switch (fun.asyncModifier) {
case AsyncModifier.sync:
break;
case AsyncModifier.async:
out(' ', isWhitespace: true);
out('async');
break;
case AsyncModifier.syncStar:
out(' ', isWhitespace: true);
out('sync*');
break;
case AsyncModifier.asyncStar:
out(' ', isWhitespace: true);
out('async*');
break;
}
spaceOut();
int closingPosition =
blockOut(fun.body, shouldIndent: false, needsNewline: false);
localNamer.leaveScope();
return closingPosition;
}
@override
visitFunctionDeclaration(FunctionDeclaration declaration) {
VarCollector vars = VarCollector();
vars.visitFunctionDeclaration(declaration);
indent();
startNode(declaration.function);
currentNode.closingPosition =
functionOut(declaration.function, declaration.name, vars);
endNode(declaration.function);
lineOut();
}
visitNestedExpression(Expression node, int requiredPrecedence,
{bool newInForInit, bool newAtStatementBegin}) {
int precedenceLevel =
(isDebugContext && !node.isFinalized) ? CALL : node.precedenceLevel;
bool needsParentheses =
// a - (b + c).
(requiredPrecedence != EXPRESSION &&
precedenceLevel < requiredPrecedence) ||
// for (a = (x in o); ... ; ... ) { ... }
(newInForInit && node is Binary && node.op == "in") ||
// (function() { ... })().
// ({a: 2, b: 3}.toString()).
(newAtStatementBegin &&
(node is NamedFunction ||
node is FunctionExpression ||
node is ObjectInitializer));
if (needsParentheses) {
inForInit = false;
atStatementBegin = false;
out("(");
visit(node);
out(")");
} else {
inForInit = newInForInit;
atStatementBegin = newAtStatementBegin;
visit(node);
}
}
@override
visitVariableDeclarationList(VariableDeclarationList list) {
out("var ");
List<Node> nodes = list.declarations;
if (inForInit) {
visitCommaSeparated(nodes, ASSIGNMENT,
newInForInit: inForInit, newAtStatementBegin: false);
} else {
// Print 'big' declarations on their own line, while keeping adjacent
// small and uninitialized declarations on the same line.
bool useIndent = nodes.length > 1 && list.indentSplits;
if (useIndent) {
indentMore();
}
bool lastWasBig = false;
for (int i = 0; i < nodes.length; i++) {
Node node = nodes[i];
bool thisIsBig = !_isSmallInitialization(node);
if (i > 0) {
atStatementBegin = false;
out(",");
if (lastWasBig || thisIsBig) {
lineOut();
indent();
} else {
spaceOut();
}
}
visitNestedExpression(node, ASSIGNMENT,
newInForInit: inForInit, newAtStatementBegin: false);
lastWasBig = thisIsBig;
}
if (useIndent) {
indentLess();
}
}
}
bool _isSmallInitialization(Node node) {
if (node is VariableInitialization) {
if (node.value == null) return true;
Node value = _undefer(node.value);
if (value == null) return true;
if (value is This) return true;
if (value is LiteralNull) return true;
if (value is LiteralNumber) return true;
if (value is LiteralString && value.value.length <= 6) return true;
if (value is ObjectInitializer && value.properties.isEmpty) return true;
if (value is ArrayInitializer && value.elements.isEmpty) return true;
if (value is Name && value.name.length <= 6) return true;
}
return false;
}
void _outputIncDec(String op, Expression variable, [Expression alias]) {
if (op == '+') {
if (lastCharCode == charCodes.$PLUS) out(" ", isWhitespace: true);
out('++');
} else {
if (lastCharCode == charCodes.$MINUS) out(" ", isWhitespace: true);
out('--');
}
if (alias != null) startNode(alias);
visitNestedExpression(variable, UNARY,
newInForInit: inForInit, newAtStatementBegin: false);
if (alias != null) endNode(alias);
}
@override
visitAssignment(Assignment assignment) {
/// To print assignments like `a = a + 1` and `a = a + b` compactly as
/// `++a` and `a += b` in the face of [DeferredExpression]s we detect the
/// pattern of the undeferred assignment.
String op = assignment.op;
Node leftHandSide = _undefer(assignment.leftHandSide);
Node value = assignment.value;
Node rightHandSide = value == null ? value : _undefer(value);
if ((op == '+' || op == '-') &&
leftHandSide is VariableUse &&
rightHandSide is LiteralNumber &&
rightHandSide.value == "1") {
// Output 'a += 1' as '++a' and 'a -= 1' as '--a'.
_outputIncDec(op, assignment.leftHandSide);
return;
}
if (!assignment.isCompound &&
leftHandSide is VariableUse &&
rightHandSide is Binary) {
Node rLeft = _undefer(rightHandSide.left);
Node rRight = _undefer(rightHandSide.right);
String op = rightHandSide.op;
if (op == '+' ||
op == '-' ||
op == '/' ||
op == '*' ||
op == '%' ||
op == '^' ||
op == '&' ||
op == '|') {
if (rLeft is VariableUse && rLeft.name == leftHandSide.name) {
// Output 'a = a + 1' as '++a' and 'a = a - 1' as '--a'.
if ((op == '+' || op == '-') &&
rRight is LiteralNumber &&
rRight.value == "1") {
_outputIncDec(op, assignment.leftHandSide, rightHandSide.left);
return;
}
// Output 'a = a + b' as 'a += b'.
startNode(rightHandSide.left);
visitNestedExpression(assignment.leftHandSide, CALL,
newInForInit: inForInit, newAtStatementBegin: atStatementBegin);
endNode(rightHandSide.left);
spaceOut();
out(op);
out("=");
spaceOut();
visitNestedExpression(rRight, ASSIGNMENT,
newInForInit: inForInit, newAtStatementBegin: false);
return;
}
}
}
visitNestedExpression(assignment.leftHandSide, CALL,
newInForInit: inForInit, newAtStatementBegin: atStatementBegin);
if (value != null) {
spaceOut();
if (op != null) out(op);
out("=");
spaceOut();
visitNestedExpression(value, ASSIGNMENT,
newInForInit: inForInit, newAtStatementBegin: false);
}
}
@override
visitVariableInitialization(VariableInitialization initialization) {
visitNestedExpression(initialization.declaration, CALL,
newInForInit: inForInit, newAtStatementBegin: atStatementBegin);
if (initialization.value != null) {
spaceOut();
out("=");
spaceOut();
visitNestedExpression(initialization.value, ASSIGNMENT,
newInForInit: inForInit, newAtStatementBegin: false);
}
}
@override
visitConditional(Conditional cond) {
visitNestedExpression(cond.condition, LOGICAL_OR,
newInForInit: inForInit, newAtStatementBegin: atStatementBegin);
spaceOut();
out("?");
spaceOut();
// The then part is allowed to have an 'in'.
visitNestedExpression(cond.then, ASSIGNMENT,
newInForInit: false, newAtStatementBegin: false);
spaceOut();
out(":");
spaceOut();
visitNestedExpression(cond.otherwise, ASSIGNMENT,
newInForInit: inForInit, newAtStatementBegin: false);
}
@override
visitNew(New node) {
out("new ");
visitNestedExpression(node.target, LEFT_HAND_SIDE,
newInForInit: inForInit, newAtStatementBegin: false);
out("(");
visitCommaSeparated(node.arguments, ASSIGNMENT,
newInForInit: false, newAtStatementBegin: false);
out(")");
}
@override
visitCall(Call call) {
visitNestedExpression(call.target, CALL,
newInForInit: inForInit, newAtStatementBegin: atStatementBegin);
out("(");
visitCommaSeparated(call.arguments, ASSIGNMENT,
newInForInit: false, newAtStatementBegin: false);
out(")");
}
@override
void visitBinary(Binary binary) {
Expression left = binary.left;
Expression right = binary.right;
String op = binary.op;
int leftPrecedenceRequirement;
int rightPrecedenceRequirement;
bool leftSpace = true; // left<HERE>op right
switch (op) {
case ',':
// x, (y, z) <=> (x, y), z.
leftPrecedenceRequirement = EXPRESSION;
rightPrecedenceRequirement = EXPRESSION;
leftSpace = false;
break;
case "||":
leftPrecedenceRequirement = LOGICAL_OR;
// x || (y || z) <=> (x || y) || z.
rightPrecedenceRequirement = LOGICAL_OR;
break;
case "&&":
leftPrecedenceRequirement = LOGICAL_AND;
// x && (y && z) <=> (x && y) && z.
rightPrecedenceRequirement = LOGICAL_AND;
break;
case "|":
leftPrecedenceRequirement = BIT_OR;
// x | (y | z) <=> (x | y) | z.
rightPrecedenceRequirement = BIT_OR;
break;
case "^":
leftPrecedenceRequirement = BIT_XOR;
// x ^ (y ^ z) <=> (x ^ y) ^ z.
rightPrecedenceRequirement = BIT_XOR;
break;
case "&":
leftPrecedenceRequirement = BIT_AND;
// x & (y & z) <=> (x & y) & z.
rightPrecedenceRequirement = BIT_AND;
break;
case "==":
case "!=":
case "===":
case "!==":
leftPrecedenceRequirement = EQUALITY;
rightPrecedenceRequirement = RELATIONAL;
break;
case "<":
case ">":
case "<=":
case ">=":
case "instanceof":
case "in":
leftPrecedenceRequirement = RELATIONAL;
rightPrecedenceRequirement = SHIFT;
break;
case ">>":
case "<<":
case ">>>":
leftPrecedenceRequirement = SHIFT;
rightPrecedenceRequirement = ADDITIVE;
break;
case "+":
case "-":
leftPrecedenceRequirement = ADDITIVE;
// We cannot remove parenthesis for "+" because
// x + (y + z) <!=> (x + y) + z:
// Example:
// "a" + (1 + 2) => "a3";
// ("a" + 1) + 2 => "a12";
rightPrecedenceRequirement = MULTIPLICATIVE;
break;
case "*":
case "/":
case "%":
leftPrecedenceRequirement = MULTIPLICATIVE;
// We cannot remove parenthesis for "*" because of precision issues.
rightPrecedenceRequirement = UNARY;
break;
default:
context.error("Forgot operator: $op");
}
visitNestedExpression(left, leftPrecedenceRequirement,
newInForInit: inForInit, newAtStatementBegin: atStatementBegin);
if (op == "in" || op == "instanceof") {
// There are cases where the space is not required but without further
// analysis we cannot know.
out(" ", isWhitespace: true);
out(op);
out(" ", isWhitespace: true);
} else {
if (leftSpace) spaceOut();
out(op);
spaceOut();
}
visitNestedExpression(right, rightPrecedenceRequirement,
newInForInit: inForInit, newAtStatementBegin: false);
}
@override
void visitPrefix(Prefix unary) {
String op = unary.op;
switch (op) {
case "delete":
case "void":
case "typeof":
// There are cases where the space is not required but without further
// analysis we cannot know.
out(op);
out(" ", isWhitespace: true);
break;
case "+":
case "++":
if (lastCharCode == charCodes.$PLUS) out(" ", isWhitespace: true);
out(op);
break;
case "-":
case "--":
if (lastCharCode == charCodes.$MINUS) out(" ", isWhitespace: true);
out(op);
break;
default:
out(op);
}
visitNestedExpression(unary.argument, UNARY,
newInForInit: inForInit, newAtStatementBegin: false);
}
@override
void visitPostfix(Postfix postfix) {
visitNestedExpression(postfix.argument, CALL,
newInForInit: inForInit, newAtStatementBegin: atStatementBegin);
out(postfix.op);
}
@override
void visitVariableUse(VariableUse ref) {
out(localNamer.getName(ref.name));
}
@override
void visitThis(This node) {
out("this");
}
@override
void visitVariableDeclaration(VariableDeclaration decl) {
out(localNamer.getName(decl.name));
}
@override
void visitParameter(Parameter param) {
out(localNamer.getName(param.name));
}
bool isDigit(int charCode) {
return charCodes.$0 <= charCode && charCode <= charCodes.$9;
}
bool isValidJavaScriptId(String field) {
if (field.length == 0) return false;
// Ignore the leading and trailing string-delimiter.
for (int i = 0; i < field.length; i++) {
// TODO(floitsch): allow more characters.
int charCode = field.codeUnitAt(i);
if (!(charCodes.$a <= charCode && charCode <= charCodes.$z ||
charCodes.$A <= charCode && charCode <= charCodes.$Z ||
charCode == charCodes.$$ ||
charCode == charCodes.$_ ||
i > 0 && isDigit(charCode))) {
return false;
}
}
// TODO(floitsch): normally we should also check that the field is not a
// reserved word. We don't generate fields with reserved word names except
// for 'super'.
if (field == 'super') return false;
if (field == 'catch') return false;
return true;
}
@override
void visitAccess(PropertyAccess access) {
visitNestedExpression(access.receiver, CALL,
newInForInit: inForInit, newAtStatementBegin: atStatementBegin);
Node selector = _undefer(access.selector);
if (isDebugContext && !selector.isFinalized) {
_dotString(
access.selector, access.receiver, selector.nonfinalizedDebugText(),
assumeValid: true);
return;
}
if (selector is LiteralString) {
_dotString(access.selector, access.receiver, selector.value);
return;
}
if (selector is StringConcatenation) {
_dotString(access.selector, access.receiver,
_StringContentsCollector(isDebugContext).collect(selector));
return;
}
if (selector is Name) {
_dotString(access.selector, access.receiver, selector.name);
return;
}
out('[');
visitNestedExpression(access.selector, EXPRESSION,
newInForInit: false, newAtStatementBegin: false);
out(']');
}
void _dotString(Node selector, Node receiver, String selectorValue,
{bool assumeValid = false}) {
if (assumeValid || isValidJavaScriptId(selectorValue)) {
if (_undefer(receiver) is LiteralNumber &&
lastCharCode != charCodes.$CLOSE_PAREN) {
out(' ', isWhitespace: true);
}
out('.');
startNode(selector);
out(selectorValue);
endNode(selector);
} else {
out('[');
_handleString(selectorValue);
out(']');
}
}
@override
void visitNamedFunction(NamedFunction namedFunction) {
VarCollector vars = VarCollector();
vars.visitNamedFunction(namedFunction);
startNode(namedFunction.function);
int closingPosition = currentNode.closingPosition =
functionOut(namedFunction.function, namedFunction.name, vars);
endNode(namedFunction.function);
// Use closing position of `namedFunction.function` as the closing position
// of the named function itself.
currentNode.closingPosition = closingPosition;
}
@override
void visitFun(Fun fun) {
VarCollector vars = VarCollector();
vars.visitFun(fun);
currentNode.closingPosition = functionOut(fun, null, vars);
}
@override
void visitArrowFunction(ArrowFunction fun) {
VarCollector vars = VarCollector();
vars.visitArrowFunction(fun);
currentNode.closingPosition = arrowFunctionOut(fun, vars);
}
int arrowFunctionOut(ArrowFunction fun, VarCollector vars) {
// TODO: support static, get/set, async, and generators.
localNamer.enterScope(vars);
final List<Parameter> params = fun.params;
if (params.length == 1 && params.first is VariableReference) {
visitNestedExpression(params.single, ASSIGNMENT,
newInForInit: false, newAtStatementBegin: false);
} else {
out("(");
if (params != null) {
visitCommaSeparated(fun.params, PRIMARY,
newInForInit: false, newAtStatementBegin: false);
}
out(")");
}
spaceOut();
out("=>");
spaceOut();
int closingPosition;
Node body = fun.body;
// Simplify arrow functions that return a single expression.
// Note that this can result in some sourcemapped positions disappearing
// around the elided Return. See http://dartbug.com/47354
if (fun.implicitReturnAllowed && body is Block) {
final statement = unwrapBlockIfSingleStatement(body);
if (statement is Return) {
body = statement.value;
}
}
if (body is Block) {
closingPosition =
blockOut(body, shouldIndent: false, needsNewline: false);
} else {
// Object initializers require parentheses to disambiguate
// AssignmentExpression from FunctionBody. See:
// https://tc39.github.io/ecma262/#sec-arrow-function-definitions
bool needsParens = body is ObjectInitializer;
if (needsParens) out("(");
visitNestedExpression(body, ASSIGNMENT,
newInForInit: false, newAtStatementBegin: false);
if (needsParens) out(")");
closingPosition = _charCount;
}
localNamer.leaveScope();
return closingPosition;
}
@override
visitDeferredExpression(DeferredExpression node) {
if (isDebugContext && !node.isFinalized) {
out(node.nonfinalizedDebugText());
return;
}
// Continue printing with the expression value.
assert(node.precedenceLevel == node.value.precedenceLevel);
node.value.accept(this);
}
@override
visitDeferredStatement(DeferredStatement node) {
startNode(node);
visit(node.statement);
endNode(node);
}
outputNumberWithRequiredWhitespace(String number) {
int charCode = number.codeUnitAt(0);
if (charCode == charCodes.$MINUS && lastCharCode == charCodes.$MINUS) {
out(" ", isWhitespace: true);
}
out(number);
}
@override
visitDeferredNumber(DeferredNumber node) {
outputNumberWithRequiredWhitespace("${node.value}");
}
@override
visitDeferredString(DeferredString node) {
out(node.value);
}
@override
visitLiteralBool(LiteralBool node) {
out(node.value ? "true" : "false");
}
@override
void visitLiteralString(LiteralString node) {
if (isDebugContext && !node.isFinalized) {
_handleString(node.nonfinalizedDebugText());
return;
}
_handleString(node.value);
}
@override
visitStringConcatenation(StringConcatenation node) {
_handleString(_StringContentsCollector(isDebugContext).collect(node));
}
void _handleString(String value) {
final kind = StringToSource.analyze(value, utf8: options.utf8);
out(kind.quote);
if (kind.simple) {
out(value);
} else {
final sb = StringBuffer();
StringToSource.writeString(sb, value, kind, utf8: options.utf8);
out(sb.toString());
}
out(kind.quote);
}
@override
visitName(Name node) {
if (isDebugContext && !node.isFinalized) {
out(node.nonfinalizedDebugText());
return;
}
out(node.name);
}
@override
visitParentheses(Parentheses node) {
out("(");
visitNestedExpression(node.enclosed, EXPRESSION,
newInForInit: false, newAtStatementBegin: false);
out(")");
}
@override
visitLiteralNumber(LiteralNumber node) {
outputNumberWithRequiredWhitespace(node.value);
}
@override
void visitLiteralNull(LiteralNull node) {
out("null");
}
@override
void visitArrayInitializer(ArrayInitializer node) {
out("[");
List<Expression> elements = node.elements;
for (int i = 0; i < elements.length; i++) {
Expression element = elements[i];
if (element is ArrayHole) {
// Note that array holes must have a trailing "," even if they are
// in last position. Otherwise `[,]` (having length 1) would become
// equal to `[]` (the empty array)
// and [1,,] (array with 1 and a hole) would become [1,] = [1].
startNode(element);
out(",");
endNode(element);
continue;
}
if (i != 0) spaceOut();
visitNestedExpression(element, ASSIGNMENT,
newInForInit: false, newAtStatementBegin: false);
// We can skip the trailing "," for the last element (since it's not
// an array hole).
if (i != elements.length - 1) out(",");
}
out("]");
}
@override
void visitArrayHole(ArrayHole node) {
context.error("Unreachable");
}
@override
void visitObjectInitializer(ObjectInitializer node) {
// Print all the properties on one line until we see a function-valued
// property. Ideally, we would use a proper pretty-printer to make the
// decision based on layout.
bool exitOneLinerMode(Expression value) {
return value is Fun ||
value is ArrayInitializer && value.elements.any((e) => e is Fun);
}
bool isOneLiner = node.isOneLiner || shouldCompressOutput;
List<Property> properties = node.properties;
out("{");
indentMore();
for (int i = 0; i < properties.length; i++) {
Node value = properties[i].value;
if (isOneLiner && exitOneLinerMode(value)) isOneLiner = false;
if (i != 0) {
out(",");
if (isOneLiner) spaceOut();
}
if (!isOneLiner) {
forceLine();
indent();
}
visit(properties[i]);
}
indentLess();
if (!isOneLiner && !properties.isEmpty) {
lineOut();
indent();
}
out("}");
}
@override
void visitProperty(Property node) {
propertyNameOut(node);
out(":");
spaceOut();
visitNestedExpression(node.value, ASSIGNMENT,
newInForInit: false, newAtStatementBegin: false);
}
@override
visitMethodDefinition(MethodDefinition node) {
propertyNameOut(node);
VarCollector vars = VarCollector();
vars.visitMethodDefinition(node);
startNode(node.function);
currentNode.closingPosition = methodOut(node, vars);
endNode(node.function);
}
int methodOut(MethodDefinition node, VarCollector vars) {
// TODO: support static, get/set, async, and generators.
Fun fun = node.function;
localNamer.enterScope(vars);
out("(");
if (fun.params != null) {
visitCommaSeparated(fun.params, PRIMARY,
newInForInit: false, newAtStatementBegin: false);
}
out(")");
spaceOut();
int closingPosition =
blockOut(fun.body, shouldIndent: false, needsNewline: false);
localNamer.leaveScope();
return closingPosition;
}
void propertyNameOut(Property node) {
startNode(node.name);
Node name = _undefer(node.name);
if (name is LiteralString) {
_outPropertyName(name.value);
} else if (name is Name) {
_outPropertyName(name.name);
} else if (name is LiteralNumber) {
out(name.value);
} else {
// TODO(sra): Handle StringConcatenation.
// TODO(sra): Handle general expressions, .e.g. `{[x]: 1}`.
throw StateError('Unexpected Property name: $name');
}
endNode(node.name);
}
void _outPropertyName(String name) {
if (isValidJavaScriptId(name)) {
out(name);
} else {
_handleString(name);
}
}
@override
void visitRegExpLiteral(RegExpLiteral node) {
out(node.pattern);
}
@override
void visitLiteralExpression(LiteralExpression node) {
out(node.template);
}
@override
void visitLiteralStatement(LiteralStatement node) {
outLn(node.code);
}
void visitInterpolatedNode(InterpolatedNode node) {
out('#${node.nameOrPosition}');
}
@override
void visitInterpolatedExpression(InterpolatedExpression node) =>
visitInterpolatedNode(node);
@override
void visitInterpolatedLiteral(InterpolatedLiteral node) =>
visitInterpolatedNode(node);
@override
void visitInterpolatedParameter(InterpolatedParameter node) =>
visitInterpolatedNode(node);
@override
void visitInterpolatedSelector(InterpolatedSelector node) =>
visitInterpolatedNode(node);
@override
void visitInterpolatedStatement(InterpolatedStatement node) {
outLn('#${node.nameOrPosition}');
}
@override
void visitInterpolatedDeclaration(InterpolatedDeclaration node) {
visitInterpolatedNode(node);
}
@override
void visitComment(Comment node) {
if (shouldCompressOutput) return;
String comment = node.comment.trim();
if (comment.isEmpty) return;
for (var line in comment.split('\n')) {
if (comment.startsWith('//')) {
outIndentLn(line.trim());
} else {
outIndentLn('// ${line.trim()}');
}
}
}
@override
void visitAwait(Await node) {
out("await ");
visit(node.expression);
}
}
class _StringContentsCollector extends BaseVisitorVoid {
final StringBuffer _buffer = StringBuffer();
final bool isDebugContext;
_StringContentsCollector(this.isDebugContext);
String collect(Node node) {
node.accept(this);
return _buffer.toString();
}
void _add(String value) {
_buffer.write(value);
}
@override
void visitNode(Node node) {
throw StateError('Node should not be part of StringConcatenation: $node');
}
@override
void visitLiteralString(LiteralString node) {
if (isDebugContext && !node.isFinalized) {
_add(node.nonfinalizedDebugText());
} else {
_add(node.value);
}
}
@override
void visitLiteralNumber(LiteralNumber node) {
if (isDebugContext && !node.isFinalized) {
_add(node.nonfinalizedDebugText());
} else {
_add(node.value);
}
}
@override
void visitName(Name node) {
if (isDebugContext && !node.isFinalized) {
_add(node.nonfinalizedDebugText());
} else {
_add(node.name);
}
}
@override
void visitStringConcatenation(StringConcatenation node) {
node.visitChildren(this);
}
}
class OrderedSet<T> {
final Set<T> set;
final List<T> list;
OrderedSet()
: set = Set<T>(),
list = <T>[];
void add(T x) {
if (set.add(x)) {
// [Set.add] returns `true` if 'x' was added.
list.add(x);
}
}
void forEach(void fun(T x)) {
list.forEach(fun);
}
}
// Collects all the var declarations in the function. We need to do this in a
// separate pass because JS vars are lifted to the top of the function.
class VarCollector extends BaseVisitorVoid {
bool nested;
bool enableRenaming = true;
final OrderedSet<String> vars;
final OrderedSet<String> params;
static final String disableVariableMinificationPattern = "::norenaming::";
static final String enableVariableMinificationPattern = "::dorenaming::";
VarCollector()
: nested = false,
vars = OrderedSet<String>(),
params = OrderedSet<String>();
void forEachVar(void fn(String v)) => vars.forEach(fn);
void forEachParam(void fn(String p)) => params.forEach(fn);
void collectVarsInFunction(FunctionExpression fun) {
if (!nested) {
nested = true;
if (fun.params != null) {
for (int i = 0; i < fun.params.length; i++) {
params.add(fun.params[i].name);
}
}
fun.body.accept(this);
nested = false;
}
}
void visitFunctionDeclaration(FunctionDeclaration declaration) {
// Note that we don't bother collecting the name of the function.
collectVarsInFunction(declaration.function);
}
void visitNamedFunction(NamedFunction namedFunction) {
// Note that we don't bother collecting the name of the function.
collectVarsInFunction(namedFunction.function);
}
void visitMethodDefinition(MethodDefinition method) {
// Note that we don't bother collecting the name of the function.
collectVarsInFunction(method.function);
}
void visitFun(Fun fun) {
collectVarsInFunction(fun);
}
void visitArrowFunction(ArrowFunction fun) {
collectVarsInFunction(fun);
}
void visitThis(This node) {}
void visitComment(Comment node) {
if (node.comment.contains(disableVariableMinificationPattern)) {
enableRenaming = false;
} else if (node.comment.contains(enableVariableMinificationPattern)) {
enableRenaming = true;
}
}
void visitVariableDeclaration(VariableDeclaration decl) {
if (enableRenaming && decl.allowRename) vars.add(decl.name);
}
}
/// Returns true, if the given node must be wrapped into braces when used
/// as then-statement in an [If] that has an else branch.
class DanglingElseVisitor extends BaseVisitor<bool> {
JavaScriptPrintingContext context;
DanglingElseVisitor(this.context);
bool visitProgram(Program node) => false;
bool visitNode(Node node) {
context.error("Forgot node: $node");
return true;
}
bool visitComment(Comment node) => true;
bool visitBlock(Block node) => false;
bool visitExpressionStatement(ExpressionStatement node) => false;
bool visitEmptyStatement(EmptyStatement node) => false;
bool visitDeferredStatement(DeferredStatement node) {
return node.statement.accept(this);
}
bool visitIf(If node) {
if (!node.hasElse) return true;
return node.otherwise.accept(this);
}
bool visitFor(For node) => node.body.accept(this);
bool visitForIn(ForIn node) => node.body.accept(this);
bool visitWhile(While node) => node.body.accept(this);
bool visitDo(Do node) => false;
bool visitContinue(Continue node) => false;
bool visitBreak(Break node) => false;
bool visitReturn(Return node) => false;
bool visitThrow(Throw node) => false;
bool visitTry(Try node) {
if (node.finallyPart != null) {
return node.finallyPart.accept(this);
} else {
return node.catchPart.accept(this);
}
}
bool visitCatch(Catch node) => node.body.accept(this);
bool visitSwitch(Switch node) => false;
bool visitCase(Case node) => false;
bool visitDefault(Default node) => false;
bool visitFunctionDeclaration(FunctionDeclaration node) => false;
bool visitLabeledStatement(LabeledStatement node) => node.body.accept(this);
bool visitLiteralStatement(LiteralStatement node) => true;
bool visitDartYield(DartYield node) => false;
bool visitExpression(Expression node) => false;
}
abstract class LocalNamer {
String getName(String oldName);
String declareVariable(String oldName);
String declareParameter(String oldName);
void enterScope(VarCollector vars);
void leaveScope();
}
class IdentityNamer implements LocalNamer {
String getName(String oldName) => oldName;
String declareVariable(String oldName) => oldName;
String declareParameter(String oldName) => oldName;
void enterScope(VarCollector vars) {}
void leaveScope() {}
}
class MinifyRenamer implements LocalNamer {
final List<Map<String, String>> maps = [];
final List<int> parameterNumberStack = [];
final List<int> variableNumberStack = [];
int parameterNumber = 0;
int variableNumber = 0;
MinifyRenamer();
void enterScope(VarCollector vars) {
maps.add({});
variableNumberStack.add(variableNumber);
parameterNumberStack.add(parameterNumber);
vars.forEachVar(declareVariable);
vars.forEachParam(declareParameter);
}
void leaveScope() {
maps.removeLast();
variableNumber = variableNumberStack.removeLast();
parameterNumber = parameterNumberStack.removeLast();
}
String getName(String oldName) {
// Go from inner scope to outer looking for mapping of name.
for (int i = maps.length - 1; i >= 0; i--) {
var map = maps[i];
var replacement = map[oldName];
if (replacement != null) return replacement;
}
return oldName;
}
static const LOWER_CASE_LETTERS = 26;
static const LETTERS = LOWER_CASE_LETTERS;
static const DIGITS = 10;
static int nthLetter(int n) {
return (n < LOWER_CASE_LETTERS)
? charCodes.$a + n
: charCodes.$A + n - LOWER_CASE_LETTERS;
}
// Parameters go from a to z and variables go from z to a. This makes each
// argument list and each top-of-function var declaration look similar and
// helps gzip compress the file. If we have more than 26 arguments and
// variables then we meet somewhere in the middle of the alphabet. After
// that we give up trying to be nice to the compression algorithm and just
// use the same namespace for arguments and variables, starting with A, and
// moving on to a0, a1, etc.
String declareVariable(String oldName) {
if (avoidRenaming(oldName)) return oldName;
var newName;
if (variableNumber + parameterNumber < LOWER_CASE_LETTERS) {
// Variables start from z and go backwards, for better gzipability.
newName = getNameNumber(oldName, LOWER_CASE_LETTERS - 1 - variableNumber);
} else {
// After 26 variables and parameters we allocate them in the same order.
newName = getNameNumber(oldName, variableNumber + parameterNumber);
}
variableNumber++;
return newName;
}
String declareParameter(String oldName) {
if (avoidRenaming(oldName)) return oldName;
var newName;
if (variableNumber + parameterNumber < LOWER_CASE_LETTERS) {
newName = getNameNumber(oldName, parameterNumber);
} else {
newName = getNameNumber(oldName, variableNumber + parameterNumber);
}
parameterNumber++;
return newName;
}
bool avoidRenaming(String oldName) {
// Variables of this $form$ are used in pattern matching the message of JS
// exceptions, so should not be renamed.
// TODO(sra): Introduce a way for indicating in the JS text which variables
// should not be renamed.
return oldName.startsWith(r'$') && oldName.endsWith(r'$');
}
String getNameNumber(String oldName, int n) {
if (maps.isEmpty) return oldName;
String newName;
if (n < LETTERS) {
// Start naming variables a, b, c, ..., z, A, B, C, ..., Z.
newName = String.fromCharCodes([nthLetter(n)]);
} else {
// Then name variables a0, a1, a2, ..., a9, b0, b1, ..., Z9, aa0, aa1, ...
// For all functions with fewer than 500 locals this is just as compact
// as using aa, ab, etc. but avoids clashes with keywords.
n -= LETTERS;
int digit = n % DIGITS;
n ~/= DIGITS;
int alphaChars = 1;
int nameSpaceSize = LETTERS;
// Find out whether we should use the 1-character namespace (size 52), the
// 2-character namespace (size 52*52), etc.
while (n >= nameSpaceSize) {
n -= nameSpaceSize;
alphaChars++;
nameSpaceSize *= LETTERS;
}
var codes = <int>[];
for (var i = 0; i < alphaChars; i++) {
nameSpaceSize ~/= LETTERS;
codes.add(nthLetter((n ~/ nameSpaceSize) % LETTERS));
}
codes.add(charCodes.$0 + digit);
newName = String.fromCharCodes(codes);
}
assert(RegExp(r'[a-zA-Z][a-zA-Z0-9]*').hasMatch(newName));
maps.last[oldName] = newName;
return newName;
}
}
/// Information pertaining the enter and exit callbacks for [node].
class EnterExitNode {
final EnterExitNode parent;
final Node node;
int startPosition;
int closingPosition;
EnterExitNode(this.parent, this.node);
void addToNode(JavaScriptPrintingContext context, int position) {
if (startPosition == null) {
// [position] is the start position of [node].
if (parent != null) {
// This might be the start position of the parent as well.
parent.addToNode(context, position);
}
startPosition = position;
context.enterNode(node, position);
}
}
EnterExitNode exitNode(JavaScriptPrintingContext context, int position) {
// Enter must happen before exit.
addToNode(context, position);
context.exitNode(node, startPosition, position, closingPosition);
return parent;
}
}