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 final bool emitTypes; | |
13 final bool allowSingleLineIfStatements; | |
14 | |
15 /// True to allow keywords in properties, such as `obj.var` or `obj.function` | |
16 /// Modern JS engines support this. | |
17 final bool allowKeywordsInProperties; | |
18 | |
19 JavaScriptPrintingOptions( | |
20 {this.shouldCompressOutput: false, | |
21 this.minifyLocalVariables: false, | |
22 this.preferSemicolonToNewlineInMinifiedOutput: false, | |
23 this.emitTypes: false, | |
24 this.allowKeywordsInProperties: false, | |
25 this.allowSingleLineIfStatements: false}); | |
26 } | |
27 | |
28 | |
29 /// An environment in which JavaScript printing is done. Provides emitting of | |
30 /// text and pre- and post-visit callbacks. | |
31 abstract class JavaScriptPrintingContext { | |
32 /// Signals an error. This should happen only for serious internal errors. | |
33 void error(String message) { throw message; } | |
34 | |
35 /// Adds [string] to the output. | |
36 void emit(String string); | |
37 | |
38 /// Callback immediately before printing [node]. Whitespace may be printed | |
39 /// after this callback before the first non-whitespace character for [node]. | |
40 void enterNode(Node node) {} | |
41 /// Callback after printing the last character representing [node]. | |
42 void exitNode(Node node) {} | |
43 } | |
44 | |
45 /// A simple implementation of [JavaScriptPrintingContext] suitable for tests. | |
46 class SimpleJavaScriptPrintingContext extends JavaScriptPrintingContext { | |
47 final StringBuffer buffer = new StringBuffer(); | |
48 | |
49 void emit(String string) { | |
50 buffer.write(string); | |
51 } | |
52 | |
53 String getText() => buffer.toString(); | |
54 } | |
55 | |
56 // TODO(ochafik): Inline the body of [TypeScriptTypePrinter] here if/when it no | |
57 // longer needs to share utils with [ClosureTypePrinter]. | |
58 class Printer extends TypeScriptTypePrinter implements NodeVisitor { | |
59 final JavaScriptPrintingOptions options; | |
60 final JavaScriptPrintingContext context; | |
61 final bool shouldCompressOutput; | |
62 final DanglingElseVisitor danglingElseVisitor; | |
63 final LocalNamer localNamer; | |
64 | |
65 bool inForInit = false; | |
66 bool atStatementBegin = false; | |
67 bool inNewTarget = false; | |
68 bool pendingSemicolon = false; | |
69 bool pendingSpace = false; | |
70 | |
71 // The current indentation level. | |
72 int _indentLevel = 0; | |
73 // A cache of all indentation strings used so far. | |
74 List<String> _indentList = <String>[""]; | |
75 /// Whether the next call to [indent] should just be a no-op. | |
76 bool _skipNextIndent = false; | |
77 | |
78 static final identifierCharacterRegExp = new RegExp(r'^[a-zA-Z_0-9$]'); | |
79 static final expressionContinuationRegExp = new RegExp(r'^[-+([]'); | |
80 | |
81 Printer(JavaScriptPrintingOptions options, | |
82 JavaScriptPrintingContext context, | |
83 {LocalNamer localNamer}) | |
84 : options = options, | |
85 context = context, | |
86 shouldCompressOutput = options.shouldCompressOutput, | |
87 danglingElseVisitor = new DanglingElseVisitor(context), | |
88 localNamer = determineRenamer(localNamer, options); | |
89 | |
90 static LocalNamer determineRenamer(LocalNamer localNamer, | |
91 JavaScriptPrintingOptions options) { | |
92 if (localNamer != null) return localNamer; | |
93 return (options.shouldCompressOutput && options.minifyLocalVariables) | |
94 ? new MinifyRenamer() : new IdentityNamer(); | |
95 } | |
96 | |
97 | |
98 // The current indentation string. | |
99 String get indentation { | |
100 // Lazily add new indentation strings as required. | |
101 while (_indentList.length <= _indentLevel) { | |
102 _indentList.add(_indentList.last + " "); | |
103 } | |
104 return _indentList[_indentLevel]; | |
105 } | |
106 | |
107 void indentMore() { | |
108 _indentLevel++; | |
109 } | |
110 | |
111 void indentLess() { | |
112 _indentLevel--; | |
113 } | |
114 | |
115 | |
116 /// Always emit a newline, even under `enableMinification`. | |
117 void forceLine() { | |
118 out("\n"); | |
119 } | |
120 /// Emits a newline for readability. | |
121 void lineOut() { | |
122 if (!shouldCompressOutput) forceLine(); | |
123 } | |
124 void spaceOut() { | |
125 if (!shouldCompressOutput) out(" "); | |
126 } | |
127 | |
128 String lastAddedString = null; | |
129 int get lastCharCode { | |
130 if (lastAddedString == null) return 0; | |
131 assert(lastAddedString.length != ""); | |
132 return lastAddedString.codeUnitAt(lastAddedString.length - 1); | |
133 } | |
134 | |
135 void out(String str) { | |
136 if (str != "") { | |
137 if (pendingSemicolon) { | |
138 if (!shouldCompressOutput) { | |
139 context.emit(";"); | |
140 } else if (str != "}") { | |
141 // We want to output newline instead of semicolon because it makes | |
142 // the raw stack traces much easier to read and it also makes line- | |
143 // based tools like diff work much better. JavaScript will | |
144 // automatically insert the semicolon at the newline if it means a | |
145 // parsing error is avoided, so we can only do this trick if the | |
146 // next line is not something that can be glued onto a valid | |
147 // expression to make a new valid expression. | |
148 | |
149 // If we're using the new emitter where most pretty printed code | |
150 // is escaped in strings, it is a lot easier to deal with semicolons | |
151 // than newlines because the former doesn't need escaping. | |
152 if (options.preferSemicolonToNewlineInMinifiedOutput || | |
153 expressionContinuationRegExp.hasMatch(str)) { | |
154 context.emit(";"); | |
155 } else { | |
156 context.emit("\n"); | |
157 } | |
158 } | |
159 } | |
160 if (pendingSpace && | |
161 (!shouldCompressOutput || identifierCharacterRegExp.hasMatch(str))) { | |
162 context.emit(" "); | |
163 } | |
164 pendingSpace = false; | |
165 pendingSemicolon = false; | |
166 context.emit(str); | |
167 lastAddedString = str; | |
168 } | |
169 } | |
170 | |
171 void outLn(String str) { | |
172 out(str); | |
173 lineOut(); | |
174 } | |
175 | |
176 void outSemicolonLn() { | |
177 if (shouldCompressOutput) { | |
178 pendingSemicolon = true; | |
179 } else { | |
180 out(";"); | |
181 forceLine(); | |
182 } | |
183 } | |
184 | |
185 void outIndent(String str) { indent(); out(str); } | |
186 void outIndentLn(String str) { indent(); outLn(str); } | |
187 | |
188 void skipNextIndent() { | |
189 _skipNextIndent = true; | |
190 } | |
191 | |
192 void indent() { | |
193 if (_skipNextIndent) { | |
194 _skipNextIndent = false; | |
195 return; | |
196 } | |
197 if (!shouldCompressOutput) { | |
198 out(indentation); | |
199 } | |
200 } | |
201 | |
202 visit(Node node) { | |
203 context.enterNode(node); | |
204 node.accept(this); | |
205 context.exitNode(node); | |
206 } | |
207 | |
208 visitCommaSeparated(List<Node> nodes, int hasRequiredType, | |
209 {bool newInForInit, bool newAtStatementBegin}) { | |
210 for (int i = 0; i < nodes.length; i++) { | |
211 if (i != 0) { | |
212 atStatementBegin = false; | |
213 out(","); | |
214 spaceOut(); | |
215 } | |
216 visitNestedExpression(nodes[i], hasRequiredType, | |
217 newInForInit: newInForInit, | |
218 newAtStatementBegin: newAtStatementBegin); | |
219 } | |
220 } | |
221 | |
222 visitAll(List<Node> nodes) { | |
223 nodes.forEach(visit); | |
224 } | |
225 | |
226 visitProgram(Program program) { | |
227 if (program.scriptTag != null) { | |
228 out('#!${program.scriptTag}\n'); | |
229 } | |
230 visitAll(program.body); | |
231 } | |
232 | |
233 bool blockBody(Node body, {bool needsSeparation, bool needsNewline}) { | |
234 if (body is Block) { | |
235 spaceOut(); | |
236 blockOut(body, false, needsNewline); | |
237 return true; | |
238 } | |
239 if (shouldCompressOutput && needsSeparation) { | |
240 // If [shouldCompressOutput] is false, then the 'lineOut' will insert | |
241 // the separation. | |
242 out(" "); | |
243 } else { | |
244 lineOut(); | |
245 } | |
246 indentMore(); | |
247 visit(body); | |
248 indentLess(); | |
249 return false; | |
250 } | |
251 | |
252 void blockOutWithoutBraces(Node node) { | |
253 if (node is Block && !node.isScope) { | |
254 context.enterNode(node); | |
255 Block block = node; | |
256 block.statements.forEach(blockOutWithoutBraces); | |
257 context.exitNode(node); | |
258 } else { | |
259 visit(node); | |
260 } | |
261 } | |
262 | |
263 void blockOut(Block node, bool shouldIndent, bool needsNewline) { | |
264 if (shouldIndent) indent(); | |
265 context.enterNode(node); | |
266 out("{"); | |
267 lineOut(); | |
268 indentMore(); | |
269 node.statements.forEach(blockOutWithoutBraces); | |
270 indentLess(); | |
271 indent(); | |
272 out("}"); | |
273 context.exitNode(node); | |
274 if (needsNewline) lineOut(); | |
275 } | |
276 | |
277 visitBlock(Block block) { | |
278 blockOut(block, true, true); | |
279 } | |
280 | |
281 visitExpressionStatement(ExpressionStatement expressionStatement) { | |
282 indent(); | |
283 outClosureAnnotation(expressionStatement); | |
284 visitNestedExpression(expressionStatement.expression, EXPRESSION, | |
285 newInForInit: false, newAtStatementBegin: true); | |
286 outSemicolonLn(); | |
287 } | |
288 | |
289 visitEmptyStatement(EmptyStatement nop) { | |
290 outIndentLn(";"); | |
291 } | |
292 | |
293 void ifOut(If node, bool shouldIndent) { | |
294 Node then = node.then; | |
295 Node elsePart = node.otherwise; | |
296 bool hasElse = node.hasElse; | |
297 | |
298 // Handle dangling elses and a work-around for Android 4.0 stock browser. | |
299 // Android 4.0 requires braces for a single do-while in the `then` branch. | |
300 // See issue 10923. | |
301 if (hasElse) { | |
302 bool needsBraces = node.then.accept(danglingElseVisitor) || then is Do; | |
303 if (needsBraces) { | |
304 then = new Block(<Statement>[then]); | |
305 } | |
306 } | |
307 if (shouldIndent) indent(); | |
308 out("if"); | |
309 spaceOut(); | |
310 out("("); | |
311 visitNestedExpression(node.condition, EXPRESSION, | |
312 newInForInit: false, newAtStatementBegin: false); | |
313 out(")"); | |
314 bool thenWasBlock; | |
315 if (options.allowSingleLineIfStatements && !hasElse && then is! Block) { | |
316 thenWasBlock = false; | |
317 spaceOut(); | |
318 skipNextIndent(); | |
319 visit(then); | |
320 } else { | |
321 thenWasBlock = | |
322 blockBody(then, needsSeparation: false, needsNewline: !hasElse); | |
323 } | |
324 if (hasElse) { | |
325 if (thenWasBlock) { | |
326 spaceOut(); | |
327 } else { | |
328 indent(); | |
329 } | |
330 out("else"); | |
331 if (elsePart is If) { | |
332 pendingSpace = true; | |
333 ifOut(elsePart, false); | |
334 } else { | |
335 blockBody(elsePart, needsSeparation: true, needsNewline: true); | |
336 } | |
337 } | |
338 } | |
339 | |
340 visitIf(If node) { | |
341 ifOut(node, true); | |
342 } | |
343 | |
344 visitFor(For loop) { | |
345 outIndent("for"); | |
346 spaceOut(); | |
347 out("("); | |
348 if (loop.init != null) { | |
349 visitNestedExpression(loop.init, EXPRESSION, | |
350 newInForInit: true, newAtStatementBegin: false); | |
351 } | |
352 out(";"); | |
353 if (loop.condition != null) { | |
354 spaceOut(); | |
355 visitNestedExpression(loop.condition, EXPRESSION, | |
356 newInForInit: false, newAtStatementBegin: false); | |
357 } | |
358 out(";"); | |
359 if (loop.update != null) { | |
360 spaceOut(); | |
361 visitNestedExpression(loop.update, EXPRESSION, | |
362 newInForInit: false, newAtStatementBegin: false); | |
363 } | |
364 out(")"); | |
365 blockBody(loop.body, needsSeparation: false, needsNewline: true); | |
366 } | |
367 | |
368 visitForIn(ForIn loop) { | |
369 outIndent("for"); | |
370 spaceOut(); | |
371 out("("); | |
372 visitNestedExpression(loop.leftHandSide, EXPRESSION, | |
373 newInForInit: true, newAtStatementBegin: false); | |
374 out(" in"); | |
375 pendingSpace = true; | |
376 visitNestedExpression(loop.object, EXPRESSION, | |
377 newInForInit: false, newAtStatementBegin: false); | |
378 out(")"); | |
379 blockBody(loop.body, needsSeparation: false, needsNewline: true); | |
380 } | |
381 | |
382 visitForOf(ForOf loop) { | |
383 outIndent("for"); | |
384 spaceOut(); | |
385 out("("); | |
386 visitNestedExpression(loop.leftHandSide, EXPRESSION, | |
387 newInForInit: true, newAtStatementBegin: false); | |
388 out(" of"); | |
389 pendingSpace = true; | |
390 visitNestedExpression(loop.iterable, EXPRESSION, | |
391 newInForInit: false, newAtStatementBegin: false); | |
392 out(")"); | |
393 blockBody(loop.body, needsSeparation: false, needsNewline: true); | |
394 } | |
395 | |
396 visitWhile(While loop) { | |
397 outIndent("while"); | |
398 spaceOut(); | |
399 out("("); | |
400 visitNestedExpression(loop.condition, EXPRESSION, | |
401 newInForInit: false, newAtStatementBegin: false); | |
402 out(")"); | |
403 blockBody(loop.body, needsSeparation: false, needsNewline: true); | |
404 } | |
405 | |
406 visitDo(Do loop) { | |
407 outIndent("do"); | |
408 if (blockBody(loop.body, needsSeparation: true, needsNewline: false)) { | |
409 spaceOut(); | |
410 } else { | |
411 indent(); | |
412 } | |
413 out("while"); | |
414 spaceOut(); | |
415 out("("); | |
416 visitNestedExpression(loop.condition, EXPRESSION, | |
417 newInForInit: false, newAtStatementBegin: false); | |
418 out(")"); | |
419 outSemicolonLn(); | |
420 } | |
421 | |
422 visitContinue(Continue node) { | |
423 if (node.targetLabel == null) { | |
424 outIndent("continue"); | |
425 } else { | |
426 outIndent("continue ${node.targetLabel}"); | |
427 } | |
428 outSemicolonLn(); | |
429 } | |
430 | |
431 visitBreak(Break node) { | |
432 if (node.targetLabel == null) { | |
433 outIndent("break"); | |
434 } else { | |
435 outIndent("break ${node.targetLabel}"); | |
436 } | |
437 outSemicolonLn(); | |
438 } | |
439 | |
440 visitReturn(Return node) { | |
441 if (node.value == null) { | |
442 outIndent("return"); | |
443 } else { | |
444 outIndent("return"); | |
445 pendingSpace = true; | |
446 visitNestedExpression(node.value, EXPRESSION, | |
447 newInForInit: false, newAtStatementBegin: false); | |
448 } | |
449 outSemicolonLn(); | |
450 } | |
451 | |
452 visitDartYield(DartYield node) { | |
453 if (node.hasStar) { | |
454 outIndent("yield*"); | |
455 } else { | |
456 outIndent("yield"); | |
457 } | |
458 pendingSpace = true; | |
459 visitNestedExpression(node.expression, EXPRESSION, | |
460 newInForInit: false, newAtStatementBegin: false); | |
461 outSemicolonLn(); | |
462 } | |
463 | |
464 | |
465 visitThrow(Throw node) { | |
466 outIndent("throw"); | |
467 pendingSpace = true; | |
468 visitNestedExpression(node.expression, EXPRESSION, | |
469 newInForInit: false, newAtStatementBegin: false); | |
470 outSemicolonLn(); | |
471 } | |
472 | |
473 visitTry(Try node) { | |
474 outIndent("try"); | |
475 blockBody(node.body, needsSeparation: true, needsNewline: false); | |
476 if (node.catchPart != null) { | |
477 visit(node.catchPart); | |
478 } | |
479 if (node.finallyPart != null) { | |
480 spaceOut(); | |
481 out("finally"); | |
482 blockBody(node.finallyPart, needsSeparation: true, needsNewline: true); | |
483 } else { | |
484 lineOut(); | |
485 } | |
486 } | |
487 | |
488 visitCatch(Catch node) { | |
489 spaceOut(); | |
490 out("catch"); | |
491 spaceOut(); | |
492 out("("); | |
493 visitNestedExpression(node.declaration, EXPRESSION, | |
494 newInForInit: false, newAtStatementBegin: false); | |
495 out(")"); | |
496 blockBody(node.body, needsSeparation: false, needsNewline: true); | |
497 } | |
498 | |
499 visitSwitch(Switch node) { | |
500 outIndent("switch"); | |
501 spaceOut(); | |
502 out("("); | |
503 visitNestedExpression(node.key, EXPRESSION, | |
504 newInForInit: false, newAtStatementBegin: false); | |
505 out(")"); | |
506 spaceOut(); | |
507 outLn("{"); | |
508 indentMore(); | |
509 visitAll(node.cases); | |
510 indentLess(); | |
511 outIndentLn("}"); | |
512 } | |
513 | |
514 visitCase(Case node) { | |
515 outIndent("case"); | |
516 pendingSpace = true; | |
517 visitNestedExpression(node.expression, EXPRESSION, | |
518 newInForInit: false, newAtStatementBegin: false); | |
519 outLn(":"); | |
520 if (!node.body.statements.isEmpty) { | |
521 blockOut(node.body, true, true); | |
522 } | |
523 } | |
524 | |
525 visitDefault(Default node) { | |
526 outIndentLn("default:"); | |
527 if (!node.body.statements.isEmpty) { | |
528 blockOut(node.body, true, true); | |
529 } | |
530 } | |
531 | |
532 visitLabeledStatement(LabeledStatement node) { | |
533 outIndent("${node.label}:"); | |
534 blockBody(node.body, needsSeparation: false, needsNewline: true); | |
535 } | |
536 | |
537 void functionOut(Fun fun, Node name) { | |
538 out("function"); | |
539 if (fun.isGenerator) out("*"); | |
540 if (name != null) { | |
541 out(" "); | |
542 // Name must be a [Decl]. Therefore only test for primary expressions. | |
543 visitNestedExpression(name, PRIMARY, | |
544 newInForInit: false, newAtStatementBegin: false); | |
545 } | |
546 localNamer.enterScope(fun); | |
547 outTypeParams(fun.typeParams); | |
548 out("("); | |
549 if (fun.params != null) { | |
550 visitCommaSeparated(fun.params, PRIMARY, | |
551 newInForInit: false, newAtStatementBegin: false); | |
552 } | |
553 out(")"); | |
554 outTypeAnnotation(fun.returnType); | |
555 switch (fun.asyncModifier) { | |
556 case const AsyncModifier.sync(): | |
557 break; | |
558 case const AsyncModifier.async(): | |
559 out(' async'); | |
560 break; | |
561 case const AsyncModifier.syncStar(): | |
562 out(' sync*'); | |
563 break; | |
564 case const AsyncModifier.asyncStar(): | |
565 out(' async*'); | |
566 break; | |
567 } | |
568 blockBody(fun.body, needsSeparation: false, needsNewline: false); | |
569 localNamer.leaveScope(); | |
570 } | |
571 | |
572 visitFunctionDeclaration(FunctionDeclaration declaration) { | |
573 indent(); | |
574 outClosureAnnotation(declaration); | |
575 functionOut(declaration.function, declaration.name); | |
576 lineOut(); | |
577 } | |
578 | |
579 visitNestedExpression(Expression node, int requiredPrecedence, | |
580 {bool newInForInit, bool newAtStatementBegin}) { | |
581 int nodePrecedence = node.precedenceLevel; | |
582 bool needsParentheses = | |
583 // a - (b + c). | |
584 (requiredPrecedence != EXPRESSION && | |
585 nodePrecedence < requiredPrecedence) || | |
586 // for (a = (x in o); ... ; ... ) { ... } | |
587 (newInForInit && node is Binary && node.op == "in") || | |
588 // (function() { ... })(). | |
589 // ({a: 2, b: 3}.toString()). | |
590 (newAtStatementBegin && (node is NamedFunction || | |
591 node is FunctionExpression || | |
592 node is ObjectInitializer)); | |
593 if (needsParentheses) { | |
594 inForInit = false; | |
595 atStatementBegin = false; | |
596 inNewTarget = false; | |
597 out("("); | |
598 visit(node); | |
599 out(")"); | |
600 } else { | |
601 inForInit = newInForInit; | |
602 atStatementBegin = newAtStatementBegin; | |
603 visit(node); | |
604 } | |
605 } | |
606 | |
607 visitVariableDeclarationList(VariableDeclarationList list) { | |
608 outClosureAnnotation(list); | |
609 // Note: keyword can be null for non-static field declarations. | |
610 if (list.keyword != null) { | |
611 out(list.keyword); | |
612 out(" "); | |
613 } | |
614 visitCommaSeparated(list.declarations, ASSIGNMENT, | |
615 newInForInit: inForInit, newAtStatementBegin: false); | |
616 } | |
617 | |
618 visitArrayBindingPattern(ArrayBindingPattern node) { | |
619 out("["); | |
620 visitCommaSeparated(node.variables, EXPRESSION, | |
621 newInForInit: false, newAtStatementBegin: false); | |
622 out("]"); | |
623 } | |
624 visitObjectBindingPattern(ObjectBindingPattern node) { | |
625 out("{"); | |
626 visitCommaSeparated(node.variables, EXPRESSION, | |
627 newInForInit: false, newAtStatementBegin: false); | |
628 out("}"); | |
629 } | |
630 | |
631 visitDestructuredVariable(DestructuredVariable node) { | |
632 var name = node.name; | |
633 var hasName = name != null; | |
634 if (hasName) { | |
635 if (name is LiteralString) { | |
636 out("["); | |
637 out(name.value); | |
638 out("]"); | |
639 } else { | |
640 visit(name); | |
641 } | |
642 } | |
643 if (node.structure != null) { | |
644 if (hasName) { | |
645 out(":"); | |
646 spaceOut(); | |
647 } | |
648 visit(node.structure); | |
649 } | |
650 outTypeAnnotation(node.type); | |
651 if (node.defaultValue != null) { | |
652 spaceOut(); | |
653 out("="); | |
654 spaceOut(); | |
655 visitNestedExpression(node.defaultValue, EXPRESSION, | |
656 newInForInit: false, newAtStatementBegin: false); | |
657 } | |
658 } | |
659 | |
660 visitSimpleBindingPattern(SimpleBindingPattern node) { | |
661 visit(node.name); | |
662 } | |
663 | |
664 visitAssignment(Assignment assignment) { | |
665 visitNestedExpression(assignment.leftHandSide, LEFT_HAND_SIDE, | |
666 newInForInit: inForInit, | |
667 newAtStatementBegin: atStatementBegin); | |
668 if (assignment.value != null) { | |
669 spaceOut(); | |
670 String op = assignment.op; | |
671 if (op != null) out(op); | |
672 out("="); | |
673 spaceOut(); | |
674 visitNestedExpression(assignment.value, ASSIGNMENT, | |
675 newInForInit: inForInit, | |
676 newAtStatementBegin: false); | |
677 } | |
678 } | |
679 | |
680 visitVariableInitialization(VariableInitialization initialization) { | |
681 outClosureAnnotation(initialization); | |
682 visitAssignment(initialization); | |
683 } | |
684 | |
685 visitConditional(Conditional cond) { | |
686 visitNestedExpression(cond.condition, LOGICAL_OR, | |
687 newInForInit: inForInit, | |
688 newAtStatementBegin: atStatementBegin); | |
689 spaceOut(); | |
690 out("?"); | |
691 spaceOut(); | |
692 // The then part is allowed to have an 'in'. | |
693 visitNestedExpression(cond.then, ASSIGNMENT, | |
694 newInForInit: false, newAtStatementBegin: false); | |
695 spaceOut(); | |
696 out(":"); | |
697 spaceOut(); | |
698 visitNestedExpression(cond.otherwise, ASSIGNMENT, | |
699 newInForInit: inForInit, newAtStatementBegin: false); | |
700 } | |
701 | |
702 visitNew(New node) { | |
703 out("new "); | |
704 inNewTarget = true; | |
705 visitNestedExpression(node.target, ACCESS, | |
706 newInForInit: inForInit, newAtStatementBegin: false); | |
707 inNewTarget = false; | |
708 out("("); | |
709 visitCommaSeparated(node.arguments, SPREAD, | |
710 newInForInit: false, newAtStatementBegin: false); | |
711 out(")"); | |
712 } | |
713 | |
714 visitCall(Call call) { | |
715 visitNestedExpression(call.target, LEFT_HAND_SIDE, | |
716 newInForInit: inForInit, | |
717 newAtStatementBegin: atStatementBegin); | |
718 out("("); | |
719 visitCommaSeparated(call.arguments, SPREAD, | |
720 newInForInit: false, newAtStatementBegin: false); | |
721 out(")"); | |
722 } | |
723 | |
724 visitBinary(Binary binary) { | |
725 Expression left = binary.left; | |
726 Expression right = binary.right; | |
727 String op = binary.op; | |
728 int leftPrecedenceRequirement; | |
729 int rightPrecedenceRequirement; | |
730 bool leftSpace = true; // left<HERE>op right | |
731 switch (op) { | |
732 case ',': | |
733 // x, (y, z) <=> (x, y), z. | |
734 leftPrecedenceRequirement = EXPRESSION; | |
735 rightPrecedenceRequirement = EXPRESSION; | |
736 leftSpace = false; | |
737 break; | |
738 case "||": | |
739 leftPrecedenceRequirement = LOGICAL_OR; | |
740 // x || (y || z) <=> (x || y) || z. | |
741 rightPrecedenceRequirement = LOGICAL_OR; | |
742 break; | |
743 case "&&": | |
744 leftPrecedenceRequirement = LOGICAL_AND; | |
745 // x && (y && z) <=> (x && y) && z. | |
746 rightPrecedenceRequirement = LOGICAL_AND; | |
747 break; | |
748 case "|": | |
749 leftPrecedenceRequirement = BIT_OR; | |
750 // x | (y | z) <=> (x | y) | z. | |
751 rightPrecedenceRequirement = BIT_OR; | |
752 break; | |
753 case "^": | |
754 leftPrecedenceRequirement = BIT_XOR; | |
755 // x ^ (y ^ z) <=> (x ^ y) ^ z. | |
756 rightPrecedenceRequirement = BIT_XOR; | |
757 break; | |
758 case "&": | |
759 leftPrecedenceRequirement = BIT_AND; | |
760 // x & (y & z) <=> (x & y) & z. | |
761 rightPrecedenceRequirement = BIT_AND; | |
762 break; | |
763 case "==": | |
764 case "!=": | |
765 case "===": | |
766 case "!==": | |
767 leftPrecedenceRequirement = EQUALITY; | |
768 rightPrecedenceRequirement = RELATIONAL; | |
769 break; | |
770 case "<": | |
771 case ">": | |
772 case "<=": | |
773 case ">=": | |
774 case "instanceof": | |
775 case "in": | |
776 leftPrecedenceRequirement = RELATIONAL; | |
777 rightPrecedenceRequirement = SHIFT; | |
778 break; | |
779 case ">>": | |
780 case "<<": | |
781 case ">>>": | |
782 leftPrecedenceRequirement = SHIFT; | |
783 rightPrecedenceRequirement = ADDITIVE; | |
784 break; | |
785 case "+": | |
786 case "-": | |
787 leftPrecedenceRequirement = ADDITIVE; | |
788 // We cannot remove parenthesis for "+" because | |
789 // x + (y + z) <!=> (x + y) + z: | |
790 // Example: | |
791 // "a" + (1 + 2) => "a3"; | |
792 // ("a" + 1) + 2 => "a12"; | |
793 rightPrecedenceRequirement = MULTIPLICATIVE; | |
794 break; | |
795 case "*": | |
796 case "/": | |
797 case "%": | |
798 leftPrecedenceRequirement = MULTIPLICATIVE; | |
799 // We cannot remove parenthesis for "*" because of precision issues. | |
800 rightPrecedenceRequirement = UNARY; | |
801 break; | |
802 default: | |
803 context.error("Forgot operator: $op"); | |
804 } | |
805 | |
806 visitNestedExpression(left, leftPrecedenceRequirement, | |
807 newInForInit: inForInit, | |
808 newAtStatementBegin: atStatementBegin); | |
809 | |
810 if (op == "in" || op == "instanceof") { | |
811 // There are cases where the space is not required but without further | |
812 // analysis we cannot know. | |
813 out(" "); | |
814 out(op); | |
815 out(" "); | |
816 } else { | |
817 if (leftSpace) spaceOut(); | |
818 out(op); | |
819 spaceOut(); | |
820 } | |
821 visitNestedExpression(right, rightPrecedenceRequirement, | |
822 newInForInit: inForInit, | |
823 newAtStatementBegin: false); | |
824 } | |
825 | |
826 visitPrefix(Prefix unary) { | |
827 String op = unary.op; | |
828 switch (op) { | |
829 case "delete": | |
830 case "void": | |
831 case "typeof": | |
832 // There are cases where the space is not required but without further | |
833 // analysis we cannot know. | |
834 out(op); | |
835 out(" "); | |
836 break; | |
837 case "+": | |
838 case "++": | |
839 if (lastCharCode == charCodes.$PLUS) out(" "); | |
840 out(op); | |
841 break; | |
842 case "-": | |
843 case "--": | |
844 if (lastCharCode == charCodes.$MINUS) out(" "); | |
845 out(op); | |
846 break; | |
847 default: | |
848 out(op); | |
849 } | |
850 visitNestedExpression(unary.argument, unary.precedenceLevel, | |
851 newInForInit: inForInit, newAtStatementBegin: false); | |
852 } | |
853 | |
854 visitSpread(Spread unary) => visitPrefix(unary); | |
855 | |
856 visitYield(Yield yield) { | |
857 out(yield.star ? "yield*" : "yield"); | |
858 if (yield.value == null) return; | |
859 out(" "); | |
860 visitNestedExpression(yield.value, yield.precedenceLevel, | |
861 newInForInit: inForInit, newAtStatementBegin: false); | |
862 } | |
863 | |
864 visitPostfix(Postfix postfix) { | |
865 visitNestedExpression(postfix.argument, LEFT_HAND_SIDE, | |
866 newInForInit: inForInit, | |
867 newAtStatementBegin: atStatementBegin); | |
868 out(postfix.op); | |
869 } | |
870 | |
871 visitThis(This node) { | |
872 out("this"); | |
873 } | |
874 | |
875 visitSuper(Super node) { | |
876 out("super"); | |
877 } | |
878 | |
879 visitIdentifier(Identifier node) { | |
880 out(localNamer.getName(node)); | |
881 outTypeAnnotation(node.type); | |
882 } | |
883 | |
884 visitRestParameter(RestParameter node) { | |
885 out('...'); | |
886 visitIdentifier(node.parameter); | |
887 } | |
888 | |
889 bool isDigit(int charCode) { | |
890 return charCodes.$0 <= charCode && charCode <= charCodes.$9; | |
891 } | |
892 | |
893 bool isValidJavaScriptId(String field) { | |
894 if (field.length < 3) return false; | |
895 // Ignore the leading and trailing string-delimiter. | |
896 for (int i = 1; i < field.length - 1; i++) { | |
897 // TODO(floitsch): allow more characters. | |
898 int charCode = field.codeUnitAt(i); | |
899 if (!(charCodes.$a <= charCode && charCode <= charCodes.$z || | |
900 charCodes.$A <= charCode && charCode <= charCodes.$Z || | |
901 charCode == charCodes.$$ || | |
902 charCode == charCodes.$_ || | |
903 i != 1 && isDigit(charCode))) { | |
904 return false; | |
905 } | |
906 } | |
907 | |
908 // TODO(floitsch): normally we should also check that the field is not a | |
909 // reserved word. We don't generate fields with reserved word names except | |
910 // for 'super'. | |
911 return options.allowKeywordsInProperties || field != '"super"'; | |
912 } | |
913 | |
914 visitAccess(PropertyAccess access) { | |
915 // Normally we can omit parens on the receiver if it is a Call, even though | |
916 // Call expressions have lower precedence. However this optimization doesn't | |
917 // work inside New expressions: | |
918 // | |
919 // new obj.foo().bar() | |
920 // | |
921 // This will be parsed as: | |
922 // | |
923 // (new obj.foo()).bar() | |
924 // | |
925 // Which is incorrect. So we must have parenthesis in this case: | |
926 // | |
927 // new (obj.foo()).bar() | |
928 // | |
929 int precedence = inNewTarget ? ACCESS : CALL; | |
930 | |
931 visitNestedExpression(access.receiver, precedence, | |
932 newInForInit: inForInit, | |
933 newAtStatementBegin: atStatementBegin); | |
934 propertyNameOut(access.selector, inAccess: true); | |
935 } | |
936 | |
937 visitNamedFunction(NamedFunction namedFunction) { | |
938 functionOut(namedFunction.function, namedFunction.name); | |
939 } | |
940 | |
941 visitFun(Fun fun) { | |
942 functionOut(fun, null); | |
943 } | |
944 | |
945 visitArrowFun(ArrowFun fun) { | |
946 localNamer.enterScope(fun); | |
947 if (fun.params.length == 1 && | |
948 (fun.params.single.type == null || !options.emitTypes)) { | |
949 visitNestedExpression(fun.params.single, SPREAD, | |
950 newInForInit: false, newAtStatementBegin: false); | |
951 } else { | |
952 out("("); | |
953 visitCommaSeparated(fun.params, SPREAD, | |
954 newInForInit: false, newAtStatementBegin: false); | |
955 out(")"); | |
956 } | |
957 outTypeAnnotation(fun.returnType); | |
958 spaceOut(); | |
959 out("=>"); | |
960 if (fun.body is Expression) { | |
961 spaceOut(); | |
962 // Object initializers require parenthesis to disambiguate | |
963 // AssignmentExpression from FunctionBody. See: | |
964 // https://people.mozilla.org/~jorendorff/es6-draft.html#sec-arrow-functio
n-definitions | |
965 var needsParen = fun.body is ObjectInitializer; | |
966 if (needsParen) out("("); | |
967 visitNestedExpression(fun.body, ASSIGNMENT, | |
968 newInForInit: false, newAtStatementBegin: false); | |
969 if (needsParen) out(")"); | |
970 } else { | |
971 blockBody(fun.body, needsSeparation: false, needsNewline: false); | |
972 } | |
973 localNamer.leaveScope(); | |
974 } | |
975 | |
976 visitLiteralBool(LiteralBool node) { | |
977 out(node.value ? "true" : "false"); | |
978 } | |
979 | |
980 visitLiteralString(LiteralString node) { | |
981 out(node.value); | |
982 } | |
983 | |
984 visitLiteralNumber(LiteralNumber node) { | |
985 int charCode = node.value.codeUnitAt(0); | |
986 if (charCode == charCodes.$MINUS && lastCharCode == charCodes.$MINUS) { | |
987 out(" "); | |
988 } | |
989 out(node.value); | |
990 } | |
991 | |
992 visitLiteralNull(LiteralNull node) { | |
993 out("null"); | |
994 } | |
995 | |
996 visitArrayInitializer(ArrayInitializer node) { | |
997 out("["); | |
998 indentMore(); | |
999 var multiline = node.multiline; | |
1000 List<Expression> elements = node.elements; | |
1001 for (int i = 0; i < elements.length; i++) { | |
1002 Expression element = elements[i]; | |
1003 if (element is ArrayHole) { | |
1004 // Note that array holes must have a trailing "," even if they are | |
1005 // in last position. Otherwise `[,]` (having length 1) would become | |
1006 // equal to `[]` (the empty array) | |
1007 // and [1,,] (array with 1 and a hole) would become [1,] = [1]. | |
1008 out(","); | |
1009 continue; | |
1010 } | |
1011 if (i != 0 && !multiline) spaceOut(); | |
1012 if (multiline) { | |
1013 forceLine(); | |
1014 indent(); | |
1015 } | |
1016 visitNestedExpression(element, ASSIGNMENT, | |
1017 newInForInit: false, newAtStatementBegin: false); | |
1018 // We can skip the trailing "," for the last element (since it's not | |
1019 // an array hole). | |
1020 if (i != elements.length - 1) out(","); | |
1021 } | |
1022 indentLess(); | |
1023 if (multiline) { | |
1024 lineOut(); | |
1025 indent(); | |
1026 } | |
1027 out("]"); | |
1028 } | |
1029 | |
1030 visitArrayHole(ArrayHole node) { | |
1031 throw "Unreachable"; | |
1032 } | |
1033 | |
1034 visitObjectInitializer(ObjectInitializer node) { | |
1035 List<Property> properties = node.properties; | |
1036 out("{"); | |
1037 indentMore(); | |
1038 | |
1039 var multiline = node.multiline; | |
1040 for (int i = 0; i < properties.length; i++) { | |
1041 if (i != 0) { | |
1042 out(","); | |
1043 if (!multiline) spaceOut(); | |
1044 } | |
1045 if (multiline) { | |
1046 forceLine(); | |
1047 indent(); | |
1048 } | |
1049 visit(properties[i]); | |
1050 } | |
1051 indentLess(); | |
1052 if (multiline) { | |
1053 lineOut(); | |
1054 indent(); | |
1055 } | |
1056 out("}"); | |
1057 } | |
1058 | |
1059 visitProperty(Property node) { | |
1060 propertyNameOut(node.name); | |
1061 out(":"); | |
1062 spaceOut(); | |
1063 visitNestedExpression(node.value, ASSIGNMENT, | |
1064 newInForInit: false, newAtStatementBegin: false); | |
1065 } | |
1066 | |
1067 visitRegExpLiteral(RegExpLiteral node) { | |
1068 out(node.pattern); | |
1069 } | |
1070 | |
1071 visitTemplateString(TemplateString node) { | |
1072 out('`'); | |
1073 for (var element in node.elements) { | |
1074 if (element is String) { | |
1075 out(element); | |
1076 } else { | |
1077 out(r'${'); | |
1078 visit(element); | |
1079 out('}'); | |
1080 } | |
1081 } | |
1082 out('`'); | |
1083 } | |
1084 | |
1085 visitTaggedTemplate(TaggedTemplate node) { | |
1086 visit(node.tag); | |
1087 visit(node.template); | |
1088 } | |
1089 | |
1090 visitClassDeclaration(ClassDeclaration node) { | |
1091 indent(); | |
1092 visit(node.classExpr); | |
1093 lineOut(); | |
1094 } | |
1095 | |
1096 void outTypeParams(Iterable<Identifier> typeParams) { | |
1097 if (typeParams != null && options.emitTypes && typeParams.isNotEmpty) { | |
1098 out("<"); | |
1099 var first = true; | |
1100 for (var typeParam in typeParams) { | |
1101 if (!first) out(", "); | |
1102 first = false; | |
1103 visit(typeParam); | |
1104 } | |
1105 out(">"); | |
1106 } | |
1107 } | |
1108 | |
1109 visitClassExpression(ClassExpression node) { | |
1110 out('class '); | |
1111 visit(node.name); | |
1112 outTypeParams(node.typeParams); | |
1113 if (node.heritage != null) { | |
1114 out(' extends '); | |
1115 visit(node.heritage); | |
1116 } | |
1117 spaceOut(); | |
1118 if (node.methods.isNotEmpty) { | |
1119 out('{'); | |
1120 lineOut(); | |
1121 indentMore(); | |
1122 if (options.emitTypes && node.fields != null) { | |
1123 for (var field in node.fields) { | |
1124 indent(); | |
1125 visit(field); | |
1126 out(";"); | |
1127 lineOut(); | |
1128 } | |
1129 } | |
1130 for (var method in node.methods) { | |
1131 indent(); | |
1132 visit(method); | |
1133 lineOut(); | |
1134 } | |
1135 indentLess(); | |
1136 indent(); | |
1137 out('}'); | |
1138 } else { | |
1139 out('{}'); | |
1140 } | |
1141 } | |
1142 | |
1143 visitMethod(Method node) { | |
1144 outClosureAnnotation(node); | |
1145 if (node.isStatic) { | |
1146 out('static '); | |
1147 } | |
1148 if (node.isGetter) { | |
1149 out('get '); | |
1150 } else if (node.isSetter) { | |
1151 out('set '); | |
1152 } else if (node.function.isGenerator) { | |
1153 out('*'); | |
1154 } | |
1155 propertyNameOut(node.name, inMethod: true); | |
1156 | |
1157 var fun = node.function; | |
1158 localNamer.enterScope(fun); | |
1159 out("("); | |
1160 if (fun.params != null) { | |
1161 visitCommaSeparated(fun.params, SPREAD, | |
1162 newInForInit: false, newAtStatementBegin: false); | |
1163 } | |
1164 out(")"); | |
1165 // TODO(jmesserly): async modifiers | |
1166 if (fun.body.statements.isEmpty) { | |
1167 spaceOut(); | |
1168 out("{}"); | |
1169 } else { | |
1170 blockBody(fun.body, needsSeparation: false, needsNewline: false); | |
1171 } | |
1172 localNamer.leaveScope(); | |
1173 } | |
1174 | |
1175 void outClosureAnnotation(Node node) { | |
1176 if (node != null && node.closureAnnotation != null) { | |
1177 String comment = node.closureAnnotation.toString(indentation); | |
1178 if (comment.isNotEmpty) { | |
1179 out(comment); | |
1180 lineOut(); | |
1181 indent(); | |
1182 } | |
1183 } | |
1184 } | |
1185 | |
1186 void propertyNameOut(Expression node, {bool inMethod: false, | |
1187 bool inAccess: false}) { | |
1188 | |
1189 if (node is LiteralNumber) { | |
1190 LiteralNumber nameNumber = node; | |
1191 if (inAccess) out('['); | |
1192 out(nameNumber.value); | |
1193 if (inAccess) out(']'); | |
1194 } else { | |
1195 if (node is LiteralString) { | |
1196 if (isValidJavaScriptId(node.value)) { | |
1197 if (inAccess) out('.'); | |
1198 out(node.valueWithoutQuotes); | |
1199 } else { | |
1200 if (inMethod || inAccess) out("["); | |
1201 out(node.value); | |
1202 if (inMethod || inAccess) out("]"); | |
1203 } | |
1204 } else { | |
1205 // ComputedPropertyName | |
1206 out("["); | |
1207 visitNestedExpression(node, EXPRESSION, | |
1208 newInForInit: false, newAtStatementBegin: false); | |
1209 out("]"); | |
1210 } | |
1211 } | |
1212 } | |
1213 | |
1214 visitImportDeclaration(ImportDeclaration node) { | |
1215 indent(); | |
1216 out('import '); | |
1217 if (node.defaultBinding != null) { | |
1218 visit(node.defaultBinding); | |
1219 if (node.namedImports != null) { | |
1220 out(','); | |
1221 spaceOut(); | |
1222 } | |
1223 } | |
1224 nameSpecifierListOut(node.namedImports); | |
1225 fromClauseOut(node.from); | |
1226 outSemicolonLn(); | |
1227 } | |
1228 | |
1229 visitExportDeclaration(ExportDeclaration node) { | |
1230 indent(); | |
1231 out('export '); | |
1232 if (node.isDefault) out('default '); | |
1233 // TODO(jmesserly): we need to avoid indent/newline if this is a statement. | |
1234 visit(node.exported); | |
1235 outSemicolonLn(); | |
1236 } | |
1237 | |
1238 visitExportClause(ExportClause node) { | |
1239 nameSpecifierListOut(node.exports); | |
1240 fromClauseOut(node.from); | |
1241 } | |
1242 | |
1243 nameSpecifierListOut(List<NameSpecifier> names) { | |
1244 if (names == null) return; | |
1245 | |
1246 if (names.length == 1 && names[0].name == '*') { | |
1247 visit(names[0]); | |
1248 return; | |
1249 } | |
1250 | |
1251 out('{'); | |
1252 spaceOut(); | |
1253 for (int i = 0; i < names.length; i++) { | |
1254 if (i != 0) { | |
1255 out(','); | |
1256 spaceOut(); | |
1257 } | |
1258 visit(names[i]); | |
1259 } | |
1260 spaceOut(); | |
1261 out('}'); | |
1262 } | |
1263 | |
1264 fromClauseOut(LiteralString from) { | |
1265 if (from != null) { | |
1266 out(' from'); | |
1267 spaceOut(); | |
1268 visit(from); | |
1269 } | |
1270 } | |
1271 | |
1272 visitNameSpecifier(NameSpecifier node) { | |
1273 out(node.name); | |
1274 if (node.asName != null) { | |
1275 out(' as '); | |
1276 out(node.asName); | |
1277 } | |
1278 } | |
1279 | |
1280 visitModule(Module node) { | |
1281 visitAll(node.body); | |
1282 } | |
1283 | |
1284 visitLiteralExpression(LiteralExpression node) { | |
1285 String template = node.template; | |
1286 List<Expression> inputs = node.inputs; | |
1287 | |
1288 List<String> parts = template.split('#'); | |
1289 int inputsLength = inputs == null ? 0 : inputs.length; | |
1290 if (parts.length != inputsLength + 1) { | |
1291 context.error('Wrong number of arguments for JS: $template'); | |
1292 } | |
1293 // Code that uses JS must take care of operator precedences, and | |
1294 // put parenthesis if needed. | |
1295 out(parts[0]); | |
1296 for (int i = 0; i < inputsLength; i++) { | |
1297 visit(inputs[i]); | |
1298 out(parts[i + 1]); | |
1299 } | |
1300 } | |
1301 | |
1302 visitLiteralStatement(LiteralStatement node) { | |
1303 outLn(node.code); | |
1304 } | |
1305 | |
1306 visitInterpolatedNode(InterpolatedNode node) { | |
1307 out('#${node.nameOrPosition}'); | |
1308 } | |
1309 | |
1310 visitInterpolatedExpression(InterpolatedExpression node) => | |
1311 visitInterpolatedNode(node); | |
1312 | |
1313 visitInterpolatedLiteral(InterpolatedLiteral node) => | |
1314 visitInterpolatedNode(node); | |
1315 | |
1316 visitInterpolatedParameter(InterpolatedParameter node) => | |
1317 visitInterpolatedNode(node); | |
1318 | |
1319 visitInterpolatedSelector(InterpolatedSelector node) => | |
1320 visitInterpolatedNode(node); | |
1321 | |
1322 visitInterpolatedMethod(InterpolatedMethod node) => | |
1323 visitInterpolatedNode(node); | |
1324 | |
1325 visitInterpolatedIdentifier(InterpolatedIdentifier node) => | |
1326 visitInterpolatedNode(node); | |
1327 | |
1328 visitInterpolatedStatement(InterpolatedStatement node) { | |
1329 outLn('#${node.nameOrPosition}'); | |
1330 } | |
1331 | |
1332 void visitComment(Comment node) { | |
1333 if (shouldCompressOutput) return; | |
1334 String comment = node.comment.trim(); | |
1335 if (comment.isEmpty) return; | |
1336 for (var line in comment.split('\n')) { | |
1337 if (comment.startsWith('//')) { | |
1338 outIndentLn(line.trim()); | |
1339 } else { | |
1340 outIndentLn('// ${line.trim()}'); | |
1341 } | |
1342 } | |
1343 } | |
1344 | |
1345 void visitCommentExpression(CommentExpression node) { | |
1346 if (shouldCompressOutput) return; | |
1347 String comment = node.comment.trim(); | |
1348 if (comment.isEmpty) return; | |
1349 if (comment.startsWith('/*')) { | |
1350 out(comment); | |
1351 } else { | |
1352 out('/* $comment */'); | |
1353 } | |
1354 visit(node.expression); | |
1355 } | |
1356 | |
1357 void visitAwait(Await node) { | |
1358 out("await "); | |
1359 visit(node.expression); | |
1360 } | |
1361 | |
1362 void outTypeAnnotation(TypeRef node) { | |
1363 if (node == null || !options.emitTypes || node.isUnknown) return; | |
1364 | |
1365 if (node is OptionalTypeRef) { | |
1366 out("?: "); | |
1367 visit(node.type); | |
1368 } else { | |
1369 out(": "); | |
1370 visit(node); | |
1371 } | |
1372 } | |
1373 } | |
1374 | |
1375 // Collects all the var declarations in the function. We need to do this in a | |
1376 // separate pass because JS vars are lifted to the top of the function. | |
1377 class VarCollector extends BaseVisitor { | |
1378 bool nested; | |
1379 final Set<String> vars; | |
1380 final Set<String> params; | |
1381 | |
1382 VarCollector() : nested = false, | |
1383 vars = new Set<String>(), | |
1384 params = new Set<String>(); | |
1385 | |
1386 void forEachVar(void fn(String v)) => vars.forEach(fn); | |
1387 void forEachParam(void fn(String p)) => params.forEach(fn); | |
1388 | |
1389 void collectVarsInFunction(FunctionExpression fun) { | |
1390 if (!nested) { | |
1391 nested = true; | |
1392 if (fun.params != null) { | |
1393 for (var param in fun.params) { | |
1394 params.add(param.name); | |
1395 } | |
1396 } | |
1397 fun.body.accept(this); | |
1398 nested = false; | |
1399 } | |
1400 } | |
1401 | |
1402 void visitFunctionDeclaration(FunctionDeclaration declaration) { | |
1403 // Note that we don't bother collecting the name of the function. | |
1404 collectVarsInFunction(declaration.function); | |
1405 } | |
1406 | |
1407 void visitNamedFunction(NamedFunction namedFunction) { | |
1408 // Note that we don't bother collecting the name of the function. | |
1409 collectVarsInFunction(namedFunction.function); | |
1410 } | |
1411 | |
1412 void visitMethod(Method declaration) { | |
1413 collectVarsInFunction(declaration.function); | |
1414 } | |
1415 | |
1416 void visitFun(Fun fun) { | |
1417 collectVarsInFunction(fun); | |
1418 } | |
1419 | |
1420 void visitArrowFun(ArrowFun fun) { | |
1421 collectVarsInFunction(fun); | |
1422 } | |
1423 | |
1424 void visitClassExpression(ClassExpression node) { | |
1425 // Note that we don't bother collecting the name of the class. | |
1426 if (node.heritage != null) node.heritage.accept(this); | |
1427 for (Method method in node.methods) method.accept(this); | |
1428 } | |
1429 | |
1430 void visitCatch(Catch node) { | |
1431 declareVariable(node.declaration); | |
1432 node.body.accept(this); | |
1433 } | |
1434 | |
1435 void visitVariableInitialization(VariableInitialization node) { | |
1436 declareVariable(node.declaration); | |
1437 if (node.value != null) node.value.accept(this); | |
1438 } | |
1439 | |
1440 void declareVariable(Identifier decl) { | |
1441 if (decl.allowRename) vars.add(decl.name); | |
1442 } | |
1443 } | |
1444 | |
1445 | |
1446 /** | |
1447 * Returns true, if the given node must be wrapped into braces when used | |
1448 * as then-statement in an [If] that has an else branch. | |
1449 */ | |
1450 class DanglingElseVisitor extends BaseVisitor<bool> { | |
1451 JavaScriptPrintingContext context; | |
1452 | |
1453 DanglingElseVisitor(this.context); | |
1454 | |
1455 bool visitProgram(Program node) => false; | |
1456 | |
1457 bool visitNode(Node node) { | |
1458 context.error("Forgot node: $node"); | |
1459 return null; | |
1460 } | |
1461 | |
1462 bool visitBlock(Block node) => false; | |
1463 bool visitExpressionStatement(ExpressionStatement node) => false; | |
1464 bool visitEmptyStatement(EmptyStatement node) => false; | |
1465 bool visitIf(If node) { | |
1466 if (!node.hasElse) return true; | |
1467 return node.otherwise.accept(this); | |
1468 } | |
1469 bool visitFor(For node) => node.body.accept(this); | |
1470 bool visitForIn(ForIn node) => node.body.accept(this); | |
1471 bool visitForOf(ForOf node) => node.body.accept(this); | |
1472 bool visitWhile(While node) => node.body.accept(this); | |
1473 bool visitDo(Do node) => false; | |
1474 bool visitContinue(Continue node) => false; | |
1475 bool visitBreak(Break node) => false; | |
1476 bool visitReturn(Return node) => false; | |
1477 bool visitThrow(Throw node) => false; | |
1478 bool visitTry(Try node) { | |
1479 if (node.finallyPart != null) { | |
1480 return node.finallyPart.accept(this); | |
1481 } else { | |
1482 return node.catchPart.accept(this); | |
1483 } | |
1484 } | |
1485 bool visitCatch(Catch node) => node.body.accept(this); | |
1486 bool visitSwitch(Switch node) => false; | |
1487 bool visitCase(Case node) => false; | |
1488 bool visitDefault(Default node) => false; | |
1489 bool visitFunctionDeclaration(FunctionDeclaration node) => false; | |
1490 bool visitLabeledStatement(LabeledStatement node) | |
1491 => node.body.accept(this); | |
1492 bool visitLiteralStatement(LiteralStatement node) => true; | |
1493 bool visitClassDeclaration(ClassDeclaration node) => false; | |
1494 | |
1495 bool visitExpression(Expression node) => false; | |
1496 } | |
1497 | |
1498 | |
1499 abstract class LocalNamer { | |
1500 String getName(Identifier node); | |
1501 void enterScope(FunctionExpression node); | |
1502 void leaveScope(); | |
1503 } | |
1504 | |
1505 | |
1506 class IdentityNamer implements LocalNamer { | |
1507 String getName(Identifier node) => node.name; | |
1508 void enterScope(FunctionExpression node) {} | |
1509 void leaveScope() {} | |
1510 } | |
1511 | |
1512 | |
1513 class MinifyRenamer implements LocalNamer { | |
1514 final List<Map<String, String>> maps = []; | |
1515 final List<int> parameterNumberStack = []; | |
1516 final List<int> variableNumberStack = []; | |
1517 int parameterNumber = 0; | |
1518 int variableNumber = 0; | |
1519 | |
1520 void enterScope(FunctionExpression node) { | |
1521 var vars = new VarCollector(); | |
1522 node.accept(vars); | |
1523 maps.add(new Map<String, String>()); | |
1524 variableNumberStack.add(variableNumber); | |
1525 parameterNumberStack.add(parameterNumber); | |
1526 vars.forEachVar(declareVariable); | |
1527 vars.forEachParam(declareParameter); | |
1528 } | |
1529 | |
1530 void leaveScope() { | |
1531 maps.removeLast(); | |
1532 variableNumber = variableNumberStack.removeLast(); | |
1533 parameterNumber = parameterNumberStack.removeLast(); | |
1534 } | |
1535 | |
1536 String getName(Identifier node) { | |
1537 String oldName = node.name; | |
1538 // Go from inner scope to outer looking for mapping of name. | |
1539 for (int i = maps.length - 1; i >= 0; i--) { | |
1540 var map = maps[i]; | |
1541 var replacement = map[oldName]; | |
1542 if (replacement != null) return replacement; | |
1543 } | |
1544 return oldName; | |
1545 } | |
1546 | |
1547 static const LOWER_CASE_LETTERS = 26; | |
1548 static const LETTERS = LOWER_CASE_LETTERS; | |
1549 static const DIGITS = 10; | |
1550 | |
1551 static int nthLetter(int n) { | |
1552 return (n < LOWER_CASE_LETTERS) ? | |
1553 charCodes.$a + n : | |
1554 charCodes.$A + n - LOWER_CASE_LETTERS; | |
1555 } | |
1556 | |
1557 // Parameters go from a to z and variables go from z to a. This makes each | |
1558 // argument list and each top-of-function var declaration look similar and | |
1559 // helps gzip compress the file. If we have more than 26 arguments and | |
1560 // variables then we meet somewhere in the middle of the alphabet. After | |
1561 // that we give up trying to be nice to the compression algorithm and just | |
1562 // use the same namespace for arguments and variables, starting with A, and | |
1563 // moving on to a0, a1, etc. | |
1564 String declareVariable(String oldName) { | |
1565 if (avoidRenaming(oldName)) return oldName; | |
1566 var newName; | |
1567 if (variableNumber + parameterNumber < LOWER_CASE_LETTERS) { | |
1568 // Variables start from z and go backwards, for better gzipability. | |
1569 newName = getNameNumber(oldName, LOWER_CASE_LETTERS - 1 - variableNumber); | |
1570 } else { | |
1571 // After 26 variables and parameters we allocate them in the same order. | |
1572 newName = getNameNumber(oldName, variableNumber + parameterNumber); | |
1573 } | |
1574 variableNumber++; | |
1575 return newName; | |
1576 } | |
1577 | |
1578 String declareParameter(String oldName) { | |
1579 if (avoidRenaming(oldName)) return oldName; | |
1580 var newName; | |
1581 if (variableNumber + parameterNumber < LOWER_CASE_LETTERS) { | |
1582 newName = getNameNumber(oldName, parameterNumber); | |
1583 } else { | |
1584 newName = getNameNumber(oldName, variableNumber + parameterNumber); | |
1585 } | |
1586 parameterNumber++; | |
1587 return newName; | |
1588 } | |
1589 | |
1590 bool avoidRenaming(String oldName) { | |
1591 // Variables of this $form$ are used in pattern matching the message of JS | |
1592 // exceptions, so should not be renamed. | |
1593 // TODO(sra): Introduce a way for indicating in the JS text which variables | |
1594 // should not be renamed. | |
1595 return oldName.startsWith(r'$') && oldName.endsWith(r'$'); | |
1596 } | |
1597 | |
1598 String getNameNumber(String oldName, int n) { | |
1599 if (maps.isEmpty) return oldName; | |
1600 | |
1601 String newName; | |
1602 if (n < LETTERS) { | |
1603 // Start naming variables a, b, c, ..., z, A, B, C, ..., Z. | |
1604 newName = new String.fromCharCodes([nthLetter(n)]); | |
1605 } else { | |
1606 // Then name variables a0, a1, a2, ..., a9, b0, b1, ..., Z9, aa0, aa1, ... | |
1607 // For all functions with fewer than 500 locals this is just as compact | |
1608 // as using aa, ab, etc. but avoids clashes with keywords. | |
1609 n -= LETTERS; | |
1610 int digit = n % DIGITS; | |
1611 n ~/= DIGITS; | |
1612 int alphaChars = 1; | |
1613 int nameSpaceSize = LETTERS; | |
1614 // Find out whether we should use the 1-character namespace (size 52), the | |
1615 // 2-character namespace (size 52*52), etc. | |
1616 while (n >= nameSpaceSize) { | |
1617 n -= nameSpaceSize; | |
1618 alphaChars++; | |
1619 nameSpaceSize *= LETTERS; | |
1620 } | |
1621 var codes = <int>[]; | |
1622 for (var i = 0; i < alphaChars; i++) { | |
1623 nameSpaceSize ~/= LETTERS; | |
1624 codes.add(nthLetter((n ~/ nameSpaceSize) % LETTERS)); | |
1625 } | |
1626 codes.add(charCodes.$0 + digit); | |
1627 newName = new String.fromCharCodes(codes); | |
1628 } | |
1629 assert(new RegExp(r'[a-zA-Z][a-zA-Z0-9]*').hasMatch(newName)); | |
1630 maps.last[oldName] = newName; | |
1631 return newName; | |
1632 } | |
1633 } | |
1634 | |
1635 /// Like [BaseVisitor], but calls [declare] for [Identifier] declarations, and | |
1636 /// [visitIdentifier] otherwise. | |
1637 abstract class VariableDeclarationVisitor<T> extends BaseVisitor<T> { | |
1638 declare(Identifier node); | |
1639 | |
1640 visitFunctionExpression(FunctionExpression node) { | |
1641 node.params.forEach(_scanVariableBinding); | |
1642 node.body.accept(this); | |
1643 } | |
1644 | |
1645 _scanVariableBinding(VariableBinding d) { | |
1646 if (d is Identifier) declare(d); | |
1647 else d.accept(this); | |
1648 } | |
1649 | |
1650 visitRestParameter(RestParameter node) { | |
1651 _scanVariableBinding(node.parameter); | |
1652 super.visitRestParameter(node); | |
1653 } | |
1654 | |
1655 visitDestructuredVariable(DestructuredVariable node) { | |
1656 var name = node.name; | |
1657 if (name is Identifier) _scanVariableBinding(name); | |
1658 super.visitDestructuredVariable(node); | |
1659 } | |
1660 | |
1661 visitSimpleBindingPattern(SimpleBindingPattern node) { | |
1662 _scanVariableBinding(node.name); | |
1663 super.visitSimpleBindingPattern(node); | |
1664 } | |
1665 | |
1666 visitVariableInitialization(VariableInitialization node) { | |
1667 _scanVariableBinding(node.declaration); | |
1668 if (node.value != null) node.value.accept(this); | |
1669 } | |
1670 | |
1671 visitCatch(Catch node) { | |
1672 declare(node.declaration); | |
1673 node.body.accept(this); | |
1674 } | |
1675 | |
1676 visitFunctionDeclaration(FunctionDeclaration node) { | |
1677 declare(node.name); | |
1678 node.function.accept(this); | |
1679 } | |
1680 | |
1681 visitNamedFunction(NamedFunction node) { | |
1682 declare(node.name); | |
1683 node.function.accept(this); | |
1684 } | |
1685 | |
1686 visitClassExpression(ClassExpression node) { | |
1687 declare(node.name); | |
1688 if (node.heritage != null) node.heritage.accept(this); | |
1689 for (Method element in node.methods) element.accept(this); | |
1690 } | |
1691 } | |
OLD | NEW |