| // 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. | |
| import 'characters.dart' as char_codes; | |
| import 'nodes.dart'; | |
| import 'precedence.dart'; | |
| import 'strings.dart'; | |
| class JavaScriptPrintingOptions { | |
| final bool utf8; | |
| final bool shouldCompressOutput; | |
| final bool minifyLocalVariables; | |
| final bool minifyStatementLabels; | |
| final bool preferSemicolonToNewlineInMinifiedOutput; | |
| const JavaScriptPrintingOptions({ | |
| this.utf8 = false, | |
| bool minify = false, | |
| this.preferSemicolonToNewlineInMinifiedOutput = false, | |
| }) : shouldCompressOutput = minify, | |
| minifyLocalVariables = minify, | |
| minifyStatementLabels = minify; | |
| } | |
| /// 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(); | |
| @override | |
| void emit(String string) { | |
| buffer.write(string); | |
| } | |
| String getText() => buffer.toString(); | |
| } | |
| class _DebugJavaScriptPrintingContext extends SimpleJavaScriptPrintingContext { | |
| @override | |
| 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<void> { | |
| final JavaScriptPrintingOptions options; | |
| final JavaScriptPrintingContext context; | |
| final bool shouldCompressOutput; | |
| final DanglingElseVisitor danglingElseVisitor; | |
| final LocalNamer localNamer; | |
| final _LabelNamer _labelNamer; | |
| final bool isDebugContext; | |
| int _charCount = 0; | |
| bool inForInit = false; | |
| bool atStatementBegin = false; | |
| // The JavaScript grammar has two sets of related productions for property | |
| // accesses - for MemberExpression and CallExpression. A subset of | |
| // productions that illustrate the two sets: | |
| // | |
| // MemberExpression : | |
| // PrimaryExpression | |
| // MemberExpression . IdentifierName | |
| // new MemberExpression Arguments | |
| // ... | |
| // | |
| // CallExpression : | |
| // MemberExpression Arguments | |
| // CallExpression Arguments | |
| // CallExpression . IdentifierName | |
| // ... | |
| // | |
| // This means that a call can be in the 'function' part of another call, but | |
| // not in the 'function' part of a `new` expression. When printing a `new` | |
| // expression, a call in the 'function' part needs to be in parentheses to | |
| // ensure that the arguments of the call are not mistaken for the arguments of | |
| // the enclosing `new` expression. | |
| // | |
| // We handle the difference in required parenthesization by making the | |
| // required precedence of the receiver of an access be context-dependent. | |
| // Both "MemberExpression . IdentifierName" and "CallExpression | |
| // . IdentifierName" are represented as a PropertyAccess AST node. The context | |
| // is tracked by [inNewTarget], which is true only during the printing of | |
| // the 'function' part of a NewExpression. | |
| bool inNewTarget = 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(this.options, this.context) | |
| : isDebugContext = context.isDebugContext, | |
| shouldCompressOutput = options.shouldCompressOutput, | |
| danglingElseVisitor = DanglingElseVisitor(context), | |
| localNamer = options.minifyLocalVariables | |
| ? MinifyRenamer() | |
| : IdentityNamer(), | |
| _labelNamer = _LabelNamer(options.minifyStatementLabels); | |
| 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 = '\u0000'; | |
| int get lastCharCode { | |
| assert(lastAddedString.isNotEmpty); | |
| 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<Expression> nodes, | |
| Precedence hasRequiredType, { | |
| required bool newInForInit, | |
| required 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); | |
| } | |
| Expression _undefer(Expression 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); | |
| } | |
| } | |
| bool blockBody( | |
| Statement body, { | |
| required bool needsSeparation, | |
| required bool needsNewline, | |
| bool needsBraces = false, | |
| }) { | |
| if (body is Block) { | |
| spaceOut(); | |
| blockOut(body, shouldIndent: false, needsNewline: needsNewline); | |
| return true; | |
| } | |
| if (needsBraces) { | |
| spaceOut(); | |
| out('{'); | |
| } | |
| if (shouldCompressOutput && needsSeparation) { | |
| // If [shouldCompressOutput] is false, then the 'lineOut' will insert | |
| // the separation. | |
| out(' ', isWhitespace: true); | |
| } else { | |
| lineOut(); | |
| } | |
| indentMore(); | |
| visit(body); | |
| indentLess(); | |
| if (needsBraces) { | |
| indent(); | |
| out('}'); | |
| return true; | |
| } | |
| 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, { | |
| required bool shouldIndent, | |
| required 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, | |
| Precedence.expression, | |
| newInForInit: false, | |
| newAtStatementBegin: true, | |
| ); | |
| outSemicolonLn(); | |
| } | |
| @override | |
| void visitEmptyStatement(EmptyStatement node) { | |
| outIndentLn(';'); | |
| } | |
| void ifOut(If node, bool shouldIndent) { | |
| Statement then = node.then; | |
| Statement elsePart = node.otherwise; | |
| bool hasElse = node.hasElse; | |
| // Handle dangling elses and a workaround for Android 4.0 stock browser. | |
| // Android 4.0 requires braces for a single do-while in the `then` branch. | |
| // See issue 10923. | |
| bool needsBraces = | |
| hasElse && (then is Do || then.accept(danglingElseVisitor)); | |
| if (shouldIndent) indent(); | |
| out('if'); | |
| spaceOut(); | |
| out('('); | |
| visitNestedExpression( | |
| node.condition, | |
| Precedence.expression, | |
| newInForInit: false, | |
| newAtStatementBegin: false, | |
| ); | |
| out(')'); | |
| bool thenWasBlock = blockBody( | |
| then, | |
| needsSeparation: false, | |
| needsNewline: !hasElse, | |
| needsBraces: needsBraces, | |
| ); | |
| if (hasElse) { | |
| if (thenWasBlock) { | |
| spaceOut(); | |
| } else { | |
| indent(); | |
| } | |
| out('else'); | |
| if (elsePart is If) { | |
| pendingSpace = true; | |
| startNode(elsePart); | |
| ifOut(elsePart, false); | |
| endNode(elsePart); | |
| } else { | |
| blockBody(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!, | |
| Precedence.expression, | |
| newInForInit: true, | |
| newAtStatementBegin: false, | |
| ); | |
| } | |
| out(';'); | |
| if (loop.condition != null) { | |
| spaceOut(); | |
| visitNestedExpression( | |
| loop.condition!, | |
| Precedence.expression, | |
| newInForInit: false, | |
| newAtStatementBegin: false, | |
| ); | |
| } | |
| out(';'); | |
| if (loop.update != null) { | |
| spaceOut(); | |
| visitNestedExpression( | |
| loop.update!, | |
| Precedence.expression, | |
| newInForInit: false, | |
| newAtStatementBegin: false, | |
| ); | |
| } | |
| out(')'); | |
| blockBody(loop.body, needsSeparation: false, needsNewline: true); | |
| } | |
| @override | |
| void visitForIn(ForIn loop) { | |
| outIndent('for'); | |
| spaceOut(); | |
| out('('); | |
| visitNestedExpression( | |
| loop.leftHandSide, | |
| Precedence.expression, | |
| newInForInit: true, | |
| newAtStatementBegin: false, | |
| ); | |
| out(' in'); | |
| pendingSpace = true; | |
| visitNestedExpression( | |
| loop.object, | |
| Precedence.expression, | |
| newInForInit: false, | |
| newAtStatementBegin: false, | |
| ); | |
| out(')'); | |
| blockBody(loop.body, needsSeparation: false, needsNewline: true); | |
| } | |
| @override | |
| void visitWhile(While loop) { | |
| outIndent('while'); | |
| spaceOut(); | |
| out('('); | |
| visitNestedExpression( | |
| loop.condition, | |
| Precedence.expression, | |
| newInForInit: false, | |
| newAtStatementBegin: false, | |
| ); | |
| out(')'); | |
| blockBody(loop.body, needsSeparation: false, needsNewline: true); | |
| } | |
| @override | |
| void visitDo(Do loop) { | |
| outIndent('do'); | |
| if (blockBody(loop.body, needsSeparation: true, needsNewline: false)) { | |
| spaceOut(); | |
| } else { | |
| indent(); | |
| } | |
| out('while'); | |
| spaceOut(); | |
| out('('); | |
| visitNestedExpression( | |
| loop.condition, | |
| Precedence.expression, | |
| newInForInit: false, | |
| newAtStatementBegin: false, | |
| ); | |
| out(')'); | |
| outSemicolonLn(); | |
| } | |
| @override | |
| void visitContinue(Continue node) { | |
| if (node.targetLabel == null) { | |
| outIndent('continue'); | |
| } else { | |
| outIndent('continue ${_labelNamer.mapLabelName(node.targetLabel!)}'); | |
| } | |
| outSemicolonLn(); | |
| } | |
| @override | |
| void visitBreak(Break node) { | |
| if (node.targetLabel == null) { | |
| outIndent('break'); | |
| } else { | |
| outIndent('break ${_labelNamer.mapLabelName(node.targetLabel!)}'); | |
| } | |
| outSemicolonLn(); | |
| } | |
| @override | |
| void visitReturn(Return node) { | |
| final value = node.value; | |
| if (value == null) { | |
| outIndent('return'); | |
| } else { | |
| outIndent('return'); | |
| pendingSpace = true; | |
| visitNestedExpression( | |
| value, | |
| Precedence.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, | |
| Precedence.expression, | |
| newInForInit: false, | |
| newAtStatementBegin: false, | |
| ); | |
| outSemicolonLn(); | |
| } | |
| @override | |
| void visitThrow(Throw node) { | |
| outIndent('throw'); | |
| pendingSpace = true; | |
| visitNestedExpression( | |
| node.expression, | |
| Precedence.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, | |
| Precedence.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, | |
| Precedence.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, | |
| Precedence.expression, | |
| newInForInit: false, | |
| newAtStatementBegin: false, | |
| ); | |
| outLn(':'); | |
| if (node.body.statements.isNotEmpty) { | |
| indentMore(); | |
| blockOutWithoutBraces(node.body); | |
| indentLess(); | |
| } | |
| } | |
| @override | |
| void visitDefault(Default node) { | |
| outIndentLn('default:'); | |
| if (node.body.statements.isNotEmpty) { | |
| indentMore(); | |
| blockOutWithoutBraces(node.body); | |
| indentLess(); | |
| } | |
| } | |
| @override | |
| void visitLabeledStatement(LabeledStatement node) { | |
| outIndent('${_labelNamer.mapLabelName(node.label)}:'); | |
| blockBody(node.body, needsSeparation: false, needsNewline: true); | |
| } | |
| int functionOut(Fun fun, Expression? name, VarCollector vars) { | |
| out('function'); | |
| if (name != null) { | |
| out(' '); | |
| // Name must be a [Decl]. Therefore only test for primary expressions. | |
| visitNestedExpression( | |
| name, | |
| Precedence.primary, | |
| newInForInit: false, | |
| newAtStatementBegin: false, | |
| ); | |
| } | |
| _enterFunctionScope(vars); | |
| out('('); | |
| visitCommaSeparated( | |
| fun.params, | |
| Precedence.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, | |
| ); | |
| _exitFunctionScope(); | |
| return closingPosition; | |
| } | |
| void _enterFunctionScope(VarCollector vars) { | |
| localNamer.enterScope(vars); | |
| _labelNamer.enterFunction(); | |
| } | |
| void _exitFunctionScope() { | |
| _labelNamer.exitFunction(); | |
| localNamer.leaveScope(); | |
| } | |
| @override | |
| void 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(); | |
| } | |
| void visitNestedExpression( | |
| Expression node, | |
| Precedence requiredPrecedence, { | |
| required bool newInForInit, | |
| required bool newAtStatementBegin, | |
| }) { | |
| Precedence precedenceLevel = (isDebugContext && !node.isFinalized) | |
| ? Precedence.call | |
| : node.precedenceLevel; | |
| bool needsParentheses = | |
| // a - (b + c). | |
| (requiredPrecedence != Precedence.expression && | |
| precedenceLevel.index < requiredPrecedence.index) || | |
| // 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)); | |
| final savedInForInit = inForInit; | |
| if (needsParentheses) { | |
| inForInit = false; | |
| atStatementBegin = false; | |
| inNewTarget = false; | |
| out('('); | |
| visit(node); | |
| out(')'); | |
| } else { | |
| inForInit = newInForInit; | |
| atStatementBegin = newAtStatementBegin; | |
| visit(node); | |
| } | |
| inForInit = savedInForInit; | |
| } | |
| @override | |
| void visitVariableDeclarationList(VariableDeclarationList list) { | |
| out('var '); | |
| final nodes = list.declarations; | |
| if (inForInit) { | |
| visitCommaSeparated( | |
| nodes, | |
| Precedence.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++) { | |
| Expression node = nodes[i]; | |
| bool thisIsBig = !_isSmallInitialization(node); | |
| if (i > 0) { | |
| atStatementBegin = false; | |
| out(','); | |
| if (lastWasBig || thisIsBig) { | |
| lineOut(); | |
| indent(); | |
| } else { | |
| spaceOut(); | |
| } | |
| } | |
| visitNestedExpression( | |
| node, | |
| Precedence.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 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 == char_codes.$PLUS) out(' ', isWhitespace: true); | |
| out('++'); | |
| } else { | |
| if (lastCharCode == char_codes.$MINUS) out(' ', isWhitespace: true); | |
| out('--'); | |
| } | |
| if (alias != null) startNode(alias); | |
| visitNestedExpression( | |
| variable, | |
| Precedence.unary, | |
| newInForInit: inForInit, | |
| newAtStatementBegin: false, | |
| ); | |
| if (alias != null) endNode(alias); | |
| } | |
| @override | |
| void 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 rightHandSide = _undefer(assignment.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) { | |
| Expression rLeft = _undefer(rightHandSide.left); | |
| Expression 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, | |
| Precedence.call, | |
| newInForInit: inForInit, | |
| newAtStatementBegin: atStatementBegin, | |
| ); | |
| endNode(rightHandSide.left); | |
| spaceOut(); | |
| out(op); | |
| out('='); | |
| spaceOut(); | |
| visitNestedExpression( | |
| rRight, | |
| Precedence.assignment, | |
| newInForInit: inForInit, | |
| newAtStatementBegin: false, | |
| ); | |
| return; | |
| } | |
| } | |
| } | |
| visitNestedExpression( | |
| assignment.leftHandSide, | |
| Precedence.call, | |
| newInForInit: inForInit, | |
| newAtStatementBegin: atStatementBegin, | |
| ); | |
| spaceOut(); | |
| if (op != null) out(op); | |
| out('='); | |
| spaceOut(); | |
| visitNestedExpression( | |
| assignment.value, | |
| Precedence.assignment, | |
| newInForInit: inForInit, | |
| newAtStatementBegin: false, | |
| ); | |
| } | |
| @override | |
| void visitVariableInitialization(VariableInitialization initialization) { | |
| visitNestedExpression( | |
| initialization.declaration, | |
| Precedence.call, | |
| newInForInit: inForInit, | |
| newAtStatementBegin: atStatementBegin, | |
| ); | |
| if (initialization.value != null) { | |
| spaceOut(); | |
| out('='); | |
| spaceOut(); | |
| visitNestedExpression( | |
| initialization.value!, | |
| Precedence.assignment, | |
| newInForInit: inForInit, | |
| newAtStatementBegin: false, | |
| ); | |
| } | |
| } | |
| @override | |
| void visitConditional(Conditional cond) { | |
| visitNestedExpression( | |
| cond.condition, | |
| Precedence.logicalOr, | |
| newInForInit: inForInit, | |
| newAtStatementBegin: atStatementBegin, | |
| ); | |
| spaceOut(); | |
| out('?'); | |
| spaceOut(); | |
| // The then part is allowed to have an 'in'. | |
| visitNestedExpression( | |
| cond.then, | |
| Precedence.assignment, | |
| newInForInit: false, | |
| newAtStatementBegin: false, | |
| ); | |
| spaceOut(); | |
| out(':'); | |
| spaceOut(); | |
| visitNestedExpression( | |
| cond.otherwise, | |
| Precedence.assignment, | |
| newInForInit: inForInit, | |
| newAtStatementBegin: false, | |
| ); | |
| } | |
| @override | |
| void visitNew(New node) { | |
| out('new '); | |
| final savedInNewTarget = inNewTarget; | |
| inNewTarget = true; | |
| visitNestedExpression( | |
| node.target, | |
| Precedence.leftHandSide, | |
| newInForInit: inForInit, | |
| newAtStatementBegin: false, | |
| ); | |
| out('('); | |
| inNewTarget = false; | |
| visitCommaSeparated( | |
| node.arguments, | |
| Precedence.assignment, | |
| newInForInit: false, | |
| newAtStatementBegin: false, | |
| ); | |
| out(')'); | |
| inNewTarget = savedInNewTarget; | |
| } | |
| @override | |
| void visitCall(Call call) { | |
| visitNestedExpression( | |
| call.target, | |
| Precedence.call, | |
| newInForInit: inForInit, | |
| newAtStatementBegin: atStatementBegin, | |
| ); | |
| out('('); | |
| visitCommaSeparated( | |
| call.arguments, | |
| Precedence.assignment, | |
| newInForInit: false, | |
| newAtStatementBegin: false, | |
| ); | |
| out(')'); | |
| } | |
| @override | |
| void visitBinary(Binary binary) { | |
| Expression left = binary.left; | |
| Expression right = binary.right; | |
| String op = binary.op; | |
| Precedence leftPrecedenceRequirement; | |
| Precedence rightPrecedenceRequirement; | |
| bool leftSpace = true; // left<HERE>op right | |
| switch (op) { | |
| case ',': | |
| // x, (y, z) <=> (x, y), z. | |
| leftPrecedenceRequirement = Precedence.expression; | |
| rightPrecedenceRequirement = Precedence.expression; | |
| leftSpace = false; | |
| break; | |
| case '||': | |
| leftPrecedenceRequirement = Precedence.logicalOr; | |
| // x || (y || z) <=> (x || y) || z. | |
| rightPrecedenceRequirement = Precedence.logicalOr; | |
| break; | |
| case '&&': | |
| leftPrecedenceRequirement = Precedence.logicalAnd; | |
| // x && (y && z) <=> (x && y) && z. | |
| rightPrecedenceRequirement = Precedence.logicalAnd; | |
| break; | |
| case '|': | |
| leftPrecedenceRequirement = Precedence.bitOr; | |
| // x | (y | z) <=> (x | y) | z. | |
| rightPrecedenceRequirement = Precedence.bitOr; | |
| break; | |
| case '^': | |
| leftPrecedenceRequirement = Precedence.bitXor; | |
| // x ^ (y ^ z) <=> (x ^ y) ^ z. | |
| rightPrecedenceRequirement = Precedence.bitXor; | |
| break; | |
| case '&': | |
| leftPrecedenceRequirement = Precedence.bitAnd; | |
| // x & (y & z) <=> (x & y) & z. | |
| rightPrecedenceRequirement = Precedence.bitAnd; | |
| break; | |
| case '==': | |
| case '!=': | |
| case '===': | |
| case '!==': | |
| leftPrecedenceRequirement = Precedence.equality; | |
| rightPrecedenceRequirement = Precedence.relational; | |
| break; | |
| case '<': | |
| case '>': | |
| case '<=': | |
| case '>=': | |
| case 'instanceof': | |
| case 'in': | |
| leftPrecedenceRequirement = Precedence.relational; | |
| rightPrecedenceRequirement = Precedence.shift; | |
| break; | |
| case '>>': | |
| case '<<': | |
| case '>>>': | |
| leftPrecedenceRequirement = Precedence.shift; | |
| rightPrecedenceRequirement = Precedence.additive; | |
| break; | |
| case '+': | |
| case '-': | |
| leftPrecedenceRequirement = Precedence.additive; | |
| // We cannot remove parenthesis for "+" because | |
| // x + (y + z) <!=> (x + y) + z: | |
| // Example: | |
| // "a" + (1 + 2) => "a3"; | |
| // ("a" + 1) + 2 => "a12"; | |
| rightPrecedenceRequirement = Precedence.multiplicative; | |
| break; | |
| case '*': | |
| case '/': | |
| case '%': | |
| leftPrecedenceRequirement = Precedence.multiplicative; | |
| // We cannot remove parenthesis for "*" because of precision issues. | |
| rightPrecedenceRequirement = Precedence.unary; | |
| break; | |
| case '**': | |
| // Exponentiation associates to the right, so `a ** b ** c` parses as `a | |
| // ** (b ** c)`. To generate the appropriate output, the left has a | |
| // higher precedence than the current node. The next precedence level | |
| // ([UNARY]), is skipped as the left hand side of an exponentiation | |
| // operator [must be an UPDATE | |
| // expression](https://tc39.es/ecma262/#sec-exp-operator). Skipping | |
| // [UNARY] avoids printing `-1 ** 2`, which is a syntax error. | |
| leftPrecedenceRequirement = Precedence.update; | |
| rightPrecedenceRequirement = Precedence.exponentiation; | |
| break; | |
| default: | |
| leftPrecedenceRequirement = Precedence.expression; | |
| rightPrecedenceRequirement = Precedence.expression; | |
| 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 == char_codes.$PLUS) out(' ', isWhitespace: true); | |
| out(op); | |
| break; | |
| case '-': | |
| case '--': | |
| if (lastCharCode == char_codes.$MINUS) out(' ', isWhitespace: true); | |
| out(op); | |
| break; | |
| default: | |
| out(op); | |
| } | |
| visitNestedExpression( | |
| unary.argument, | |
| Precedence.unary, | |
| newInForInit: inForInit, | |
| newAtStatementBegin: false, | |
| ); | |
| } | |
| @override | |
| void visitPostfix(Postfix postfix) { | |
| visitNestedExpression( | |
| postfix.argument, | |
| Precedence.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)); | |
| } | |
| @override | |
| void visitRestParameter(RestParameter param) { | |
| out('...'); | |
| visitParameter(param); | |
| } | |
| bool isDigit(int charCode) { | |
| return char_codes.$0 <= charCode && charCode <= char_codes.$9; | |
| } | |
| bool isValidJavaScriptId(String field) { | |
| if (field.isEmpty) 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 (!(char_codes.$a <= charCode && charCode <= char_codes.$z || | |
| char_codes.$A <= charCode && charCode <= char_codes.$Z || | |
| charCode == char_codes.$$ || | |
| charCode == char_codes.$_ || | |
| 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) { | |
| final precedence = inNewTarget ? Precedence.leftHandSide : Precedence.call; | |
| visitNestedExpression( | |
| access.receiver, | |
| precedence, | |
| 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('['); | |
| inNewTarget = false; | |
| visitNestedExpression( | |
| access.selector, | |
| Precedence.expression, | |
| newInForInit: false, | |
| newAtStatementBegin: false, | |
| ); | |
| out(']'); | |
| } | |
| void _dotString( | |
| Node selector, | |
| Expression receiver, | |
| String selectorValue, { | |
| bool assumeValid = false, | |
| }) { | |
| if (assumeValid || isValidJavaScriptId(selectorValue)) { | |
| if (_undefer(receiver) is LiteralNumber && | |
| lastCharCode != char_codes.$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); | |
| } | |
| static bool _isIdentifierParameter(Node node) => | |
| node is VariableReference && node is! RestParameter; | |
| int arrowFunctionOut(ArrowFunction fun, VarCollector vars) { | |
| // TODO: support static, get/set, async, and generators. | |
| _enterFunctionScope(vars); | |
| final List<Parameter> params = fun.params; | |
| if (params.length == 1 && _isIdentifierParameter(params.first)) { | |
| visitNestedExpression( | |
| params.single, | |
| Precedence.assignment, | |
| newInForInit: false, | |
| newAtStatementBegin: false, | |
| ); | |
| } else { | |
| out('('); | |
| visitCommaSeparated( | |
| fun.params, | |
| Precedence.primary, | |
| newInForInit: false, | |
| newAtStatementBegin: false, | |
| ); | |
| out(')'); | |
| } | |
| spaceOut(); | |
| out('=>'); | |
| spaceOut(); | |
| int closingPosition; | |
| Node body = fun.body; | |
| 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 as Expression, | |
| Precedence.assignment, | |
| newInForInit: false, | |
| newAtStatementBegin: false, | |
| ); | |
| if (needsParens) out(')'); | |
| closingPosition = _charCount; | |
| } | |
| _exitFunctionScope(); | |
| return closingPosition; | |
| } | |
| @override | |
| void 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 | |
| void visitDeferredStatement(DeferredStatement node) { | |
| startNode(node); | |
| visit(node.statement); | |
| endNode(node); | |
| } | |
| void outputNumberWithRequiredWhitespace(String number) { | |
| int charCode = number.codeUnitAt(0); | |
| if (charCode == char_codes.$MINUS && lastCharCode == char_codes.$MINUS) { | |
| out(' ', isWhitespace: true); | |
| } | |
| out(number); | |
| } | |
| @override | |
| void visitDeferredNumber(DeferredNumber node) { | |
| outputNumberWithRequiredWhitespace('${node.value}'); | |
| } | |
| @override | |
| void visitDeferredString(DeferredString node) { | |
| out(node.value); | |
| } | |
| @override | |
| void 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 | |
| void 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 | |
| void visitName(Name node) { | |
| if (isDebugContext && !node.isFinalized) { | |
| out(node.nonfinalizedDebugText()); | |
| return; | |
| } | |
| out(node.name); | |
| } | |
| @override | |
| void visitParentheses(Parentheses node) { | |
| out('('); | |
| visitNestedExpression( | |
| node.enclosed, | |
| Precedence.expression, | |
| newInForInit: false, | |
| newAtStatementBegin: false, | |
| ); | |
| out(')'); | |
| } | |
| @override | |
| void 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, | |
| Precedence.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++) { | |
| Expression 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.isNotEmpty) { | |
| lineOut(); | |
| indent(); | |
| } | |
| out('}'); | |
| } | |
| @override | |
| void visitProperty(Property node) { | |
| propertyNameOut(node); | |
| out(':'); | |
| spaceOut(); | |
| visitNestedExpression( | |
| node.value, | |
| Precedence.assignment, | |
| newInForInit: false, | |
| newAtStatementBegin: false, | |
| ); | |
| } | |
| @override | |
| void 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; | |
| _enterFunctionScope(vars); | |
| out('('); | |
| visitCommaSeparated( | |
| fun.params, | |
| Precedence.primary, | |
| newInForInit: false, | |
| newAtStatementBegin: false, | |
| ); | |
| out(')'); | |
| spaceOut(); | |
| int closingPosition = blockOut( | |
| fun.body, | |
| shouldIndent: false, | |
| needsNewline: false, | |
| ); | |
| _exitFunctionScope(); | |
| 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 { | |
| // Handle general expressions, .e.g. `{[x]: 1}`. | |
| // String concatenation could be better. | |
| out('['); | |
| visitNestedExpression( | |
| node.name, | |
| Precedence.expression, | |
| newInForInit: false, | |
| newAtStatementBegin: false, | |
| ); | |
| out(']'); | |
| } | |
| 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 = <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 Function(T x) fun) { | |
| 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 Function(String) fn) => vars.forEach(fn); | |
| void forEachParam(void Function(String) fn) => params.forEach(fn); | |
| void collectVarsInFunction(FunctionExpression fun) { | |
| if (!nested) { | |
| nested = true; | |
| for (int i = 0; i < fun.params.length; i++) { | |
| params.add(fun.params[i].name); | |
| } | |
| fun.body.accept(this); | |
| nested = false; | |
| } | |
| } | |
| @override | |
| void visitFunctionDeclaration(FunctionDeclaration declaration) { | |
| // Note that we don't bother collecting the name of the function. | |
| collectVarsInFunction(declaration.function); | |
| } | |
| @override | |
| void visitNamedFunction(NamedFunction namedFunction) { | |
| // Note that we don't bother collecting the name of the function. | |
| collectVarsInFunction(namedFunction.function); | |
| } | |
| @override | |
| void visitMethodDefinition(MethodDefinition method) { | |
| // Note that we don't bother collecting the name of the function. | |
| collectVarsInFunction(method.function); | |
| } | |
| @override | |
| void visitFun(Fun fun) { | |
| collectVarsInFunction(fun); | |
| } | |
| @override | |
| void visitArrowFunction(ArrowFunction fun) { | |
| collectVarsInFunction(fun); | |
| } | |
| @override | |
| void visitThis(This node) {} | |
| @override | |
| void visitComment(Comment node) { | |
| if (node.comment.contains(disableVariableMinificationPattern)) { | |
| enableRenaming = false; | |
| } else if (node.comment.contains(enableVariableMinificationPattern)) { | |
| enableRenaming = true; | |
| } | |
| } | |
| @override | |
| 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); | |
| @override | |
| bool visitProgram(Program node) => false; | |
| @override | |
| bool visitNode(Node node) { | |
| context.error('Forgot node: $node'); | |
| return true; | |
| } | |
| @override | |
| bool visitComment(Comment node) => true; | |
| @override | |
| bool visitBlock(Block node) { | |
| // TODO(sra): The following is no longer true. Revert to `=> false;`. | |
| // Singleton blocks are in many places printed as the contained statement so | |
| // that statement might capture the dangling else. | |
| if (node.statements.length != 1) return false; | |
| return node.statements.single.accept(this); | |
| } | |
| @override | |
| bool visitExpressionStatement(ExpressionStatement node) => false; | |
| @override | |
| bool visitEmptyStatement(EmptyStatement node) => false; | |
| @override | |
| bool visitDeferredStatement(DeferredStatement node) { | |
| return node.statement.accept(this); | |
| } | |
| @override | |
| bool visitIf(If node) { | |
| if (!node.hasElse) return true; | |
| return node.otherwise.accept(this); | |
| } | |
| @override | |
| bool visitFor(For node) => node.body.accept(this); | |
| @override | |
| bool visitForIn(ForIn node) => node.body.accept(this); | |
| @override | |
| bool visitWhile(While node) => node.body.accept(this); | |
| @override | |
| bool visitDo(Do node) => false; | |
| @override | |
| bool visitContinue(Continue node) => false; | |
| @override | |
| bool visitBreak(Break node) => false; | |
| @override | |
| bool visitReturn(Return node) => false; | |
| @override | |
| bool visitThrow(Throw node) => false; | |
| @override | |
| bool visitTry(Try node) { | |
| if (node.finallyPart != null) { | |
| return node.finallyPart!.accept(this); | |
| } else { | |
| return node.catchPart!.accept(this); | |
| } | |
| } | |
| @override | |
| bool visitCatch(Catch node) => node.body.accept(this); | |
| @override | |
| bool visitSwitch(Switch node) => false; | |
| @override | |
| bool visitCase(Case node) => false; | |
| @override | |
| bool visitDefault(Default node) => false; | |
| @override | |
| bool visitFunctionDeclaration(FunctionDeclaration node) => false; | |
| @override | |
| bool visitLabeledStatement(LabeledStatement node) => node.body.accept(this); | |
| @override | |
| bool visitLiteralStatement(LiteralStatement node) => true; | |
| @override | |
| bool visitDartYield(DartYield node) => false; | |
| @override | |
| 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 { | |
| @override | |
| String getName(String oldName) => oldName; | |
| @override | |
| String declareVariable(String oldName) => oldName; | |
| @override | |
| String declareParameter(String oldName) => oldName; | |
| @override | |
| void enterScope(VarCollector vars) {} | |
| @override | |
| 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(); | |
| @override | |
| void enterScope(VarCollector vars) { | |
| maps.add({}); | |
| variableNumberStack.add(variableNumber); | |
| parameterNumberStack.add(parameterNumber); | |
| vars.forEachVar(declareVariable); | |
| vars.forEachParam(declareParameter); | |
| } | |
| @override | |
| void leaveScope() { | |
| maps.removeLast(); | |
| variableNumber = variableNumberStack.removeLast(); | |
| parameterNumber = parameterNumberStack.removeLast(); | |
| } | |
| @override | |
| 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) | |
| ? char_codes.$a + n | |
| : char_codes.$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. | |
| @override | |
| String declareVariable(String oldName) { | |
| if (avoidRenaming(oldName)) return oldName; | |
| String 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; | |
| } | |
| @override | |
| String declareParameter(String oldName) { | |
| if (avoidRenaming(oldName)) return oldName; | |
| String 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(char_codes.$0 + digit); | |
| newName = String.fromCharCodes(codes); | |
| } | |
| assert(RegExp(r'[a-zA-Z][a-zA-Z0-9]*').hasMatch(newName)); | |
| maps.last[oldName] = newName; | |
| return newName; | |
| } | |
| } | |
| class _LabelNamer { | |
| final bool renameLabels; | |
| Map<String, String> _renamings = {}; | |
| final List<Map<String, String>> _outerScopes = []; | |
| _LabelNamer(this.renameLabels); | |
| String mapLabelName(String name) { | |
| if (!renameLabels) return name; | |
| return _renamings[name] ??= _newLabelName(_renamings, name); | |
| } | |
| static String _newLabelName(Map<String, String> renamings, String name) { | |
| assert(!renamings.containsKey(name)); | |
| int index = renamings.length; | |
| if (index < 26) return String.fromCharCode(index + 'A'.codeUnitAt(0)); | |
| index -= 26; | |
| if (index < 26) return String.fromCharCode(index + 'a'.codeUnitAt(0)); | |
| index -= 26; | |
| return 'L$index'; | |
| } | |
| void enterFunction() { | |
| if (!renameLabels) return; | |
| _outerScopes.add(_renamings); | |
| _renamings = {}; | |
| } | |
| void exitFunction() { | |
| if (!renameLabels) return; | |
| _renamings = _outerScopes.removeLast(); | |
| } | |
| } | |
| /// 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]. | |
| // 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; | |
| } | |
| } |