| // Copyright (c) 2020, 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. |
| |
| library js.size_estimator; |
| |
| import 'package:js_ast/js_ast.dart'; |
| import 'package:js_ast/src/characters.dart' as charCodes; |
| import 'package:js_ast/src/precedence.dart'; |
| |
| import '../js_backend/deferred_holder_expression.dart'; |
| import '../js_backend/string_reference.dart'; |
| import '../js_backend/type_reference.dart'; |
| import '../js_emitter/metadata_collector.dart'; |
| |
| /// Estimates the size of the Javascript AST represented by the provided [Node]. |
| int estimateSize(Node node) { |
| var estimator = SizeEstimator(); |
| estimator.visit(node); |
| return estimator.charCount; |
| } |
| |
| /// [SizeEstimator] is a [NodeVisitor] designed to produce a consistent size |
| /// estimate for a given JavaScript AST. [SizeEstimator] trades accuracy for |
| /// stability and performance. In addition, [SizeEstimator] assumes we will emit |
| /// production quality minified JavaScript. |
| class SizeEstimator implements NodeVisitor { |
| int charCount = 0; |
| bool inForInit = false; |
| bool atStatementBegin = false; |
| bool pendingSemicolon = false; |
| bool pendingSpace = false; |
| |
| static final String variableSizeEstimate = '#'; |
| static final String nameSizeEstimate = '###'; |
| |
| String sizeEstimate(Node node) { |
| if (node is VariableDeclaration || node is VariableUse) { |
| // We assume all [VariableDeclaration] and [VariableUse] nodes are |
| // locals. |
| return variableSizeEstimate; |
| } else if (node is Name || |
| node is Parameter || |
| node is VariableDeclaration || |
| node is VariableUse) { |
| return nameSizeEstimate; |
| } else if (node is LiteralString) { |
| assert(!node.isFinalized); |
| // We assume all non-final literal strings are minified names, and thus |
| // use the nameSizeEstimate. |
| return nameSizeEstimate; |
| } else if (node is BoundMetadataEntry) { |
| // Value is an int. |
| return '####'; |
| } else if (node is TypeReference) { |
| // Type references vary in size. Some references look like: |
| // '<typeHolder>$.<type>' where <typeHolder> is a one byte local and |
| // <type> is roughly 3 bytes. However, we also have to initialize the type |
| // in the holder, some like ab:f("QQ<b7c>"), ie 16 bytes. For two |
| // occurences we will have on average 13 bytes. For a more detailed |
| // estimate, we'd have to partially finalize the results. |
| return '###_###_###_#'; |
| } else if (node is StringReference) { |
| // Worst case we have to inline the string so size of string + 2 bytes for |
| // quotes. |
| return "'${node.constant.toDartString()}'"; |
| } else if (node is DeferredHolderExpression) { |
| // 1 byte holder + dot + nameSizeEstimate |
| return '#.$nameSizeEstimate'; |
| } else { |
| throw UnsupportedError('$node type is not supported'); |
| } |
| } |
| |
| String literalStringToString(LiteralString node) { |
| if (node.isFinalized) { |
| return node.value; |
| } else { |
| return sizeEstimate(node); |
| } |
| } |
| |
| /// Always emit a newline, even under `enableMinification`. |
| void forceLine() { |
| out('\n'); // '\n' |
| } |
| |
| void out(String s) { |
| if (s.length > 0) { |
| // We can elide a semicolon in some cases, but for simplicity we |
| // assume a semicolon is needed here. |
| if (pendingSemicolon) { |
| emit(';'); // ';' |
| } |
| |
| // We can elide a pending space in some cases, but for simplicity we |
| // assume a space is needed here. |
| if (pendingSpace) { |
| emit(' '); // ' ' |
| } |
| pendingSpace = false; |
| pendingSemicolon = false; |
| emit(s); // str |
| } |
| } |
| |
| void outSemicolonLn() { |
| pendingSemicolon = true; |
| } |
| |
| void emit(String s) { |
| charCount += s.length; |
| } |
| |
| void visit(Node node) { |
| node.accept(this); |
| } |
| |
| void visitCommaSeparated(List<Node> nodes, int hasRequiredType, |
| {bool newInForInit, bool newAtStatementBegin}) { |
| for (int i = 0; i < nodes.length; i++) { |
| if (i != 0) { |
| atStatementBegin = false; |
| out(','); // ',' |
| } |
| visitNestedExpression(nodes[i], hasRequiredType, |
| newInForInit: newInForInit, newAtStatementBegin: newAtStatementBegin); |
| } |
| } |
| |
| void visitAll(List<Node> nodes) { |
| nodes.forEach(visit); |
| } |
| |
| @override |
| void visitProgram(Program program) { |
| if (program.body.isNotEmpty) { |
| visitAll(program.body); |
| } |
| } |
| |
| Statement unwrapBlockIfSingleStatement(Statement body) { |
| Statement result = body; |
| while (result is Block) { |
| Block block = result; |
| if (block.statements.length != 1) break; |
| result = block.statements.single; |
| } |
| return result; |
| } |
| |
| bool blockBody(Statement body, {bool needsSeparation}) { |
| if (body is Block) { |
| blockOut(body); |
| return true; |
| } |
| if (needsSeparation) { |
| out(' '); // ' ' |
| } |
| visit(body); |
| return false; |
| } |
| |
| void blockOutWithoutBraces(Node node) { |
| if (node is Block) { |
| Block block = node; |
| block.statements.forEach(blockOutWithoutBraces); |
| } else { |
| visit(node); |
| } |
| } |
| |
| int blockOut(Block node) { |
| out('{'); // '{' |
| node.statements.forEach(blockOutWithoutBraces); |
| out('}'); // '}' |
| int closingPosition = charCount - 1; |
| return closingPosition; |
| } |
| |
| @override |
| void visitBlock(Block block) { |
| blockOut(block); |
| } |
| |
| @override |
| void visitExpressionStatement(ExpressionStatement node) { |
| visitNestedExpression(node.expression, EXPRESSION, |
| newInForInit: false, newAtStatementBegin: true); |
| outSemicolonLn(); |
| } |
| |
| @override |
| void visitEmptyStatement(EmptyStatement node) { |
| out(';'); // ';' |
| } |
| |
| void ifOut(If node) { |
| Statement then = unwrapBlockIfSingleStatement(node.then); |
| Statement elsePart = node.otherwise; |
| bool hasElse = node.hasElse; |
| |
| out('if('); // 'if(' |
| visitNestedExpression(node.condition, EXPRESSION, |
| newInForInit: false, newAtStatementBegin: false); |
| out(')'); // ')' |
| blockBody(then, needsSeparation: false); |
| if (hasElse) { |
| out('else'); // 'else' |
| if (elsePart is If) { |
| pendingSpace = true; |
| ifOut(elsePart); |
| } else { |
| blockBody(unwrapBlockIfSingleStatement(elsePart), |
| needsSeparation: true); |
| } |
| } |
| } |
| |
| @override |
| void visitIf(If node) { |
| ifOut(node); |
| } |
| |
| @override |
| void visitFor(For loop) { |
| out('for('); // 'for(' |
| if (loop.init != null) { |
| visitNestedExpression(loop.init, EXPRESSION, |
| newInForInit: true, newAtStatementBegin: false); |
| } |
| out(';'); // ';' |
| if (loop.condition != null) { |
| visitNestedExpression(loop.condition, EXPRESSION, |
| newInForInit: false, newAtStatementBegin: false); |
| } |
| out(';'); // ';' |
| if (loop.update != null) { |
| visitNestedExpression(loop.update, EXPRESSION, |
| newInForInit: false, newAtStatementBegin: false); |
| } |
| out(')'); // ')' |
| blockBody(unwrapBlockIfSingleStatement(loop.body), needsSeparation: false); |
| } |
| |
| @override |
| void visitForIn(ForIn loop) { |
| out('for('); // 'for(' |
| visitNestedExpression(loop.leftHandSide, EXPRESSION, |
| newInForInit: true, newAtStatementBegin: false); |
| out(' in'); // ' in' |
| pendingSpace = true; |
| visitNestedExpression(loop.object, EXPRESSION, |
| newInForInit: false, newAtStatementBegin: false); |
| out(')'); // ')' |
| blockBody(unwrapBlockIfSingleStatement(loop.body), needsSeparation: false); |
| } |
| |
| @override |
| void visitWhile(While loop) { |
| out('while('); // 'while(' |
| visitNestedExpression(loop.condition, EXPRESSION, |
| newInForInit: false, newAtStatementBegin: false); |
| out(')'); // ')' |
| blockBody(unwrapBlockIfSingleStatement(loop.body), needsSeparation: false); |
| } |
| |
| @override |
| void visitDo(Do loop) { |
| out('do'); // 'do' |
| if (blockBody(unwrapBlockIfSingleStatement(loop.body), |
| needsSeparation: true)) {} |
| out('while('); // 'while(' |
| visitNestedExpression(loop.condition, EXPRESSION, |
| newInForInit: false, newAtStatementBegin: false); |
| out(')'); // ')' |
| outSemicolonLn(); |
| } |
| |
| @override |
| void visitContinue(Continue node) { |
| if (node.targetLabel == null) { |
| out('continue'); // 'continue' |
| } else { |
| out('continue ${node.targetLabel}'); // 'continue ${node.targetLabel}' |
| } |
| outSemicolonLn(); |
| } |
| |
| @override |
| void visitBreak(Break node) { |
| if (node.targetLabel == null) { |
| out('break'); |
| } else { |
| out('break ${node.targetLabel}'); |
| } |
| outSemicolonLn(); |
| } |
| |
| @override |
| void visitReturn(Return node) { |
| out('return'); // 'return' |
| if (node.value != null) { |
| pendingSpace = true; |
| visitNestedExpression(node.value, EXPRESSION, |
| newInForInit: false, newAtStatementBegin: false); |
| } |
| outSemicolonLn(); |
| } |
| |
| @override |
| void visitDartYield(DartYield node) { |
| if (node.hasStar) { |
| out('yield*'); // 'yield*' |
| } else { |
| out('yield'); // 'yield' |
| } |
| pendingSpace = true; |
| visitNestedExpression(node.expression, EXPRESSION, |
| newInForInit: false, newAtStatementBegin: false); |
| outSemicolonLn(); |
| } |
| |
| @override |
| void visitThrow(Throw node) { |
| out('throw'); // 'throw' |
| pendingSpace = true; |
| visitNestedExpression(node.expression, EXPRESSION, |
| newInForInit: false, newAtStatementBegin: false); |
| outSemicolonLn(); |
| } |
| |
| @override |
| void visitTry(Try node) { |
| out('try'); // 'try' |
| blockBody(node.body, needsSeparation: true); |
| if (node.catchPart != null) { |
| visit(node.catchPart); |
| } |
| if (node.finallyPart != null) { |
| out('finally'); // 'finally' |
| blockBody(node.finallyPart, needsSeparation: true); |
| } |
| } |
| |
| @override |
| void visitCatch(Catch node) { |
| out('catch('); // 'catch(' |
| visitNestedExpression(node.declaration, EXPRESSION, |
| newInForInit: false, newAtStatementBegin: false); |
| out(')'); // ')' |
| blockBody(node.body, needsSeparation: false); |
| } |
| |
| @override |
| void visitSwitch(Switch node) { |
| out('switch('); // 'switch(' |
| visitNestedExpression(node.key, EXPRESSION, |
| newInForInit: false, newAtStatementBegin: false); |
| out('){'); // '){ |
| visitAll(node.cases); |
| out('}'); // '}' |
| } |
| |
| @override |
| void visitCase(Case node) { |
| out('case'); // 'case' |
| pendingSpace = true; |
| visitNestedExpression(node.expression, EXPRESSION, |
| newInForInit: false, newAtStatementBegin: false); |
| out(':'); // ':' |
| if (!node.body.statements.isEmpty) { |
| blockOutWithoutBraces(node.body); |
| } |
| } |
| |
| @override |
| void visitDefault(Default node) { |
| out('default:'); // 'default:' |
| if (!node.body.statements.isEmpty) { |
| blockOutWithoutBraces(node.body); |
| } |
| } |
| |
| @override |
| void visitLabeledStatement(LabeledStatement node) { |
| Statement body = unwrapBlockIfSingleStatement(node.body); |
| // `label: break label;` |
| // Does not work on IE. The statement is a nop, so replace it by an empty |
| // statement. |
| // See: |
| // https://connect.microsoft.com/IE/feedback/details/891889/parser-bugs |
| if (body is Break && body.targetLabel == node.label) { |
| visit(new EmptyStatement()); |
| return; |
| } |
| out('${node.label}:'); |
| blockBody(body, needsSeparation: false); |
| } |
| |
| int functionOut(Fun fun, Node name, VarCollector vars) { |
| out('function'); // 'function' |
| if (name != null) { |
| out(' '); // ' ' |
| // Name must be a [Decl]. Therefore only test for primary expressions. |
| visitNestedExpression(name, PRIMARY, |
| newInForInit: false, newAtStatementBegin: false); |
| } |
| out('('); // '(' |
| if (fun.params != null) { |
| visitCommaSeparated(fun.params, PRIMARY, |
| newInForInit: false, newAtStatementBegin: false); |
| } |
| out(')'); // ')' |
| switch (fun.asyncModifier) { |
| case AsyncModifier.sync: |
| break; |
| case AsyncModifier.async: |
| out(' async'); // ' async' |
| break; |
| case AsyncModifier.syncStar: |
| out(' sync*'); // ' sync*' |
| break; |
| case AsyncModifier.asyncStar: |
| out(' async*'); // ' async*' |
| break; |
| } |
| int closingPosition = blockOut(fun.body); |
| return closingPosition; |
| } |
| |
| @override |
| visitFunctionDeclaration(FunctionDeclaration declaration) { |
| VarCollector vars = new VarCollector(); |
| vars.visitFunctionDeclaration(declaration); |
| functionOut(declaration.function, declaration.name, vars); |
| } |
| |
| visitNestedExpression(Expression node, int requiredPrecedence, |
| {bool newInForInit, bool newAtStatementBegin}) { |
| bool needsParentheses = !node.isFinalized || |
| // a - (b + c). |
| (requiredPrecedence != EXPRESSION && |
| node.precedenceLevel < requiredPrecedence) || |
| // for (a = (x in o); ... ; ... ) { ... } |
| (newInForInit && node is Binary && node.op == "in") || |
| // (function() { ... })(). |
| // ({a: 2, b: 3}.toString()). |
| (newAtStatementBegin && |
| (node is NamedFunction || |
| node is FunctionExpression || |
| node is ObjectInitializer)); |
| if (needsParentheses) { |
| inForInit = false; |
| atStatementBegin = false; |
| out('('); // '(' |
| visit(node); |
| out(')'); // ')' |
| } else { |
| inForInit = newInForInit; |
| atStatementBegin = newAtStatementBegin; |
| visit(node); |
| } |
| } |
| |
| @override |
| visitVariableDeclarationList(VariableDeclarationList list) { |
| out('var '); // 'var ' |
| List<Node> nodes = list.declarations; |
| if (inForInit) { |
| visitCommaSeparated(nodes, ASSIGNMENT, |
| newInForInit: inForInit, newAtStatementBegin: false); |
| } else { |
| for (int i = 0; i < nodes.length; i++) { |
| Node node = nodes[i]; |
| if (i > 0) { |
| atStatementBegin = false; |
| out(','); // ',' |
| } |
| visitNestedExpression(node, ASSIGNMENT, |
| newInForInit: inForInit, newAtStatementBegin: false); |
| } |
| } |
| } |
| |
| void _outputIncDec(String op, Expression variable, [Expression alias]) { |
| // We can eliminate the space preceding the inc/dec in some cases, |
| // but for estimation purposes we assume the worst case. |
| if (op == '+') { |
| out(' ++'); |
| } else { |
| out(' --'); |
| } |
| visitNestedExpression(variable, UNARY, |
| newInForInit: inForInit, newAtStatementBegin: false); |
| } |
| |
| @override |
| visitAssignment(Assignment assignment) { |
| /// To print assignments like `a = a + 1` and `a = a + b` compactly as |
| /// `++a` and `a += b` in the face of [DeferredExpression]s we detect the |
| /// pattern of the undeferred assignment. |
| String op = assignment.op; |
| Node leftHandSide = assignment.leftHandSide; |
| Node rightHandSide = 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) { |
| Node rLeft = rightHandSide.left; |
| Node rRight = 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'. |
| visitNestedExpression(assignment.leftHandSide, CALL, |
| newInForInit: inForInit, newAtStatementBegin: atStatementBegin); |
| assert(op.length == 1); |
| out('$op='); // '$op=' |
| visitNestedExpression(rRight, ASSIGNMENT, |
| newInForInit: inForInit, newAtStatementBegin: false); |
| return; |
| } |
| } |
| } |
| visitNestedExpression(assignment.leftHandSide, CALL, |
| newInForInit: inForInit, newAtStatementBegin: atStatementBegin); |
| if (assignment.value != null) { |
| if (op != null) out(op); |
| out('='); // '=' |
| visitNestedExpression(assignment.value, ASSIGNMENT, |
| newInForInit: inForInit, newAtStatementBegin: false); |
| } |
| } |
| |
| @override |
| visitVariableInitialization(VariableInitialization initialization) { |
| visitAssignment(initialization); |
| } |
| |
| @override |
| visitConditional(Conditional cond) { |
| visitNestedExpression(cond.condition, LOGICAL_OR, |
| newInForInit: inForInit, newAtStatementBegin: atStatementBegin); |
| out('?'); // '?' |
| // The then part is allowed to have an 'in'. |
| visitNestedExpression(cond.then, ASSIGNMENT, |
| newInForInit: false, newAtStatementBegin: false); |
| out(':'); // ':' |
| visitNestedExpression(cond.otherwise, ASSIGNMENT, |
| newInForInit: inForInit, newAtStatementBegin: false); |
| } |
| |
| @override |
| visitNew(New node) { |
| out('new'); // 'new' |
| visitNestedExpression(node.target, LEFT_HAND_SIDE, |
| newInForInit: inForInit, newAtStatementBegin: false); |
| out('('); // '(' |
| visitCommaSeparated(node.arguments, ASSIGNMENT, |
| newInForInit: false, newAtStatementBegin: false); |
| out(')'); // ')' |
| } |
| |
| @override |
| visitCall(Call call) { |
| visitNestedExpression(call.target, CALL, |
| newInForInit: inForInit, newAtStatementBegin: atStatementBegin); |
| out('('); // '(' |
| visitCommaSeparated(call.arguments, ASSIGNMENT, |
| newInForInit: false, newAtStatementBegin: false); |
| out(')'); // ')' |
| } |
| |
| @override |
| void visitBinary(Binary binary) { |
| Expression left = binary.left; |
| Expression right = binary.right; |
| String op = binary.op; |
| int leftPrecedenceRequirement; |
| int rightPrecedenceRequirement; |
| switch (op) { |
| case ',': |
| // x, (y, z) <=> (x, y), z. |
| leftPrecedenceRequirement = EXPRESSION; |
| rightPrecedenceRequirement = EXPRESSION; |
| break; |
| case "||": |
| leftPrecedenceRequirement = LOGICAL_OR; |
| // x || (y || z) <=> (x || y) || z. |
| rightPrecedenceRequirement = LOGICAL_OR; |
| break; |
| case "&&": |
| leftPrecedenceRequirement = LOGICAL_AND; |
| // x && (y && z) <=> (x && y) && z. |
| rightPrecedenceRequirement = LOGICAL_AND; |
| break; |
| case "|": |
| leftPrecedenceRequirement = BIT_OR; |
| // x | (y | z) <=> (x | y) | z. |
| rightPrecedenceRequirement = BIT_OR; |
| break; |
| case "^": |
| leftPrecedenceRequirement = BIT_XOR; |
| // x ^ (y ^ z) <=> (x ^ y) ^ z. |
| rightPrecedenceRequirement = BIT_XOR; |
| break; |
| case "&": |
| leftPrecedenceRequirement = BIT_AND; |
| // x & (y & z) <=> (x & y) & z. |
| rightPrecedenceRequirement = BIT_AND; |
| break; |
| case "==": |
| case "!=": |
| case "===": |
| case "!==": |
| leftPrecedenceRequirement = EQUALITY; |
| rightPrecedenceRequirement = RELATIONAL; |
| break; |
| case "<": |
| case ">": |
| case "<=": |
| case ">=": |
| case "instanceof": |
| case "in": |
| leftPrecedenceRequirement = RELATIONAL; |
| rightPrecedenceRequirement = SHIFT; |
| break; |
| case ">>": |
| case "<<": |
| case ">>>": |
| leftPrecedenceRequirement = SHIFT; |
| rightPrecedenceRequirement = ADDITIVE; |
| break; |
| case "+": |
| case "-": |
| leftPrecedenceRequirement = ADDITIVE; |
| // We cannot remove parenthesis for "+" because |
| // x + (y + z) <!=> (x + y) + z: |
| // Example: |
| // "a" + (1 + 2) => "a3"; |
| // ("a" + 1) + 2 => "a12"; |
| rightPrecedenceRequirement = MULTIPLICATIVE; |
| break; |
| case "*": |
| case "/": |
| case "%": |
| leftPrecedenceRequirement = MULTIPLICATIVE; |
| // We cannot remove parenthesis for "*" because of precision issues. |
| rightPrecedenceRequirement = UNARY; |
| break; |
| default: |
| throw UnsupportedError("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(' $op '); // ' $op ' |
| } else { |
| out(op); // '$op' |
| } |
| visitNestedExpression(right, rightPrecedenceRequirement, |
| newInForInit: inForInit, newAtStatementBegin: false); |
| } |
| |
| @override |
| void visitPrefix(Prefix unary) { |
| String op = unary.op; |
| switch (op) { |
| case "delete": |
| case "void": |
| case "typeof": |
| case "+": |
| case "++": |
| case "-": |
| case "--": |
| // We may be able to eliminate the space in some cases, but for |
| // estimation we assume the worst case. |
| out('$op '); // '$op ' |
| break; |
| default: |
| out('$op'); // '$op' |
| } |
| visitNestedExpression(unary.argument, UNARY, |
| newInForInit: inForInit, newAtStatementBegin: false); |
| } |
| |
| @override |
| void visitPostfix(Postfix postfix) { |
| visitNestedExpression(postfix.argument, CALL, |
| newInForInit: inForInit, newAtStatementBegin: atStatementBegin); |
| out(postfix.op); // '${postfix.op}' |
| } |
| |
| @override |
| void visitVariableUse(VariableUse ref) { |
| // For simplicity and stability we use a constant name size estimate. |
| // In production this is: |
| // '${localNamer.getName(ref.name)' |
| out(sizeEstimate(ref)); |
| } |
| |
| @override |
| void visitThis(This node) { |
| out('this'); // 'this' |
| } |
| |
| @override |
| void visitVariableDeclaration(VariableDeclaration decl) { |
| // '${localNamer.getName(decl.name)' |
| out(sizeEstimate(decl)); |
| } |
| |
| @override |
| void visitParameter(Parameter param) { |
| // For simplicity and stability we use a constant name size estimate. |
| // In production this is: |
| // '${localNamer.getName(param.name)' |
| out(sizeEstimate(param)); |
| } |
| |
| bool isDigit(int charCode) { |
| return charCodes.$0 <= charCode && charCode <= charCodes.$9; |
| } |
| |
| bool isValidJavaScriptId(String field) { |
| if (field.length == 0) return false; |
| // Ignore the leading and trailing string-delimiter. |
| for (int i = 0; i < field.length; i++) { |
| // TODO(floitsch): allow more characters. |
| int charCode = field.codeUnitAt(i); |
| if (!(charCodes.$a <= charCode && charCode <= charCodes.$z || |
| charCodes.$A <= charCode && charCode <= charCodes.$Z || |
| charCode == charCodes.$$ || |
| charCode == charCodes.$_ || |
| i > 0 && isDigit(charCode))) { |
| return false; |
| } |
| } |
| // TODO(floitsch): normally we should also check that the field is not a |
| // reserved word. We don't generate fields with reserved word names except |
| // for 'super'. |
| if (field == 'super') return false; |
| if (field == 'catch') return false; |
| return true; |
| } |
| |
| @override |
| void visitAccess(PropertyAccess access) { |
| visitNestedExpression(access.receiver, CALL, |
| newInForInit: inForInit, newAtStatementBegin: atStatementBegin); |
| Node selector = access.selector; |
| if (selector is LiteralString) { |
| String field = literalStringToString(selector); |
| if (isValidJavaScriptId(field)) { |
| if (access.receiver is LiteralNumber) { |
| // We can eliminate the space in some cases, but for simplicity we |
| // always assume it is necessary. |
| out(' '); // ' ' |
| } |
| |
| // '.${field}' |
| out('.'); |
| out(field); |
| return; |
| } |
| } else if (selector is Name) { |
| Node receiver = access.receiver; |
| if (receiver is LiteralNumber) { |
| // We can eliminate the space in some cases, but for simplicity we |
| // always assume it is necessary. |
| out(' '); // ' ' |
| } |
| out('.'); // '.' |
| selector.accept(this); |
| return; |
| } |
| out('['); // '[' |
| visitNestedExpression(access.selector, EXPRESSION, |
| newInForInit: false, newAtStatementBegin: false); |
| out(']'); // '] |
| } |
| |
| @override |
| void visitNamedFunction(NamedFunction namedFunction) { |
| VarCollector vars = new VarCollector(); |
| vars.visitNamedFunction(namedFunction); |
| functionOut(namedFunction.function, namedFunction.name, vars); |
| } |
| |
| @override |
| void visitFun(Fun fun) { |
| VarCollector vars = new VarCollector(); |
| vars.visitFun(fun); |
| functionOut(fun, null, vars); |
| } |
| |
| @override |
| void visitArrowFunction(ArrowFunction fun) { |
| VarCollector vars = new VarCollector(); |
| vars.visitArrowFunction(fun); |
| arrowFunctionOut(fun, vars); |
| } |
| |
| int arrowFunctionOut(ArrowFunction fun, VarCollector vars) { |
| // TODO: support static, get/set, async, and generators. |
| if (fun.params.length == 1 && fun.params.first is VariableReference) { |
| visitNestedExpression(fun.params.single, ASSIGNMENT, |
| newInForInit: false, newAtStatementBegin: false); |
| } else { |
| out("("); |
| if (fun.params != null) { |
| visitCommaSeparated(fun.params, PRIMARY, |
| newInForInit: false, newAtStatementBegin: false); |
| } |
| out(")"); |
| } |
| out("=>"); |
| int closingPosition; |
| Node body = fun.body; |
| if (body is Block) { |
| closingPosition = blockOut(body); |
| } else { |
| // Object initializers require parentheses to disambiguate |
| // AssignmentExpression from FunctionBody. See: |
| // https://tc39.github.io/ecma262/#sec-arrow-function-definitions |
| bool needsParens = body is ObjectInitializer; |
| if (needsParens) out("("); |
| visitNestedExpression(body, ASSIGNMENT, |
| newInForInit: false, newAtStatementBegin: false); |
| if (needsParens) out(")"); |
| closingPosition = charCount - 1; |
| } |
| return closingPosition; |
| } |
| |
| @override |
| visitDeferredExpression(DeferredExpression node) { |
| if (node.isFinalized) { |
| // Continue printing with the expression value. |
| assert(node.precedenceLevel == node.value.precedenceLevel); |
| node.value.accept(this); |
| } else { |
| out(sizeEstimate(node)); |
| } |
| } |
| |
| @override |
| visitDeferredStatement(DeferredStatement node) { |
| if (node.isFinalized) { |
| // Continue printing with the statement value. |
| node.statement.accept(this); |
| } else { |
| sizeEstimate(node); |
| } |
| } |
| |
| outputNumberWithRequiredWhitespace(String number) { |
| int charCode = number.codeUnitAt(0); |
| if (charCode == charCodes.$MINUS) { |
| // We can eliminate the space in some cases, but for simplicity we |
| // always assume it is necessary. |
| out(' '); |
| } |
| out(number); // '${number}' |
| } |
| |
| @override |
| visitDeferredNumber(DeferredNumber node) { |
| if (node.isFinalized) { |
| outputNumberWithRequiredWhitespace("${node.value}"); |
| } else { |
| out(sizeEstimate(node)); |
| } |
| } |
| |
| @override |
| visitDeferredString(DeferredString node) { |
| if (node.isFinalized) { |
| out(node.value); |
| } else { |
| out(sizeEstimate(node)); |
| } |
| } |
| |
| @override |
| visitLiteralBool(LiteralBool node) { |
| out(node.value ? '!0' : '!1'); |
| } |
| |
| @override |
| void visitLiteralString(LiteralString node) { |
| out('"'); |
| out(literalStringToString(node)); |
| out('"'); |
| } |
| |
| @override |
| visitStringConcatenation(StringConcatenation node) { |
| node.visitChildren(this); |
| } |
| |
| @override |
| visitName(Name node) { |
| // For simplicity and stability we use a constant name size estimate. |
| out(sizeEstimate(node)); |
| } |
| |
| @override |
| visitParentheses(Parentheses node) { |
| out('('); // '(' |
| visitNestedExpression(node.enclosed, EXPRESSION, |
| newInForInit: false, newAtStatementBegin: false); |
| out(')'); // ')' |
| } |
| |
| @override |
| visitLiteralNumber(LiteralNumber node) { |
| outputNumberWithRequiredWhitespace(node.value); |
| } |
| |
| @override |
| void visitLiteralNull(LiteralNull node) { |
| out('null'); // '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]. |
| out(','); // ',' |
| continue; |
| } |
| visitNestedExpression(element, ASSIGNMENT, |
| newInForInit: false, newAtStatementBegin: false); |
| // We can skip the trailing "," for the last element (since it's not |
| // an array hole). |
| if (i != elements.length - 1) out(','); // ',' |
| } |
| out(']'); // ']' |
| } |
| |
| @override |
| void visitArrayHole(ArrayHole node) { |
| throw UnsupportedError("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 = true; |
| List<Property> properties = node.properties; |
| out('{'); // '{' |
| for (int i = 0; i < properties.length; i++) { |
| Node value = properties[i].value; |
| if (isOneLiner && exitOneLinerMode(value)) isOneLiner = false; |
| if (i != 0) { |
| out(','); // ',' |
| } |
| if (!isOneLiner) { |
| forceLine(); |
| } |
| visit(properties[i]); |
| } |
| out('}'); // '}' |
| } |
| |
| @override |
| void visitProperty(Property node) { |
| propertyNameOut(node); |
| out(':'); // ':' |
| visitNestedExpression(node.value, ASSIGNMENT, |
| newInForInit: false, newAtStatementBegin: false); |
| } |
| |
| @override |
| void visitMethodDefinition(MethodDefinition node) { |
| propertyNameOut(node); |
| VarCollector vars = new VarCollector(); |
| vars.visitMethodDefinition(node); |
| methodOut(node, vars); |
| } |
| |
| int methodOut(MethodDefinition node, VarCollector vars) { |
| // TODO: support static, get/set, async, and generators. |
| Fun fun = node.function; |
| out("("); |
| if (fun.params != null) { |
| visitCommaSeparated(fun.params, PRIMARY, |
| newInForInit: false, newAtStatementBegin: false); |
| } |
| out(")"); |
| int closingPosition = blockOut(fun.body); |
| return closingPosition; |
| } |
| |
| void propertyNameOut(Property node) { |
| Node name = node.name; |
| if (name is LiteralString) { |
| String text = literalStringToString(name); |
| if (isValidJavaScriptId(text)) { |
| out(text); |
| } else { |
| // Approximation to `_handleString(text)`. |
| out('"'); |
| out(text); |
| out('"'); |
| } |
| } else if (name is Name) { |
| node.name.accept(this); |
| } else if (name is DeferredExpression) { |
| out(sizeEstimate(name)); |
| } else { |
| assert(name is LiteralNumber); |
| LiteralNumber nameNumber = node.name; |
| out(nameNumber.value); // '${nameNumber.value}' |
| } |
| } |
| |
| @override |
| void visitRegExpLiteral(RegExpLiteral node) { |
| out(node.pattern); // '${node.pattern}' |
| } |
| |
| @override |
| void visitLiteralExpression(LiteralExpression node) { |
| out(node.template); // '${node.template}' |
| } |
| |
| @override |
| void visitLiteralStatement(LiteralStatement node) { |
| out(node.code); // '${node.code}' |
| } |
| |
| void visitInterpolatedNode(InterpolatedNode node) { |
| throw UnsupportedError('InterpolatedStatements are not supported'); |
| } |
| |
| @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) { |
| throw UnsupportedError('InterpolatedStatements are not supported'); |
| } |
| |
| @override |
| void visitInterpolatedDeclaration(InterpolatedDeclaration node) { |
| visitInterpolatedNode(node); |
| } |
| |
| @override |
| void visitComment(Comment node) { |
| // We assume output is compressed and thus do not output comments. |
| } |
| |
| @override |
| void visitAwait(Await node) { |
| out('await '); // 'await ' |
| visit(node.expression); |
| } |
| } |