6d347961b2
Also simplify use of JavaScriptPrintingOptions. Change-Id: Icdb603edd76b71dbc4a5d913407b96e5e9589265 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/452223 Reviewed-by: Mayank Patke <fishythefish@google.com> Commit-Queue: Stephen Adams <sra@google.com>
2218 lines
58 KiB
Dart
2218 lines
58 KiB
Dart
// 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));
|
|
}
|
|
|
|
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;
|
|
|
|
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;
|
|
}
|
|
}
|