Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(553)

Unified Diff: lib/compiler/implementation/js/printer.dart

Issue 10825180: Add JavaScript AST. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Cosmetic changes (comments). Created 8 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: lib/compiler/implementation/js/printer.dart
diff --git a/lib/compiler/implementation/js/printer.dart b/lib/compiler/implementation/js/printer.dart
new file mode 100644
index 0000000000000000000000000000000000000000..0cb01bc7f0043b2c4ffcfbe760890f33a8b7c839
--- /dev/null
+++ b/lib/compiler/implementation/js/printer.dart
@@ -0,0 +1,778 @@
+// 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.
+
+class Printer implements NodeVisitor {
+ final bool shouldCompressOutput = false;
+ leg.Compiler compiler;
+ var positionElement;
+ leg.CodeBuffer outBuffer;
+ int indentLevel = 0;
+ bool inForInit = false;
+ bool atStatementBegin = false;
+
+ Printer(this.compiler, this.positionElement)
+ : outBuffer = new leg.CodeBuffer();
+
+ void spaceOut() {
+ if (!shouldCompressOutput) out(" ");
+ }
+ void lineOut() {
+ if (!shouldCompressOutput) out("\n");
+ }
+
+ String lastAddedString = null;
+ int get lastCharCode() {
+ if (lastAddedString == null) return 0;
+ assert(lastAddedString.length != "");
+ return lastAddedString.charCodeAt(lastAddedString.length - 1);
+ }
+
+ void out(String str) {
+ if (str != "") {
+ outBuffer.add(str);
+ lastAddedString = str;
+ }
+ }
+ void outLn(String str) {
+ out(str);
+ lineOut();
+ }
+ void outIndent(String str) { indent(); out(str); }
+ void outIndentLn(String str) { indent(); outLn(str); }
+ void indent() {
+ if (!shouldCompressOutput) {
+ for (int i = 0; i < indentLevel; i++) out(" ");
+ }
+ }
+
+ void recordSourcePosition(var position) {
+ if (position != null) {
+ outBuffer.setSourceLocation(positionElement, position);
+ }
+ }
+
+ visit(Node node) {
+ recordSourcePosition(node.sourcePosition);
+ node.accept(this);
+ recordSourcePosition(node.endSourcePosition);
+ }
+
+ visitCommaSeparated(List<Node> nodes, int hasRequiredType,
+ [bool newInForInit, bool newAtStatementBegin]) {
+ for (int i = 0; i < nodes.length; i++) {
+ if (i != 0) {
+ atStatementBegin = false;
+ out(",");
+ spaceOut();
+ }
+ visitNestedExpression(nodes[i], hasRequiredType,
+ newInForInit, newAtStatementBegin);
+ }
+ }
+
+ visitAll(List<Node> nodes) {
+ nodes.forEach(visit);
+ }
+
+ visitProgram(Program program) {
+ visitAll(program.body);
+ }
+
+ bool blockBody(Node body, [bool needsSeparation, bool needsNewline]) {
+ if (body is Block) {
+ spaceOut();
+ blockOut(body, false, needsNewline);
+ return true;
+ }
+ if (shouldCompressOutput && needsSeparation) {
+ // If [shouldCompressOutput] is false, then the 'lineOut' will insert
+ // the separation.
+ out(" ");
+ } else {
+ lineOut();
+ }
+ indentLevel++;
+ visit(body);
+ indentLevel--;
+ return false;
+ }
+
+ void blockOutWithoutBraces(Node node) {
+ if (node is Block) {
+ node.statements.forEach(blockOutWithoutBraces);
+ } else {
+ visit(node);
+ }
+ }
+
+ void blockOut(Block node, bool shouldIndent, bool needsNewline) {
+ if (shouldIndent) indent();
+ out("{");
+ lineOut();
+ indentLevel++;
+ node.statements.forEach(blockOutWithoutBraces);
+ indentLevel--;
+ indent();
+ out("}");
+ if (needsNewline) lineOut();
+ }
+
+ visitBlock(Block block) {
+ blockOut(block, true, true);
+ }
+
+ visitExpressionStatement(ExpressionStatement expressionStatement) {
+ indent();
+ visitNestedExpression(expressionStatement.expression, EXPRESSION,
+ newInForInit: false, newAtStatementBegin: true);
+ outLn(";");
+ }
+
+ visitEmptyStatement(EmptyStatement nop) {
+ outIndentLn(";");
+ }
+
+ void ifOut(If node, bool shouldIndent) {
+ Node then = node.then;
+ Node elsePart = node.otherwise;
+ bool hasElse = node.hasElse;
+ // Handle dangling elses.
+ // If the then-branch is an if, which has no else-branch, but we do
+ // have one, then we need to put the nested if into braces.
+ if (hasElse && then is If) {
+ Node nested = then;
+ do {
+ If nestedIf = nested;
+ if (!nestedIf.hasElse) {
+ then = new Block(<Statement>[then]);
+ break;
+ }
+ nested = nestedIf.otherwise;
+ } while (nested is If);
+ }
+ if (then is If && !(then as If).hasElse && hasElse) {
+ then = new Block(<Statement>[then]);
+ }
+ if (shouldIndent) indent();
+ out("if");
+ spaceOut();
+ out("(");
+ visitNestedExpression(node.condition, EXPRESSION,
+ newInForInit: false, newAtStatementBegin: false);
+ out(")");
+ bool thenWasBlock =
+ blockBody(then, needsSeparation: false, needsNewline: !hasElse);
+ if (hasElse) {
+ if (thenWasBlock) {
+ spaceOut();
+ } else {
+ indent();
+ }
+ out("else");
+ if (elsePart is If) {
+ out(" ");
+ ifOut(elsePart, false);
+ } else {
+ blockBody(elsePart, needsSeparation: true, needsNewline: true);
+ }
+ }
+ }
+
+ visitIf(If node) {
+ ifOut(node, true);
+ }
+
+ visitFor(For loop) {
+ outIndent("for");
+ spaceOut();
+ out("(");
+ if (loop.init !== null) {
+ visitNestedExpression(loop.init, EXPRESSION,
+ newInForInit: true, newAtStatementBegin: false);
+ }
+ out(";");
+ if (loop.condition !== null) {
+ spaceOut();
+ visitNestedExpression(loop.condition, EXPRESSION,
+ newInForInit: false, newAtStatementBegin: false);
+ }
+ out(";");
+ if (loop.update !== null) {
+ spaceOut();
+ visitNestedExpression(loop.update, EXPRESSION,
+ newInForInit: false, newAtStatementBegin: false);
+ }
+ out(")");
+ blockBody(loop.body, needsSeparation: false, needsNewline: true);
+ }
+
+ visitForIn(ForIn loop) {
+ outIndent("for");
+ spaceOut();
+ out("(");
+ visitNestedExpression(loop.leftHandSide, EXPRESSION,
+ newInForInit: true, newAtStatementBegin: false);
+ out(" in ");
+ visitNestedExpression(loop.object, EXPRESSION,
+ newInForInit: false, newAtStatementBegin: false);
+ out(")");
+ blockBody(loop.body, needsSeparation: false, needsNewline: true);
+ }
+
+ visitWhile(While loop) {
+ outIndent("while");
+ spaceOut();
+ out("(");
+ visitNestedExpression(loop.condition, EXPRESSION,
+ newInForInit: false, newAtStatementBegin: false);
+ out(")");
+ blockBody(loop.body, needsSeparation: false, needsNewline: true);
+ }
+
+ visitDo(Do loop) {
+ outIndent("do");
+ if (blockBody(loop.body, needsSeparation: true, needsNewline: false)) {
+ spaceOut();
+ } else {
+ indent();
+ }
+ out("while");
+ spaceOut();
+ out("(");
+ visitNestedExpression(loop.condition, EXPRESSION,
+ newInForInit: false, newAtStatementBegin: false);
+ outLn(");");
+ }
+
+ visitContinue(Continue node) {
+ if (node.targetLabel == null) {
+ outIndentLn("continue;");
+ } else {
+ outIndentLn("continue ${node.targetLabel};");
+ }
+ }
+
+ visitBreak(Break node) {
+ if (node.targetLabel == null) {
+ outIndentLn("break;");
+ } else {
+ outIndentLn("break ${node.targetLabel};");
+ }
+ }
+
+ visitReturn(Return node) {
+ if (node.value == null) {
+ outIndentLn("return;");
+ } else {
+ outIndent("return ");
+ visitNestedExpression(node.value, EXPRESSION,
+ newInForInit: false, newAtStatementBegin: false);
+ outLn(";");
+ }
+ }
+
+ visitThrow(Throw node) {
+ outIndent("throw ");
+ visitNestedExpression(node.expression, EXPRESSION,
+ newInForInit: false, newAtStatementBegin: false);
+ outLn(";");
+ }
+
+ visitTry(Try node) {
+ outIndent("try");
+ blockBody(node.body, needsSeparation: true, needsNewline: false);
+ spaceOut();
+ if (node.catchPart !== null) {
+ visit(node.catchPart);
+ }
+ if (node.finallyPart !== null) {
+ spaceOut();
+ out("finally");
+ blockBody(node.finallyPart, needsSeparation: true, needsNewline: true);
+ } else {
+ lineOut();
+ }
+ }
+
+ visitCatch(Catch node) {
+ spaceOut();
+ out("catch");
+ spaceOut();
+ out("(");
+ visitNestedExpression(node.declaration, EXPRESSION,
+ newInForInit: false, newAtStatementBegin: false);
+ out(")");
+ blockBody(node.body, needsSeparation: false, needsNewline: true);
+ }
+
+ visitSwitch(Switch node) {
+ outIndent("switch");
+ spaceOut();
+ out("(");
+ visitNestedExpression(node.key, EXPRESSION,
+ newInForInit: false, newAtStatementBegin: false);
+ out(")");
+ spaceOut();
+ outLn("{");
+ indentLevel++;
+ visitAll(node.cases);
+ indentLevel--;
+ outIndentLn("}");
+ }
+
+ visitCase(Case node) {
+ outIndent("case ");
+ visitNestedExpression(node.expression, EXPRESSION,
+ newInForInit: false, newAtStatementBegin: false);
+ outLn(":");
+ if (!node.body.statements.isEmpty()) {
+ indentLevel++;
+ blockOutWithoutBraces(node.body);
+ indentLevel--;
+ }
+ }
+
+ visitDefault(Default node) {
+ outIndentLn("default:");
+ if (!node.body.statements.isEmpty()) {
+ indentLevel++;
+ blockOutWithoutBraces(node.body);
+ indentLevel--;
+ }
+ }
+
+ visitLabeledStatement(LabeledStatement node) {
+ outIndent("${node.label}:");
+ blockBody(node.body, needsSeparation: false, needsNewline: true);
+ }
+
+ void functionOut(Fun fun, Node name) {
+ out("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(")");
+ blockBody(fun.body, needsSeparation: false, needsNewline: false);
+ }
+
+ visitFunctionDeclaration(FunctionDeclaration declaration) {
+ indent();
+ functionOut(declaration.function, declaration.name);
+ lineOut();
+ }
+
+ visitNestedExpression(Expression node, int requiredPrecedence,
+ [bool newInForInit, bool newAtStatementBegin]) {
+ bool needsParentheses =
+ // a - (b + c).
+ (requiredPrecedence != EXPRESSION &&
+ node.precedenceLevel < requiredPrecedence) ||
+ // for (a = (x in o); ... ; ... ) { ... }
+ (newInForInit && node is Binary && (node as Binary).op == "in") ||
+ // (function() { ... })().
+ // ({a: 2, b: 3}.toString()).
+ (newAtStatementBegin && (node is NamedFunction ||
+ node is Fun ||
+ node is ObjectInitialization));
+ if (needsParentheses) {
+ inForInit = false;
+ atStatementBegin = false;
+ out("(");
+ visit(node);
+ out(")");
+ } else {
+ inForInit = newInForInit;
+ atStatementBegin = newAtStatementBegin;
+ visit(node);
+ }
+ }
+
+ visitVariableDeclarationList(VariableDeclarationList list) {
+ out("var ");
+ visitCommaSeparated(list.declarations, ASSIGNMENT,
+ newInForInit: inForInit, newAtStatementBegin: false);
+ }
+
+ visitSequence(Sequence sequence) {
+ // Note that we only require that the entries are expressions and not
+ // assignments. This means that nested sequences are not put into
+ // parenthesis.
+ visitCommaSeparated(sequence.expressions, EXPRESSION,
+ newInForInit: false,
+ newAtStatementBegin: atStatementBegin);
+ }
+
+ visitAssignment(Assignment assignment) {
+ visitNestedExpression(assignment.leftHandSide, LEFT_HAND_SIDE,
+ newInForInit: inForInit,
+ newAtStatementBegin: atStatementBegin);
+ if (assignment.value !== null) {
+ spaceOut();
+ String op = assignment.op;
+ if (op != null) out(op);
+ out("=");
+ spaceOut();
+ visitNestedExpression(assignment.value, ASSIGNMENT,
+ newInForInit: inForInit,
+ newAtStatementBegin: false);
+ }
+ }
+
+ visitVariableInitialization(VariableInitialization initialization) {
+ visitAssignment(initialization);
+ }
+
+ visitConditional(Conditional cond) {
+ visitNestedExpression(cond.condition, LOGICAL_OR,
+ newInForInit: inForInit,
+ newAtStatementBegin: atStatementBegin);
+ spaceOut();
+ out("?");
+ spaceOut();
+ // The then part is allowed to have an 'in'.
+ visitNestedExpression(cond.then, ASSIGNMENT,
+ newInForInit: false, newAtStatementBegin: false);
+ spaceOut();
+ out(":");
+ spaceOut();
+ visitNestedExpression(cond.otherwise, ASSIGNMENT,
+ newInForInit: inForInit, newAtStatementBegin: false);
+ }
+
+ visitNew(New node) {
+ out("new ");
+ visitNestedExpression(node.target, CALL,
+ newInForInit: inForInit, newAtStatementBegin: false);
+ out("(");
+ visitCommaSeparated(node.arguments, ASSIGNMENT,
+ newInForInit: false, newAtStatementBegin: false);
+ out(")");
+ }
+
+ visitCall(Call call) {
+ visitNestedExpression(call.target, LEFT_HAND_SIDE,
+ newInForInit: inForInit,
+ newAtStatementBegin: atStatementBegin);
+ out("(");
+ visitCommaSeparated(call.arguments, ASSIGNMENT,
+ newInForInit: false, newAtStatementBegin: false);
+ out(")");
+ }
+
+ visitBinary(Binary binary) {
+ Expression left = binary.left;
+ Expression right = binary.right;
+ String op = binary.op;
+ int leftPrecedenceRequirement;
+ int rightPrecedenceRequirement;
+ switch (op) {
+ 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:
+ compiler.internalError("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(" ");
+ out(op);
+ out(" ");
+ } else {
+ spaceOut();
+ out(op);
+ spaceOut();
+ }
+ visitNestedExpression(right, rightPrecedenceRequirement,
+ newInForInit: inForInit,
+ newAtStatementBegin: false);
+ }
+
+ 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(" ");
+ break;
+ case "+":
+ case "++":
+ if (lastCharCode == charCodes.$PLUS) out(" ");
+ out(op);
+ break;
+ case "-":
+ case "--":
+ if (lastCharCode == charCodes.$MINUS) out(" ");
+ out(op);
+ break;
+ default:
+ out(op);
+ }
+ visitNestedExpression(unary.argument, UNARY,
+ newInForInit: inForInit, newAtStatementBegin: false);
+ }
+
+ visitPostfix(Postfix postfix) {
+ visitNestedExpression(postfix.argument, LEFT_HAND_SIDE,
+ newInForInit: inForInit,
+ newAtStatementBegin: atStatementBegin);
+ out(postfix.op);
+ }
+
+ visitVariableUse(VariableUse ref) {
+ out(ref.name);
+ }
+
+ visitThis(This node) {
+ out("this");
+ }
+
+ visitVariableDeclaration(VariableDeclaration decl) {
+ out(decl.name);
+ }
+
+ visitParameter(Parameter param) {
+ out(param.name);
+ }
+
+ bool isDigit(int charCode) {
+ return charCodes.$0 <= charCode && charCode <= charCodes.$9;
+ }
+
+ bool isValidJavaScriptId(String field) {
+ if (field.length < 3) return false;
+ // Ignore the leading and trailing string-delimiter.
+ for (int i = 1; i < field.length - 1; i++) {
+ // TODO(floitsch): allow more characters.
+ int charCode = field.charCodeAt(i);
+ if (!(charCodes.$a <= charCode && charCode <= charCodes.$z ||
+ charCodes.$A <= charCode && charCode <= charCodes.$Z ||
+ charCode == charCodes.$$ ||
+ charCode == charCodes.$_ ||
+ i != 1 && isDigit(charCode))) {
+ return false;
+ }
+ }
+ // TODO(floitsch): normally we should also check that the field is not
+ // a reserved word.
+ return true;
+ }
+
+ visitAccess(PropertyAccess access) {
+ visitNestedExpression(access.receiver, CALL,
+ newInForInit: inForInit,
+ newAtStatementBegin: atStatementBegin);
+ Node selector = access.selector;
+ if (selector is LiteralString) {
+ String fieldWithQuotes = (selector as LiteralString).value;
+ if (isValidJavaScriptId(fieldWithQuotes)) {
+ if (isDigit(lastCharCode)) out(" ");
+ out(".");
+ out(fieldWithQuotes.substring(1, fieldWithQuotes.length - 1));
+ return;
+ }
+ }
+ out("[");
+ visitNestedExpression(selector, EXPRESSION,
+ newInForInit: false, newAtStatementBegin: false);
+ out("]");
+ }
+
+ visitNamedFunction(NamedFunction namedFunction) {
+ functionOut(namedFunction.function, namedFunction.name);
+ }
+
+ visitFun(Fun fun) {
+ functionOut(fun, null);
+ }
+
+ visitLiteralBool(LiteralBool node) {
+ out(node.value ? "true" : "false");
+ }
+
+ visitLiteralString(LiteralString node) {
+ out(node.value);
+ }
+
+ visitLiteralNumber(LiteralNumber node) {
+ int charCode = node.value.charCodeAt(0);
+ if (charCode == charCodes.$MINUS && lastCharCode == charCodes.$MINUS) {
+ out(" ");
+ }
+ out(node.value);
+ }
+
+ visitLiteralNull(LiteralNull node) {
+ out("null");
+ }
+
+ visitArrayInitialization(ArrayInitialization node) {
+ out("[");
+ List<ArrayElement> elements = node.elements;
+ int elementIndex = 0;
+ for (int i = 0; i < node.length; i++) {
+ if (elementIndex < elements.length &&
+ elements[elementIndex].index == i) {
+ visitNestedExpression(elements[elementIndex].value, ASSIGNMENT,
+ newInForInit: false, newAtStatementBegin: false);
+ elementIndex++;
+ // We can avoid a trailing "," if there was an element just before. So
+ // `[1]` and `[1,]` are the same, but `[,]` and `[]` are not.
+ if (i != node.length - 1) {
+ out(",");
+ spaceOut();
+ }
+ } else {
+ out(",");
+ }
+ }
+ out("]");
+ }
+
+ visitArrayElement(ArrayElement node) {
+ throw "Unreachable";
+ }
+
+ visitObjectLiteral(ObjectInitialization node) {
+ out("{");
+ List<Property> properties = node.properties;
+ for (int i = 0; i < properties.length; i++) {
+ if (i != 0) {
+ out(",");
+ spaceOut();
+ }
+ visitProperty(properties[i]);
+ }
+ out("}");
+ }
+
+ visitProperty(Property node) {
+ if (node.name is LiteralString) {
+ String name = (node.name as LiteralString).value;
+ if (isValidJavaScriptId(name)) {
+ out(name.substring(1, name.length - 1));
+ } else {
+ out(name);
+ }
+ } else {
+ assert(node.name is LiteralNumber);
+ out((node.name as LiteralNumber).value);
+ }
+ out(":");
+ spaceOut();
+ visitNestedExpression(node.value, ASSIGNMENT,
+ newInForInit: false, newAtStatementBegin: false);
+ }
+
+ visitRegExpLiteral(RegExpLiteral node) {
+ out(node.pattern);
+ }
+
+ visitLiteralExpression(LiteralExpression node) {
+ String template = node.template;
+ List<Expression> inputs = node.inputs;
+
+ List<String> parts = template.split('#');
+ if (parts.length != inputs.length + 1) {
+ compiler.internalError('Wrong number of arguments for JS: $template');
+ }
+ out("(");
+ out(parts[0]);
+ for (int i = 0; i < inputs.length; i++) {
+ visit(inputs[i]);
+ out(parts[i + 1]);
+ }
+ out(")");
+ }
+
+ visitLiteralStatement(LiteralStatement node) {
+ outLn(node.code);
+ }
+}
+
+leg.CodeBuffer prettyPrint(Node node,
+ leg.Compiler compiler,
+ Dynamic positionElement) {
+ Printer printer = new Printer(compiler, positionElement);
+ printer.visit(node);
+ return printer.outBuffer;
+}

Powered by Google App Engine
This is Rietveld 408576698