OLD | NEW |
| (Empty) |
1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 part of js_ast; | |
6 | |
7 | |
8 class JavaScriptPrintingOptions { | |
9 final bool shouldCompressOutput; | |
10 final bool minifyLocalVariables; | |
11 final bool preferSemicolonToNewlineInMinifiedOutput; | |
12 | |
13 JavaScriptPrintingOptions( | |
14 {this.shouldCompressOutput: false, | |
15 this.minifyLocalVariables: false, | |
16 this.preferSemicolonToNewlineInMinifiedOutput: false}); | |
17 } | |
18 | |
19 | |
20 /// An environment in which JavaScript printing is done. Provides emitting of | |
21 /// text and pre- and post-visit callbacks. | |
22 abstract class JavaScriptPrintingContext { | |
23 /// Signals an error. This should happen only for serious internal errors. | |
24 void error(String message) { throw message; } | |
25 | |
26 /// Adds [string] to the output. | |
27 void emit(String string); | |
28 | |
29 /// Callback immediately before printing [node]. Whitespace may be printed | |
30 /// after this callback before the first non-whitespace character for [node]. | |
31 void enterNode(Node node) {} | |
32 /// Callback after printing the last character representing [node]. | |
33 void exitNode(Node node) {} | |
34 } | |
35 | |
36 /// A simple implementation of [JavaScriptPrintingContext] suitable for tests. | |
37 class SimpleJavaScriptPrintingContext extends JavaScriptPrintingContext { | |
38 final StringBuffer buffer = new StringBuffer(); | |
39 | |
40 void emit(String string) { | |
41 buffer.write(string); | |
42 } | |
43 | |
44 String getText() => buffer.toString(); | |
45 } | |
46 | |
47 | |
48 class Printer implements NodeVisitor { | |
49 final JavaScriptPrintingOptions options; | |
50 final JavaScriptPrintingContext context; | |
51 final bool shouldCompressOutput; | |
52 final DanglingElseVisitor danglingElseVisitor; | |
53 final LocalNamer localNamer; | |
54 | |
55 bool inForInit = false; | |
56 bool atStatementBegin = false; | |
57 bool pendingSemicolon = false; | |
58 bool pendingSpace = false; | |
59 | |
60 // The current indentation level. | |
61 int _indentLevel = 0; | |
62 // A cache of all indentation strings used so far. | |
63 List<String> _indentList = <String>[""]; | |
64 | |
65 static final identifierCharacterRegExp = new RegExp(r'^[a-zA-Z_0-9$]'); | |
66 static final expressionContinuationRegExp = new RegExp(r'^[-+([]'); | |
67 | |
68 Printer(JavaScriptPrintingOptions options, | |
69 JavaScriptPrintingContext context) | |
70 : options = options, | |
71 context = context, | |
72 shouldCompressOutput = options.shouldCompressOutput, | |
73 danglingElseVisitor = new DanglingElseVisitor(context), | |
74 localNamer = determineRenamer(options.shouldCompressOutput, | |
75 options.minifyLocalVariables); | |
76 | |
77 static LocalNamer determineRenamer(bool shouldCompressOutput, | |
78 bool allowVariableMinification) { | |
79 return (shouldCompressOutput && allowVariableMinification) | |
80 ? new MinifyRenamer() : new IdentityNamer(); | |
81 } | |
82 | |
83 | |
84 // The current indentation string. | |
85 String get indentation { | |
86 // Lazily add new indentation strings as required. | |
87 while (_indentList.length <= _indentLevel) { | |
88 _indentList.add(_indentList.last + " "); | |
89 } | |
90 return _indentList[_indentLevel]; | |
91 } | |
92 | |
93 void indentMore() { | |
94 _indentLevel++; | |
95 } | |
96 | |
97 void indentLess() { | |
98 _indentLevel--; | |
99 } | |
100 | |
101 | |
102 /// Always emit a newline, even under `enableMinification`. | |
103 void forceLine() { | |
104 out("\n"); | |
105 } | |
106 /// Emits a newline for readability. | |
107 void lineOut() { | |
108 if (!shouldCompressOutput) forceLine(); | |
109 } | |
110 void spaceOut() { | |
111 if (!shouldCompressOutput) out(" "); | |
112 } | |
113 | |
114 String lastAddedString = null; | |
115 int get lastCharCode { | |
116 if (lastAddedString == null) return 0; | |
117 assert(lastAddedString.length != ""); | |
118 return lastAddedString.codeUnitAt(lastAddedString.length - 1); | |
119 } | |
120 | |
121 void out(String str) { | |
122 if (str != "") { | |
123 if (pendingSemicolon) { | |
124 if (!shouldCompressOutput) { | |
125 context.emit(";"); | |
126 } else if (str != "}") { | |
127 // We want to output newline instead of semicolon because it makes | |
128 // the raw stack traces much easier to read and it also makes line- | |
129 // based tools like diff work much better. JavaScript will | |
130 // automatically insert the semicolon at the newline if it means a | |
131 // parsing error is avoided, so we can only do this trick if the | |
132 // next line is not something that can be glued onto a valid | |
133 // expression to make a new valid expression. | |
134 | |
135 // If we're using the new emitter where most pretty printed code | |
136 // is escaped in strings, it is a lot easier to deal with semicolons | |
137 // than newlines because the former doesn't need escaping. | |
138 if (options.preferSemicolonToNewlineInMinifiedOutput || | |
139 expressionContinuationRegExp.hasMatch(str)) { | |
140 context.emit(";"); | |
141 } else { | |
142 context.emit("\n"); | |
143 } | |
144 } | |
145 } | |
146 if (pendingSpace && | |
147 (!shouldCompressOutput || identifierCharacterRegExp.hasMatch(str))) { | |
148 context.emit(" "); | |
149 } | |
150 pendingSpace = false; | |
151 pendingSemicolon = false; | |
152 context.emit(str); | |
153 lastAddedString = str; | |
154 } | |
155 } | |
156 | |
157 void outLn(String str) { | |
158 out(str); | |
159 lineOut(); | |
160 } | |
161 | |
162 void outSemicolonLn() { | |
163 if (shouldCompressOutput) { | |
164 pendingSemicolon = true; | |
165 } else { | |
166 out(";"); | |
167 forceLine(); | |
168 } | |
169 } | |
170 | |
171 void outIndent(String str) { indent(); out(str); } | |
172 void outIndentLn(String str) { indent(); outLn(str); } | |
173 void indent() { | |
174 if (!shouldCompressOutput) { | |
175 out(indentation); | |
176 } | |
177 } | |
178 | |
179 visit(Node node) { | |
180 context.enterNode(node); | |
181 node.accept(this); | |
182 context.exitNode(node); | |
183 } | |
184 | |
185 visitCommaSeparated(List<Node> nodes, int hasRequiredType, | |
186 {bool newInForInit, bool newAtStatementBegin}) { | |
187 for (int i = 0; i < nodes.length; i++) { | |
188 if (i != 0) { | |
189 atStatementBegin = false; | |
190 out(","); | |
191 spaceOut(); | |
192 } | |
193 visitNestedExpression(nodes[i], hasRequiredType, | |
194 newInForInit: newInForInit, | |
195 newAtStatementBegin: newAtStatementBegin); | |
196 } | |
197 } | |
198 | |
199 visitAll(List<Node> nodes) { | |
200 nodes.forEach(visit); | |
201 } | |
202 | |
203 visitProgram(Program program) { | |
204 visitAll(program.body); | |
205 } | |
206 | |
207 bool blockBody(Node body, {bool needsSeparation, bool needsNewline}) { | |
208 if (body is Block) { | |
209 spaceOut(); | |
210 blockOut(body, false, needsNewline); | |
211 return true; | |
212 } | |
213 if (shouldCompressOutput && needsSeparation) { | |
214 // If [shouldCompressOutput] is false, then the 'lineOut' will insert | |
215 // the separation. | |
216 out(" "); | |
217 } else { | |
218 lineOut(); | |
219 } | |
220 indentMore(); | |
221 visit(body); | |
222 indentLess(); | |
223 return false; | |
224 } | |
225 | |
226 void blockOutWithoutBraces(Node node) { | |
227 if (node is Block) { | |
228 context.enterNode(node); | |
229 Block block = node; | |
230 block.statements.forEach(blockOutWithoutBraces); | |
231 context.exitNode(node); | |
232 } else { | |
233 visit(node); | |
234 } | |
235 } | |
236 | |
237 void blockOut(Block node, bool shouldIndent, bool needsNewline) { | |
238 if (shouldIndent) indent(); | |
239 context.enterNode(node); | |
240 out("{"); | |
241 lineOut(); | |
242 indentMore(); | |
243 node.statements.forEach(blockOutWithoutBraces); | |
244 indentLess(); | |
245 indent(); | |
246 out("}"); | |
247 context.exitNode(node); | |
248 if (needsNewline) lineOut(); | |
249 } | |
250 | |
251 visitBlock(Block block) { | |
252 blockOut(block, true, true); | |
253 } | |
254 | |
255 visitExpressionStatement(ExpressionStatement expressionStatement) { | |
256 indent(); | |
257 visitNestedExpression(expressionStatement.expression, EXPRESSION, | |
258 newInForInit: false, newAtStatementBegin: true); | |
259 outSemicolonLn(); | |
260 } | |
261 | |
262 visitEmptyStatement(EmptyStatement nop) { | |
263 outIndentLn(";"); | |
264 } | |
265 | |
266 void ifOut(If node, bool shouldIndent) { | |
267 Node then = node.then; | |
268 Node elsePart = node.otherwise; | |
269 bool hasElse = node.hasElse; | |
270 | |
271 // Handle dangling elses and a work-around for Android 4.0 stock browser. | |
272 // Android 4.0 requires braces for a single do-while in the `then` branch. | |
273 // See issue 10923. | |
274 if (hasElse) { | |
275 bool needsBraces = node.then.accept(danglingElseVisitor) || then is Do; | |
276 if (needsBraces) { | |
277 then = new Block(<Statement>[then]); | |
278 } | |
279 } | |
280 if (shouldIndent) indent(); | |
281 out("if"); | |
282 spaceOut(); | |
283 out("("); | |
284 visitNestedExpression(node.condition, EXPRESSION, | |
285 newInForInit: false, newAtStatementBegin: false); | |
286 out(")"); | |
287 bool thenWasBlock = | |
288 blockBody(then, needsSeparation: false, needsNewline: !hasElse); | |
289 if (hasElse) { | |
290 if (thenWasBlock) { | |
291 spaceOut(); | |
292 } else { | |
293 indent(); | |
294 } | |
295 out("else"); | |
296 if (elsePart is If) { | |
297 pendingSpace = true; | |
298 ifOut(elsePart, false); | |
299 } else { | |
300 blockBody(elsePart, needsSeparation: true, needsNewline: true); | |
301 } | |
302 } | |
303 } | |
304 | |
305 visitIf(If node) { | |
306 ifOut(node, true); | |
307 } | |
308 | |
309 visitFor(For loop) { | |
310 outIndent("for"); | |
311 spaceOut(); | |
312 out("("); | |
313 if (loop.init != null) { | |
314 visitNestedExpression(loop.init, EXPRESSION, | |
315 newInForInit: true, newAtStatementBegin: false); | |
316 } | |
317 out(";"); | |
318 if (loop.condition != null) { | |
319 spaceOut(); | |
320 visitNestedExpression(loop.condition, EXPRESSION, | |
321 newInForInit: false, newAtStatementBegin: false); | |
322 } | |
323 out(";"); | |
324 if (loop.update != null) { | |
325 spaceOut(); | |
326 visitNestedExpression(loop.update, EXPRESSION, | |
327 newInForInit: false, newAtStatementBegin: false); | |
328 } | |
329 out(")"); | |
330 blockBody(loop.body, needsSeparation: false, needsNewline: true); | |
331 } | |
332 | |
333 visitForIn(ForIn loop) { | |
334 outIndent("for"); | |
335 spaceOut(); | |
336 out("("); | |
337 visitNestedExpression(loop.leftHandSide, EXPRESSION, | |
338 newInForInit: true, newAtStatementBegin: false); | |
339 out(" in"); | |
340 pendingSpace = true; | |
341 visitNestedExpression(loop.object, EXPRESSION, | |
342 newInForInit: false, newAtStatementBegin: false); | |
343 out(")"); | |
344 blockBody(loop.body, needsSeparation: false, needsNewline: true); | |
345 } | |
346 | |
347 visitWhile(While loop) { | |
348 outIndent("while"); | |
349 spaceOut(); | |
350 out("("); | |
351 visitNestedExpression(loop.condition, EXPRESSION, | |
352 newInForInit: false, newAtStatementBegin: false); | |
353 out(")"); | |
354 blockBody(loop.body, needsSeparation: false, needsNewline: true); | |
355 } | |
356 | |
357 visitDo(Do loop) { | |
358 outIndent("do"); | |
359 if (blockBody(loop.body, needsSeparation: true, needsNewline: false)) { | |
360 spaceOut(); | |
361 } else { | |
362 indent(); | |
363 } | |
364 out("while"); | |
365 spaceOut(); | |
366 out("("); | |
367 visitNestedExpression(loop.condition, EXPRESSION, | |
368 newInForInit: false, newAtStatementBegin: false); | |
369 out(")"); | |
370 outSemicolonLn(); | |
371 } | |
372 | |
373 visitContinue(Continue node) { | |
374 if (node.targetLabel == null) { | |
375 outIndent("continue"); | |
376 } else { | |
377 outIndent("continue ${node.targetLabel}"); | |
378 } | |
379 outSemicolonLn(); | |
380 } | |
381 | |
382 visitBreak(Break node) { | |
383 if (node.targetLabel == null) { | |
384 outIndent("break"); | |
385 } else { | |
386 outIndent("break ${node.targetLabel}"); | |
387 } | |
388 outSemicolonLn(); | |
389 } | |
390 | |
391 visitReturn(Return node) { | |
392 if (node.value == null) { | |
393 outIndent("return"); | |
394 } else { | |
395 outIndent("return"); | |
396 pendingSpace = true; | |
397 visitNestedExpression(node.value, EXPRESSION, | |
398 newInForInit: false, newAtStatementBegin: false); | |
399 } | |
400 outSemicolonLn(); | |
401 } | |
402 | |
403 visitDartYield(DartYield node) { | |
404 if (node.hasStar) { | |
405 outIndent("yield*"); | |
406 } else { | |
407 outIndent("yield"); | |
408 } | |
409 pendingSpace = true; | |
410 visitNestedExpression(node.expression, EXPRESSION, | |
411 newInForInit: false, newAtStatementBegin: false); | |
412 outSemicolonLn(); | |
413 } | |
414 | |
415 | |
416 visitThrow(Throw node) { | |
417 outIndent("throw"); | |
418 pendingSpace = true; | |
419 visitNestedExpression(node.expression, EXPRESSION, | |
420 newInForInit: false, newAtStatementBegin: false); | |
421 outSemicolonLn(); | |
422 } | |
423 | |
424 visitTry(Try node) { | |
425 outIndent("try"); | |
426 blockBody(node.body, needsSeparation: true, needsNewline: false); | |
427 if (node.catchPart != null) { | |
428 visit(node.catchPart); | |
429 } | |
430 if (node.finallyPart != null) { | |
431 spaceOut(); | |
432 out("finally"); | |
433 blockBody(node.finallyPart, needsSeparation: true, needsNewline: true); | |
434 } else { | |
435 lineOut(); | |
436 } | |
437 } | |
438 | |
439 visitCatch(Catch node) { | |
440 spaceOut(); | |
441 out("catch"); | |
442 spaceOut(); | |
443 out("("); | |
444 visitNestedExpression(node.declaration, EXPRESSION, | |
445 newInForInit: false, newAtStatementBegin: false); | |
446 out(")"); | |
447 blockBody(node.body, needsSeparation: false, needsNewline: true); | |
448 } | |
449 | |
450 visitSwitch(Switch node) { | |
451 outIndent("switch"); | |
452 spaceOut(); | |
453 out("("); | |
454 visitNestedExpression(node.key, EXPRESSION, | |
455 newInForInit: false, newAtStatementBegin: false); | |
456 out(")"); | |
457 spaceOut(); | |
458 outLn("{"); | |
459 indentMore(); | |
460 visitAll(node.cases); | |
461 indentLess(); | |
462 outIndentLn("}"); | |
463 } | |
464 | |
465 visitCase(Case node) { | |
466 outIndent("case"); | |
467 pendingSpace = true; | |
468 visitNestedExpression(node.expression, EXPRESSION, | |
469 newInForInit: false, newAtStatementBegin: false); | |
470 outLn(":"); | |
471 if (!node.body.statements.isEmpty) { | |
472 indentMore(); | |
473 blockOutWithoutBraces(node.body); | |
474 indentLess(); | |
475 } | |
476 } | |
477 | |
478 visitDefault(Default node) { | |
479 outIndentLn("default:"); | |
480 if (!node.body.statements.isEmpty) { | |
481 indentMore(); | |
482 blockOutWithoutBraces(node.body); | |
483 indentLess(); | |
484 } | |
485 } | |
486 | |
487 visitLabeledStatement(LabeledStatement node) { | |
488 outIndent("${node.label}:"); | |
489 blockBody(node.body, needsSeparation: false, needsNewline: true); | |
490 } | |
491 | |
492 void functionOut(Fun fun, Node name, VarCollector vars) { | |
493 out("function"); | |
494 if (name != null) { | |
495 out(" "); | |
496 // Name must be a [Decl]. Therefore only test for primary expressions. | |
497 visitNestedExpression(name, PRIMARY, | |
498 newInForInit: false, newAtStatementBegin: false); | |
499 } | |
500 localNamer.enterScope(vars); | |
501 out("("); | |
502 if (fun.params != null) { | |
503 visitCommaSeparated(fun.params, PRIMARY, | |
504 newInForInit: false, newAtStatementBegin: false); | |
505 } | |
506 out(")"); | |
507 switch (fun.asyncModifier) { | |
508 case const AsyncModifier.sync(): | |
509 break; | |
510 case const AsyncModifier.async(): | |
511 out(' async'); | |
512 break; | |
513 case const AsyncModifier.syncStar(): | |
514 out(' sync*'); | |
515 break; | |
516 case const AsyncModifier.asyncStar(): | |
517 out(' async*'); | |
518 break; | |
519 } | |
520 blockBody(fun.body, needsSeparation: false, needsNewline: false); | |
521 localNamer.leaveScope(); | |
522 } | |
523 | |
524 visitFunctionDeclaration(FunctionDeclaration declaration) { | |
525 VarCollector vars = new VarCollector(); | |
526 vars.visitFunctionDeclaration(declaration); | |
527 indent(); | |
528 functionOut(declaration.function, declaration.name, vars); | |
529 lineOut(); | |
530 } | |
531 | |
532 visitNestedExpression(Expression node, int requiredPrecedence, | |
533 {bool newInForInit, bool newAtStatementBegin}) { | |
534 bool needsParentheses = | |
535 // a - (b + c). | |
536 (requiredPrecedence != EXPRESSION && | |
537 node.precedenceLevel < requiredPrecedence) || | |
538 // for (a = (x in o); ... ; ... ) { ... } | |
539 (newInForInit && node is Binary && node.op == "in") || | |
540 // (function() { ... })(). | |
541 // ({a: 2, b: 3}.toString()). | |
542 (newAtStatementBegin && (node is NamedFunction || | |
543 node is Fun || | |
544 node is ObjectInitializer)); | |
545 if (needsParentheses) { | |
546 inForInit = false; | |
547 atStatementBegin = false; | |
548 out("("); | |
549 visit(node); | |
550 out(")"); | |
551 } else { | |
552 inForInit = newInForInit; | |
553 atStatementBegin = newAtStatementBegin; | |
554 visit(node); | |
555 } | |
556 } | |
557 | |
558 visitVariableDeclarationList(VariableDeclarationList list) { | |
559 out("var "); | |
560 visitCommaSeparated(list.declarations, ASSIGNMENT, | |
561 newInForInit: inForInit, newAtStatementBegin: false); | |
562 } | |
563 | |
564 visitAssignment(Assignment assignment) { | |
565 visitNestedExpression(assignment.leftHandSide, LEFT_HAND_SIDE, | |
566 newInForInit: inForInit, | |
567 newAtStatementBegin: atStatementBegin); | |
568 if (assignment.value != null) { | |
569 spaceOut(); | |
570 String op = assignment.op; | |
571 if (op != null) out(op); | |
572 out("="); | |
573 spaceOut(); | |
574 visitNestedExpression(assignment.value, ASSIGNMENT, | |
575 newInForInit: inForInit, | |
576 newAtStatementBegin: false); | |
577 } | |
578 } | |
579 | |
580 visitVariableInitialization(VariableInitialization initialization) { | |
581 visitAssignment(initialization); | |
582 } | |
583 | |
584 visitConditional(Conditional cond) { | |
585 visitNestedExpression(cond.condition, LOGICAL_OR, | |
586 newInForInit: inForInit, | |
587 newAtStatementBegin: atStatementBegin); | |
588 spaceOut(); | |
589 out("?"); | |
590 spaceOut(); | |
591 // The then part is allowed to have an 'in'. | |
592 visitNestedExpression(cond.then, ASSIGNMENT, | |
593 newInForInit: false, newAtStatementBegin: false); | |
594 spaceOut(); | |
595 out(":"); | |
596 spaceOut(); | |
597 visitNestedExpression(cond.otherwise, ASSIGNMENT, | |
598 newInForInit: inForInit, newAtStatementBegin: false); | |
599 } | |
600 | |
601 visitNew(New node) { | |
602 out("new "); | |
603 visitNestedExpression(node.target, CALL, | |
604 newInForInit: inForInit, newAtStatementBegin: false); | |
605 out("("); | |
606 visitCommaSeparated(node.arguments, ASSIGNMENT, | |
607 newInForInit: false, newAtStatementBegin: false); | |
608 out(")"); | |
609 } | |
610 | |
611 visitCall(Call call) { | |
612 visitNestedExpression(call.target, LEFT_HAND_SIDE, | |
613 newInForInit: inForInit, | |
614 newAtStatementBegin: atStatementBegin); | |
615 out("("); | |
616 visitCommaSeparated(call.arguments, ASSIGNMENT, | |
617 newInForInit: false, newAtStatementBegin: false); | |
618 out(")"); | |
619 } | |
620 | |
621 visitBinary(Binary binary) { | |
622 Expression left = binary.left; | |
623 Expression right = binary.right; | |
624 String op = binary.op; | |
625 int leftPrecedenceRequirement; | |
626 int rightPrecedenceRequirement; | |
627 bool leftSpace = true; // left<HERE>op right | |
628 switch (op) { | |
629 case ',': | |
630 // x, (y, z) <=> (x, y), z. | |
631 leftPrecedenceRequirement = EXPRESSION; | |
632 rightPrecedenceRequirement = EXPRESSION; | |
633 leftSpace = false; | |
634 break; | |
635 case "||": | |
636 leftPrecedenceRequirement = LOGICAL_OR; | |
637 // x || (y || z) <=> (x || y) || z. | |
638 rightPrecedenceRequirement = LOGICAL_OR; | |
639 break; | |
640 case "&&": | |
641 leftPrecedenceRequirement = LOGICAL_AND; | |
642 // x && (y && z) <=> (x && y) && z. | |
643 rightPrecedenceRequirement = LOGICAL_AND; | |
644 break; | |
645 case "|": | |
646 leftPrecedenceRequirement = BIT_OR; | |
647 // x | (y | z) <=> (x | y) | z. | |
648 rightPrecedenceRequirement = BIT_OR; | |
649 break; | |
650 case "^": | |
651 leftPrecedenceRequirement = BIT_XOR; | |
652 // x ^ (y ^ z) <=> (x ^ y) ^ z. | |
653 rightPrecedenceRequirement = BIT_XOR; | |
654 break; | |
655 case "&": | |
656 leftPrecedenceRequirement = BIT_AND; | |
657 // x & (y & z) <=> (x & y) & z. | |
658 rightPrecedenceRequirement = BIT_AND; | |
659 break; | |
660 case "==": | |
661 case "!=": | |
662 case "===": | |
663 case "!==": | |
664 leftPrecedenceRequirement = EQUALITY; | |
665 rightPrecedenceRequirement = RELATIONAL; | |
666 break; | |
667 case "<": | |
668 case ">": | |
669 case "<=": | |
670 case ">=": | |
671 case "instanceof": | |
672 case "in": | |
673 leftPrecedenceRequirement = RELATIONAL; | |
674 rightPrecedenceRequirement = SHIFT; | |
675 break; | |
676 case ">>": | |
677 case "<<": | |
678 case ">>>": | |
679 leftPrecedenceRequirement = SHIFT; | |
680 rightPrecedenceRequirement = ADDITIVE; | |
681 break; | |
682 case "+": | |
683 case "-": | |
684 leftPrecedenceRequirement = ADDITIVE; | |
685 // We cannot remove parenthesis for "+" because | |
686 // x + (y + z) <!=> (x + y) + z: | |
687 // Example: | |
688 // "a" + (1 + 2) => "a3"; | |
689 // ("a" + 1) + 2 => "a12"; | |
690 rightPrecedenceRequirement = MULTIPLICATIVE; | |
691 break; | |
692 case "*": | |
693 case "/": | |
694 case "%": | |
695 leftPrecedenceRequirement = MULTIPLICATIVE; | |
696 // We cannot remove parenthesis for "*" because of precision issues. | |
697 rightPrecedenceRequirement = UNARY; | |
698 break; | |
699 default: | |
700 context.error("Forgot operator: $op"); | |
701 } | |
702 | |
703 visitNestedExpression(left, leftPrecedenceRequirement, | |
704 newInForInit: inForInit, | |
705 newAtStatementBegin: atStatementBegin); | |
706 | |
707 if (op == "in" || op == "instanceof") { | |
708 // There are cases where the space is not required but without further | |
709 // analysis we cannot know. | |
710 out(" "); | |
711 out(op); | |
712 out(" "); | |
713 } else { | |
714 if (leftSpace) spaceOut(); | |
715 out(op); | |
716 spaceOut(); | |
717 } | |
718 visitNestedExpression(right, rightPrecedenceRequirement, | |
719 newInForInit: inForInit, | |
720 newAtStatementBegin: false); | |
721 } | |
722 | |
723 visitPrefix(Prefix unary) { | |
724 String op = unary.op; | |
725 switch (op) { | |
726 case "delete": | |
727 case "void": | |
728 case "typeof": | |
729 // There are cases where the space is not required but without further | |
730 // analysis we cannot know. | |
731 out(op); | |
732 out(" "); | |
733 break; | |
734 case "+": | |
735 case "++": | |
736 if (lastCharCode == charCodes.$PLUS) out(" "); | |
737 out(op); | |
738 break; | |
739 case "-": | |
740 case "--": | |
741 if (lastCharCode == charCodes.$MINUS) out(" "); | |
742 out(op); | |
743 break; | |
744 default: | |
745 out(op); | |
746 } | |
747 visitNestedExpression(unary.argument, UNARY, | |
748 newInForInit: inForInit, newAtStatementBegin: false); | |
749 } | |
750 | |
751 visitPostfix(Postfix postfix) { | |
752 visitNestedExpression(postfix.argument, LEFT_HAND_SIDE, | |
753 newInForInit: inForInit, | |
754 newAtStatementBegin: atStatementBegin); | |
755 out(postfix.op); | |
756 } | |
757 | |
758 visitVariableUse(VariableUse ref) { | |
759 out(localNamer.getName(ref.name)); | |
760 } | |
761 | |
762 visitThis(This node) { | |
763 out("this"); | |
764 } | |
765 | |
766 visitVariableDeclaration(VariableDeclaration decl) { | |
767 out(localNamer.getName(decl.name)); | |
768 } | |
769 | |
770 visitParameter(Parameter param) { | |
771 out(localNamer.getName(param.name)); | |
772 } | |
773 | |
774 bool isDigit(int charCode) { | |
775 return charCodes.$0 <= charCode && charCode <= charCodes.$9; | |
776 } | |
777 | |
778 bool isValidJavaScriptId(String field) { | |
779 if (field.length < 3) return false; | |
780 // Ignore the leading and trailing string-delimiter. | |
781 for (int i = 1; i < field.length - 1; i++) { | |
782 // TODO(floitsch): allow more characters. | |
783 int charCode = field.codeUnitAt(i); | |
784 if (!(charCodes.$a <= charCode && charCode <= charCodes.$z || | |
785 charCodes.$A <= charCode && charCode <= charCodes.$Z || | |
786 charCode == charCodes.$$ || | |
787 charCode == charCodes.$_ || | |
788 i != 1 && isDigit(charCode))) { | |
789 return false; | |
790 } | |
791 } | |
792 // TODO(floitsch): normally we should also check that the field is not a | |
793 // reserved word. We don't generate fields with reserved word names except | |
794 // for 'super'. | |
795 if (field == '"super"') return false; | |
796 return true; | |
797 } | |
798 | |
799 visitAccess(PropertyAccess access) { | |
800 visitNestedExpression(access.receiver, CALL, | |
801 newInForInit: inForInit, | |
802 newAtStatementBegin: atStatementBegin); | |
803 Node selector = access.selector; | |
804 if (selector is LiteralString) { | |
805 LiteralString selectorString = selector; | |
806 String fieldWithQuotes = selectorString.value; | |
807 if (isValidJavaScriptId(fieldWithQuotes)) { | |
808 if (access.receiver is LiteralNumber) out(" "); | |
809 out("."); | |
810 out(fieldWithQuotes.substring(1, fieldWithQuotes.length - 1)); | |
811 return; | |
812 } | |
813 } | |
814 out("["); | |
815 visitNestedExpression(selector, EXPRESSION, | |
816 newInForInit: false, newAtStatementBegin: false); | |
817 out("]"); | |
818 } | |
819 | |
820 visitNamedFunction(NamedFunction namedFunction) { | |
821 VarCollector vars = new VarCollector(); | |
822 vars.visitNamedFunction(namedFunction); | |
823 functionOut(namedFunction.function, namedFunction.name, vars); | |
824 } | |
825 | |
826 visitFun(Fun fun) { | |
827 VarCollector vars = new VarCollector(); | |
828 vars.visitFun(fun); | |
829 functionOut(fun, null, vars); | |
830 } | |
831 | |
832 visitLiteralBool(LiteralBool node) { | |
833 out(node.value ? "true" : "false"); | |
834 } | |
835 | |
836 visitLiteralString(LiteralString node) { | |
837 out(node.value); | |
838 } | |
839 | |
840 visitLiteralNumber(LiteralNumber node) { | |
841 int charCode = node.value.codeUnitAt(0); | |
842 if (charCode == charCodes.$MINUS && lastCharCode == charCodes.$MINUS) { | |
843 out(" "); | |
844 } | |
845 out(node.value); | |
846 } | |
847 | |
848 visitLiteralNull(LiteralNull node) { | |
849 out("null"); | |
850 } | |
851 | |
852 visitArrayInitializer(ArrayInitializer node) { | |
853 out("["); | |
854 List<Expression> elements = node.elements; | |
855 for (int i = 0; i < elements.length; i++) { | |
856 Expression element = elements[i]; | |
857 if (element is ArrayHole) { | |
858 // Note that array holes must have a trailing "," even if they are | |
859 // in last position. Otherwise `[,]` (having length 1) would become | |
860 // equal to `[]` (the empty array) | |
861 // and [1,,] (array with 1 and a hole) would become [1,] = [1]. | |
862 out(","); | |
863 continue; | |
864 } | |
865 if (i != 0) spaceOut(); | |
866 visitNestedExpression(element, ASSIGNMENT, | |
867 newInForInit: false, newAtStatementBegin: false); | |
868 // We can skip the trailing "," for the last element (since it's not | |
869 // an array hole). | |
870 if (i != elements.length - 1) out(","); | |
871 } | |
872 out("]"); | |
873 } | |
874 | |
875 visitArrayHole(ArrayHole node) { | |
876 throw "Unreachable"; | |
877 } | |
878 | |
879 visitObjectInitializer(ObjectInitializer node) { | |
880 // Print all the properties on one line until we see a function-valued | |
881 // property. Ideally, we would use a proper pretty-printer to make the | |
882 // decision based on layout. | |
883 List<Property> properties = node.properties; | |
884 out("{"); | |
885 indentMore(); | |
886 for (int i = 0; i < properties.length; i++) { | |
887 Expression value = properties[i].value; | |
888 if (i != 0) { | |
889 out(","); | |
890 if (node.isOneLiner) spaceOut(); | |
891 } | |
892 if (!node.isOneLiner) { | |
893 forceLine(); | |
894 indent(); | |
895 } | |
896 visit(properties[i]); | |
897 } | |
898 indentLess(); | |
899 if (!node.isOneLiner && !properties.isEmpty) { | |
900 lineOut(); | |
901 indent(); | |
902 } | |
903 out("}"); | |
904 } | |
905 | |
906 visitProperty(Property node) { | |
907 if (node.name is LiteralString) { | |
908 LiteralString nameString = node.name; | |
909 String name = nameString.value; | |
910 if (isValidJavaScriptId(name)) { | |
911 out(name.substring(1, name.length - 1)); | |
912 } else { | |
913 out(name); | |
914 } | |
915 } else { | |
916 assert(node.name is LiteralNumber); | |
917 LiteralNumber nameNumber = node.name; | |
918 out(nameNumber.value); | |
919 } | |
920 out(":"); | |
921 spaceOut(); | |
922 visitNestedExpression(node.value, ASSIGNMENT, | |
923 newInForInit: false, newAtStatementBegin: false); | |
924 } | |
925 | |
926 visitRegExpLiteral(RegExpLiteral node) { | |
927 out(node.pattern); | |
928 } | |
929 | |
930 visitLiteralExpression(LiteralExpression node) { | |
931 String template = node.template; | |
932 List<Expression> inputs = node.inputs; | |
933 | |
934 List<String> parts = template.split('#'); | |
935 int inputsLength = inputs == null ? 0 : inputs.length; | |
936 if (parts.length != inputsLength + 1) { | |
937 context.error('Wrong number of arguments for JS: $template'); | |
938 } | |
939 // Code that uses JS must take care of operator precedences, and | |
940 // put parenthesis if needed. | |
941 out(parts[0]); | |
942 for (int i = 0; i < inputsLength; i++) { | |
943 visit(inputs[i]); | |
944 out(parts[i + 1]); | |
945 } | |
946 } | |
947 | |
948 visitLiteralStatement(LiteralStatement node) { | |
949 outLn(node.code); | |
950 } | |
951 | |
952 visitInterpolatedNode(InterpolatedNode node) { | |
953 out('#${node.nameOrPosition}'); | |
954 } | |
955 | |
956 visitInterpolatedExpression(InterpolatedExpression node) => | |
957 visitInterpolatedNode(node); | |
958 | |
959 visitInterpolatedLiteral(InterpolatedLiteral node) => | |
960 visitInterpolatedNode(node); | |
961 | |
962 visitInterpolatedParameter(InterpolatedParameter node) => | |
963 visitInterpolatedNode(node); | |
964 | |
965 visitInterpolatedSelector(InterpolatedSelector node) => | |
966 visitInterpolatedNode(node); | |
967 | |
968 visitInterpolatedStatement(InterpolatedStatement node) { | |
969 outLn('#${node.nameOrPosition}'); | |
970 } | |
971 | |
972 void visitComment(Comment node) { | |
973 if (shouldCompressOutput) return; | |
974 String comment = node.comment.trim(); | |
975 if (comment.isEmpty) return; | |
976 for (var line in comment.split('\n')) { | |
977 if (comment.startsWith('//')) { | |
978 outIndentLn(line.trim()); | |
979 } else { | |
980 outIndentLn('// ${line.trim()}'); | |
981 } | |
982 } | |
983 } | |
984 | |
985 void visitAwait(Await node) { | |
986 out("await "); | |
987 visit(node.expression); | |
988 } | |
989 } | |
990 | |
991 | |
992 class OrderedSet<T> { | |
993 final Set<T> set; | |
994 final List<T> list; | |
995 | |
996 OrderedSet() : set = new Set<T>(), list = <T>[]; | |
997 | |
998 void add(T x) { | |
999 if (!set.contains(x)) { | |
1000 set.add(x); | |
1001 list.add(x); | |
1002 } | |
1003 } | |
1004 | |
1005 void forEach(void fun(T x)) { | |
1006 list.forEach(fun); | |
1007 } | |
1008 } | |
1009 | |
1010 // Collects all the var declarations in the function. We need to do this in a | |
1011 // separate pass because JS vars are lifted to the top of the function. | |
1012 class VarCollector extends BaseVisitor { | |
1013 bool nested; | |
1014 final OrderedSet<String> vars; | |
1015 final OrderedSet<String> params; | |
1016 | |
1017 VarCollector() : nested = false, | |
1018 vars = new OrderedSet<String>(), | |
1019 params = new OrderedSet<String>(); | |
1020 | |
1021 void forEachVar(void fn(String v)) => vars.forEach(fn); | |
1022 void forEachParam(void fn(String p)) => params.forEach(fn); | |
1023 | |
1024 void collectVarsInFunction(Fun fun) { | |
1025 if (!nested) { | |
1026 nested = true; | |
1027 if (fun.params != null) { | |
1028 for (int i = 0; i < fun.params.length; i++) { | |
1029 params.add(fun.params[i].name); | |
1030 } | |
1031 } | |
1032 visitBlock(fun.body); | |
1033 nested = false; | |
1034 } | |
1035 } | |
1036 | |
1037 void visitFunctionDeclaration(FunctionDeclaration declaration) { | |
1038 // Note that we don't bother collecting the name of the function. | |
1039 collectVarsInFunction(declaration.function); | |
1040 } | |
1041 | |
1042 void visitNamedFunction(NamedFunction namedFunction) { | |
1043 // Note that we don't bother collecting the name of the function. | |
1044 collectVarsInFunction(namedFunction.function); | |
1045 } | |
1046 | |
1047 void visitFun(Fun fun) { | |
1048 collectVarsInFunction(fun); | |
1049 } | |
1050 | |
1051 void visitThis(This node) {} | |
1052 | |
1053 void visitVariableDeclaration(VariableDeclaration decl) { | |
1054 if (decl.allowRename) vars.add(decl.name); | |
1055 } | |
1056 } | |
1057 | |
1058 | |
1059 /** | |
1060 * Returns true, if the given node must be wrapped into braces when used | |
1061 * as then-statement in an [If] that has an else branch. | |
1062 */ | |
1063 class DanglingElseVisitor extends BaseVisitor<bool> { | |
1064 JavaScriptPrintingContext context; | |
1065 | |
1066 DanglingElseVisitor(this.context); | |
1067 | |
1068 bool visitProgram(Program node) => false; | |
1069 | |
1070 bool visitNode(Statement node) { | |
1071 context.error("Forgot node: $node"); | |
1072 return null; | |
1073 } | |
1074 | |
1075 bool visitBlock(Block node) => false; | |
1076 bool visitExpressionStatement(ExpressionStatement node) => false; | |
1077 bool visitEmptyStatement(EmptyStatement node) => false; | |
1078 bool visitIf(If node) { | |
1079 if (!node.hasElse) return true; | |
1080 return node.otherwise.accept(this); | |
1081 } | |
1082 bool visitFor(For node) => node.body.accept(this); | |
1083 bool visitForIn(ForIn node) => node.body.accept(this); | |
1084 bool visitWhile(While node) => node.body.accept(this); | |
1085 bool visitDo(Do node) => false; | |
1086 bool visitContinue(Continue node) => false; | |
1087 bool visitBreak(Break node) => false; | |
1088 bool visitReturn(Return node) => false; | |
1089 bool visitThrow(Throw node) => false; | |
1090 bool visitTry(Try node) { | |
1091 if (node.finallyPart != null) { | |
1092 return node.finallyPart.accept(this); | |
1093 } else { | |
1094 return node.catchPart.accept(this); | |
1095 } | |
1096 } | |
1097 bool visitCatch(Catch node) => node.body.accept(this); | |
1098 bool visitSwitch(Switch node) => false; | |
1099 bool visitCase(Case node) => false; | |
1100 bool visitDefault(Default node) => false; | |
1101 bool visitFunctionDeclaration(FunctionDeclaration node) => false; | |
1102 bool visitLabeledStatement(LabeledStatement node) | |
1103 => node.body.accept(this); | |
1104 bool visitLiteralStatement(LiteralStatement node) => true; | |
1105 | |
1106 bool visitExpression(Expression node) => false; | |
1107 } | |
1108 | |
1109 | |
1110 abstract class LocalNamer { | |
1111 String getName(String oldName); | |
1112 String declareVariable(String oldName); | |
1113 String declareParameter(String oldName); | |
1114 void enterScope(VarCollector vars); | |
1115 void leaveScope(); | |
1116 } | |
1117 | |
1118 | |
1119 class IdentityNamer implements LocalNamer { | |
1120 String getName(String oldName) => oldName; | |
1121 String declareVariable(String oldName) => oldName; | |
1122 String declareParameter(String oldName) => oldName; | |
1123 void enterScope(VarCollector vars) {} | |
1124 void leaveScope() {} | |
1125 } | |
1126 | |
1127 | |
1128 class MinifyRenamer implements LocalNamer { | |
1129 final List<Map<String, String>> maps = []; | |
1130 final List<int> parameterNumberStack = []; | |
1131 final List<int> variableNumberStack = []; | |
1132 int parameterNumber = 0; | |
1133 int variableNumber = 0; | |
1134 | |
1135 MinifyRenamer(); | |
1136 | |
1137 void enterScope(VarCollector vars) { | |
1138 maps.add(new Map<String, String>()); | |
1139 variableNumberStack.add(variableNumber); | |
1140 parameterNumberStack.add(parameterNumber); | |
1141 vars.forEachVar(declareVariable); | |
1142 vars.forEachParam(declareParameter); | |
1143 } | |
1144 | |
1145 void leaveScope() { | |
1146 maps.removeLast(); | |
1147 variableNumber = variableNumberStack.removeLast(); | |
1148 parameterNumber = parameterNumberStack.removeLast(); | |
1149 } | |
1150 | |
1151 String getName(String oldName) { | |
1152 // Go from inner scope to outer looking for mapping of name. | |
1153 for (int i = maps.length - 1; i >= 0; i--) { | |
1154 var map = maps[i]; | |
1155 var replacement = map[oldName]; | |
1156 if (replacement != null) return replacement; | |
1157 } | |
1158 return oldName; | |
1159 } | |
1160 | |
1161 static const LOWER_CASE_LETTERS = 26; | |
1162 static const LETTERS = LOWER_CASE_LETTERS; | |
1163 static const DIGITS = 10; | |
1164 | |
1165 static int nthLetter(int n) { | |
1166 return (n < LOWER_CASE_LETTERS) ? | |
1167 charCodes.$a + n : | |
1168 charCodes.$A + n - LOWER_CASE_LETTERS; | |
1169 } | |
1170 | |
1171 // Parameters go from a to z and variables go from z to a. This makes each | |
1172 // argument list and each top-of-function var declaration look similar and | |
1173 // helps gzip compress the file. If we have more than 26 arguments and | |
1174 // variables then we meet somewhere in the middle of the alphabet. After | |
1175 // that we give up trying to be nice to the compression algorithm and just | |
1176 // use the same namespace for arguments and variables, starting with A, and | |
1177 // moving on to a0, a1, etc. | |
1178 String declareVariable(String oldName) { | |
1179 if (avoidRenaming(oldName)) return oldName; | |
1180 var newName; | |
1181 if (variableNumber + parameterNumber < LOWER_CASE_LETTERS) { | |
1182 // Variables start from z and go backwards, for better gzipability. | |
1183 newName = getNameNumber(oldName, LOWER_CASE_LETTERS - 1 - variableNumber); | |
1184 } else { | |
1185 // After 26 variables and parameters we allocate them in the same order. | |
1186 newName = getNameNumber(oldName, variableNumber + parameterNumber); | |
1187 } | |
1188 variableNumber++; | |
1189 return newName; | |
1190 } | |
1191 | |
1192 String declareParameter(String oldName) { | |
1193 if (avoidRenaming(oldName)) return oldName; | |
1194 var newName; | |
1195 if (variableNumber + parameterNumber < LOWER_CASE_LETTERS) { | |
1196 newName = getNameNumber(oldName, parameterNumber); | |
1197 } else { | |
1198 newName = getNameNumber(oldName, variableNumber + parameterNumber); | |
1199 } | |
1200 parameterNumber++; | |
1201 return newName; | |
1202 } | |
1203 | |
1204 bool avoidRenaming(String oldName) { | |
1205 // Variables of this $form$ are used in pattern matching the message of JS | |
1206 // exceptions, so should not be renamed. | |
1207 // TODO(sra): Introduce a way for indicating in the JS text which variables | |
1208 // should not be renamed. | |
1209 return oldName.startsWith(r'$') && oldName.endsWith(r'$'); | |
1210 } | |
1211 | |
1212 String getNameNumber(String oldName, int n) { | |
1213 if (maps.isEmpty) return oldName; | |
1214 | |
1215 String newName; | |
1216 if (n < LETTERS) { | |
1217 // Start naming variables a, b, c, ..., z, A, B, C, ..., Z. | |
1218 newName = new String.fromCharCodes([nthLetter(n)]); | |
1219 } else { | |
1220 // Then name variables a0, a1, a2, ..., a9, b0, b1, ..., Z9, aa0, aa1, ... | |
1221 // For all functions with fewer than 500 locals this is just as compact | |
1222 // as using aa, ab, etc. but avoids clashes with keywords. | |
1223 n -= LETTERS; | |
1224 int digit = n % DIGITS; | |
1225 n ~/= DIGITS; | |
1226 int alphaChars = 1; | |
1227 int nameSpaceSize = LETTERS; | |
1228 // Find out whether we should use the 1-character namespace (size 52), the | |
1229 // 2-character namespace (size 52*52), etc. | |
1230 while (n >= nameSpaceSize) { | |
1231 n -= nameSpaceSize; | |
1232 alphaChars++; | |
1233 nameSpaceSize *= LETTERS; | |
1234 } | |
1235 var codes = <int>[]; | |
1236 for (var i = 0; i < alphaChars; i++) { | |
1237 nameSpaceSize ~/= LETTERS; | |
1238 codes.add(nthLetter((n ~/ nameSpaceSize) % LETTERS)); | |
1239 } | |
1240 codes.add(charCodes.$0 + digit); | |
1241 newName = new String.fromCharCodes(codes); | |
1242 } | |
1243 assert(new RegExp(r'[a-zA-Z][a-zA-Z0-9]*').hasMatch(newName)); | |
1244 maps.last[oldName] = newName; | |
1245 return newName; | |
1246 } | |
1247 } | |
OLD | NEW |