UNPKG

42.4 kBJavaScriptView Raw
1"use strict";
2
3const some = require("lodash.some");
4
5const _require = require("babel-helper-mark-eval-scopes"),
6 markEvalScopes = _require.markEvalScopes,
7 hasEval = _require.hasEval;
8
9const removeUseStrict = require("./remove-use-strict");
10
11const evaluate = require("babel-helper-evaluate-path");
12
13function evaluateTruthy(path) {
14 const res = evaluate(path);
15 if (res.confident) return !!res.value;
16}
17
18function prevSiblings(path) {
19 const parentPath = path.parentPath;
20 const siblings = [];
21 let key = parentPath.key;
22
23 while ((path = parentPath.getSibling(--key)).type) {
24 siblings.push(path);
25 }
26
27 return siblings;
28}
29
30function forEachAncestor(path, callback) {
31 while (path = path.parentPath) {
32 callback(path);
33 }
34}
35
36module.exports = ({
37 types: t,
38 traverse
39}) => {
40 const removeOrVoid = require("babel-helper-remove-or-void")(t);
41
42 const shouldRevisit = Symbol("shouldRevisit"); // this is used for tracking fn params that can be removed
43 // as traversal takes place from left and
44 // unused params can be removed only on the right
45
46 const markForRemoval = Symbol("markForRemoval");
47 const main = {
48 // remove side effectless statement
49 ExpressionStatement(path) {
50 if (path.get("expression").isPure()) {
51 removeOrVoid(path);
52 }
53 },
54
55 Function: {
56 // Let's take all the vars in a function that are not in the top level scope and hoist them
57 // with the first var declaration in the top-level scope. This transform in itself may
58 // not yield much returns (or even can be marginally harmful to size). However it's great
59 // for taking away statements from blocks that can be only expressions which the `simplify`
60 // plugin can turn into other things (e.g. if => conditional).
61 exit(path) {
62 // This hurts gzip size.
63 if (!this.optimizeRawSize) {
64 return;
65 }
66
67 const node = path.node,
68 scope = path.scope;
69 const seen = new Set();
70 const declars = [];
71 const mutations = [];
72
73 for (const name in scope.bindings) {
74 const binding = scope.bindings[name];
75
76 if (!binding.path.isVariableDeclarator()) {
77 continue;
78 }
79
80 const declarPath = binding.path.parentPath;
81
82 if (seen.has(declarPath)) {
83 continue;
84 }
85
86 seen.add(declarPath);
87
88 if (declarPath.parentPath.isForInStatement()) {
89 continue;
90 }
91
92 if (declarPath.parentPath.parentPath.isFunction()) {
93 continue;
94 }
95
96 if (!declarPath.node || !declarPath.node.declarations) {
97 continue;
98 }
99
100 const assignmentSequence = [];
101 var _iteratorNormalCompletion = true;
102 var _didIteratorError = false;
103 var _iteratorError = undefined;
104
105 try {
106 for (var _iterator = declarPath.node.declarations[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
107 const declar = _step.value;
108 declars.push(declar);
109
110 if (declar.init) {
111 assignmentSequence.push(t.assignmentExpression("=", declar.id, declar.init));
112 mutations.push(() => {
113 declar.init = null;
114 });
115 }
116 }
117 } catch (err) {
118 _didIteratorError = true;
119 _iteratorError = err;
120 } finally {
121 try {
122 if (!_iteratorNormalCompletion && _iterator.return != null) {
123 _iterator.return();
124 }
125 } finally {
126 if (_didIteratorError) {
127 throw _iteratorError;
128 }
129 }
130 }
131
132 if (assignmentSequence.length) {
133 mutations.push(() => declarPath.replaceWith(t.sequenceExpression(assignmentSequence)));
134 } else {
135 mutations.push(() => removeOrVoid(declarPath));
136 }
137 }
138
139 if (declars.length) {
140 mutations.forEach(f => f());
141 var _iteratorNormalCompletion2 = true;
142 var _didIteratorError2 = false;
143 var _iteratorError2 = undefined;
144
145 try {
146 for (var _iterator2 = node.body.body[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
147 const statement = _step2.value;
148
149 if (t.isVariableDeclaration(statement)) {
150 statement.declarations.push(...declars);
151 return;
152 }
153 }
154 } catch (err) {
155 _didIteratorError2 = true;
156 _iteratorError2 = err;
157 } finally {
158 try {
159 if (!_iteratorNormalCompletion2 && _iterator2.return != null) {
160 _iterator2.return();
161 }
162 } finally {
163 if (_didIteratorError2) {
164 throw _iteratorError2;
165 }
166 }
167 }
168
169 const varDecl = t.variableDeclaration("var", declars);
170 node.body.body.unshift(varDecl);
171 }
172 }
173
174 },
175 // Remove bindings with no references.
176 Scope: {
177 exit(path) {
178 if (path.node[shouldRevisit]) {
179 delete path.node[shouldRevisit];
180 path.visit();
181 }
182 },
183
184 enter(path) {
185 if (path.isProgram()) {
186 return;
187 }
188
189 if (hasEval(path.scope)) {
190 return;
191 }
192
193 const scope = path.scope; // if the scope is created by a function, we obtain its
194 // parameter list
195
196 const canRemoveParams = path.isFunction() && path.node.kind !== "set";
197 const paramsList = canRemoveParams ? path.get("params") : [];
198
199 for (let i = paramsList.length - 1; i >= 0; i--) {
200 const param = paramsList[i];
201
202 if (param.isIdentifier()) {
203 const binding = scope.bindings[param.node.name];
204 if (!binding) continue;
205
206 if (binding.referenced) {
207 // when the first binding is referenced (right to left)
208 // exit without marking anything after this
209 break;
210 }
211
212 binding[markForRemoval] = true;
213 continue;
214 } else if (param.isAssignmentPattern()) {
215 const left = param.get("left");
216 const right = param.get("right");
217
218 if (left.isIdentifier() && right.isPure()) {
219 const binding = scope.bindings[left.node.name];
220
221 if (binding.referenced) {
222 // when the first binding is referenced (right to left)
223 // exit without marking anything after this
224 break;
225 }
226
227 binding[markForRemoval] = true;
228 continue;
229 }
230 } // other patterns - assignment, object have side-effects
231 // and cannot be safely removed
232
233
234 break;
235 }
236
237 for (const name in scope.bindings) {
238 const binding = scope.bindings[name];
239
240 if (!binding.referenced && binding.kind !== "module") {
241 if (binding.kind === "param" && (this.keepFnArgs || !binding[markForRemoval])) {
242 continue;
243 } else if (binding.path.isVariableDeclarator()) {
244 const declaration = binding.path.parentPath;
245 const maybeBlockParent = declaration.parentPath;
246
247 if (maybeBlockParent && maybeBlockParent.isForXStatement({
248 left: declaration.node
249 })) {
250 // Can't remove if in a for-in/for-of/for-await statement `for (var x in wat)`.
251 continue;
252 }
253 } else if (!scope.isPure(binding.path.node)) {
254 // TODO: AssignmentPattern are marked as impure and unused ids aren't removed yet
255 continue;
256 } else if (binding.path.isFunctionExpression() || binding.path.isClassExpression()) {
257 // `bar(function foo() {})` foo is not referenced but it's used.
258 continue;
259 } else if ( // ClassDeclaration has binding in two scopes
260 // 1. The scope in which it is declared
261 // 2. The class's own scope
262 binding.path.isClassDeclaration() && binding.path === scope.path) {
263 continue;
264 }
265
266 const mutations = [];
267 let bail = false; // Make sure none of the assignments value is used
268
269 binding.constantViolations.forEach(p => {
270 if (bail || p === binding.path) {
271 return;
272 }
273
274 if (!p.parentPath.isExpressionStatement()) {
275 bail = true;
276 }
277
278 if (p.isAssignmentExpression() && !p.get("right").isPure()) {
279 mutations.push(() => p.replaceWith(p.get("right")));
280 } else {
281 mutations.push(() => removeOrVoid(p));
282 }
283 });
284
285 if (bail) {
286 continue;
287 }
288
289 if (binding.path.isVariableDeclarator()) {
290 if (!binding.path.get("id").isIdentifier()) {
291 // deopt for object and array pattern
292 continue;
293 } // if declarator has some impure init expression
294 // var x = foo();
295 // => foo();
296
297
298 if (binding.path.node.init && !scope.isPure(binding.path.node.init) && binding.path.parentPath.node.declarations) {
299 // binding path has more than one declarations
300 if (binding.path.parentPath.node.declarations.length !== 1) {
301 continue;
302 }
303
304 binding.path.parentPath.replaceWith(binding.path.node.init);
305 } else {
306 updateReferences(binding.path, this);
307 removeOrVoid(binding.path);
308 }
309 } else {
310 updateReferences(binding.path, this);
311 removeOrVoid(binding.path);
312 }
313
314 mutations.forEach(f => f());
315 scope.removeBinding(name);
316 } else if (binding.constant) {
317 if (binding.path.isFunctionDeclaration() || binding.path.isVariableDeclarator() && binding.path.get("init").isFunction()) {
318 const fun = binding.path.isFunctionDeclaration() ? binding.path : binding.path.get("init");
319 let allInside = true;
320 var _iteratorNormalCompletion3 = true;
321 var _didIteratorError3 = false;
322 var _iteratorError3 = undefined;
323
324 try {
325 for (var _iterator3 = binding.referencePaths[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {
326 const ref = _step3.value;
327
328 if (!ref.find(p => p.node === fun.node)) {
329 allInside = false;
330 break;
331 }
332 }
333 } catch (err) {
334 _didIteratorError3 = true;
335 _iteratorError3 = err;
336 } finally {
337 try {
338 if (!_iteratorNormalCompletion3 && _iterator3.return != null) {
339 _iterator3.return();
340 }
341 } finally {
342 if (_didIteratorError3) {
343 throw _iteratorError3;
344 }
345 }
346 }
347
348 if (allInside) {
349 scope.removeBinding(name);
350 updateReferences(binding.path, this);
351 removeOrVoid(binding.path);
352 continue;
353 }
354 }
355
356 if (binding.references === 1 && binding.kind !== "param" && binding.kind !== "module" && binding.constant) {
357 let replacement = binding.path.node;
358 let replacementPath = binding.path;
359 let isReferencedBefore = false;
360 const refPath = binding.referencePaths[0];
361
362 if (t.isVariableDeclarator(replacement)) {
363 const _prevSiblings = prevSiblings(replacementPath); // traverse ancestors of a reference checking if it's before declaration
364
365
366 forEachAncestor(refPath, ancestor => {
367 if (_prevSiblings.indexOf(ancestor) > -1) {
368 isReferencedBefore = true;
369 }
370 }); // deopt if reference is in different scope than binding
371 // since we don't know if it's sync or async execution
372 // (i.e. whether value has been assigned to a reference or not)
373
374 if (isReferencedBefore && refPath.scope !== binding.scope) {
375 continue;
376 } // simulate hoisting by replacing value
377 // with undefined if declaration is after reference
378
379
380 replacement = isReferencedBefore ? t.unaryExpression("void", t.numericLiteral(0), true) : replacement.init; // Bail out for ArrayPattern and ObjectPattern
381 // TODO: maybe a more intelligent approach instead of simply bailing out
382
383 if (!replacementPath.get("id").isIdentifier()) {
384 continue;
385 }
386
387 replacementPath = replacementPath.get("init");
388 }
389
390 if (!replacement) {
391 continue;
392 }
393
394 if (!scope.isPure(replacement, true) && !isReferencedBefore) {
395 continue;
396 }
397
398 let bail = false;
399
400 if (replacementPath.isIdentifier()) {
401 const binding = scope.getBinding(replacement.name); // the reference should be in the same scope
402 // and the replacement should be a constant - this is to
403 // ensure that the duplication of replacement is not affected
404 // https://github.com/babel/minify/issues/685
405
406 bail = !(binding && refPath.scope.getBinding(replacement.name) === binding && binding.constantViolations.length === 0);
407 } else {
408 replacementPath.traverse({
409 Function(path) {
410 path.skip();
411 },
412
413 ReferencedIdentifier({
414 node
415 }) {
416 if (bail) {
417 return;
418 }
419
420 const binding = scope.getBinding(node.name);
421
422 if (binding && refPath.scope.getBinding(node.name) === binding) {
423 bail = binding.constantViolations.length > 0;
424 }
425 }
426
427 });
428 }
429
430 if (bail) {
431 continue;
432 }
433
434 let parent = binding.path.parent;
435
436 if (t.isVariableDeclaration(parent)) {
437 parent = binding.path.parentPath.parent;
438 } // 1. Make sure we share the parent with the node. In other words it's lexically defined
439 // and not in an if statement or otherwise.
440 // 2. If the replacement is an object then we have to make sure we are not in a loop or a function
441 // because otherwise we'll be inlining and doing a lot more allocation than we have to
442 // which would also could affect correctness in that they are not the same reference.
443
444
445 let mayLoop = false;
446 const sharesRoot = refPath.find(({
447 node
448 }) => {
449 if (!mayLoop) {
450 mayLoop = t.isWhileStatement(node) || t.isFor(node) || t.isFunction(node);
451 }
452
453 return node === parent;
454 }); // Anything that inherits from Object.
455
456 const isObj = n => t.isFunction(n) || t.isObjectExpression(n) || t.isArrayExpression(n);
457
458 const isReplacementObj = isObj(replacement) || some(replacement, isObj);
459
460 if (!sharesRoot || isReplacementObj && mayLoop) {
461 continue;
462 } // check if it's safe to replace
463 // To solve https://github.com/babel/minify/issues/691
464 // Here we bail for property checks using the "in" operator
465 // This is because - `in` is a side-effect-free operation but the property
466 // could be deleted between the replacementPath and referencePath
467 // It is expensive to compute the delete operation and we bail for
468 // all the binary "in" operations
469
470
471 let inExpression = replacementPath.isBinaryExpression({
472 operator: "in"
473 });
474
475 if (!inExpression) {
476 replacementPath.traverse({
477 Function(path) {
478 path.skip();
479 },
480
481 BinaryExpression(path) {
482 if (path.node.operator === "in") {
483 inExpression = true;
484 path.stop();
485 }
486 }
487
488 });
489 }
490
491 if (inExpression) {
492 continue;
493 }
494
495 const replaced = replace(binding.referencePaths[0], {
496 binding,
497 scope,
498 replacement,
499 replacementPath
500 });
501
502 if (replaced) {
503 scope.removeBinding(name);
504
505 if (binding.path.node) {
506 removeOrVoid(binding.path);
507 }
508 }
509 }
510 }
511 } // end-for-of
512
513 }
514
515 },
516
517 // Remove unreachable code.
518 BlockStatement(path) {
519 const paths = path.get("body");
520 let purge = false;
521
522 for (let i = 0; i < paths.length; i++) {
523 const p = paths[i];
524
525 if (!purge && p.isCompletionStatement()) {
526 purge = true;
527 continue;
528 }
529
530 if (purge && !canExistAfterCompletion(p)) {
531 removeOrVoid(p);
532 }
533 }
534 },
535
536 // Double check unreachable code and remove return statements that
537 // have no semantic meaning
538 ReturnStatement(path) {
539 const node = path.node;
540
541 if (!path.inList) {
542 return;
543 } // Not last in its block? (See BlockStatement visitor)
544
545
546 if (path.container.length - 1 !== path.key && !canExistAfterCompletion(path.getSibling(path.key + 1)) && path.parentPath.isBlockStatement()) {
547 // This is probably a new oppurtinity by some other transform
548 // let's call the block visitor on this again before proceeding.
549 path.parentPath.pushContext(path.context);
550 path.parentPath.visit();
551 path.parentPath.popContext();
552 return;
553 }
554
555 if (node.argument) {
556 return;
557 }
558
559 let noNext = true;
560 let parentPath = path.parentPath;
561
562 while (parentPath && !parentPath.isFunction() && noNext) {
563 // https://github.com/babel/minify/issues/265
564 if (hasLoopParent(parentPath)) {
565 noNext = false;
566 break;
567 }
568
569 const nextPath = parentPath.getSibling(parentPath.key + 1);
570
571 if (nextPath.node) {
572 if (nextPath.isReturnStatement()) {
573 nextPath.pushContext(path.context);
574 nextPath.visit();
575 nextPath.popContext();
576
577 if (parentPath.getSibling(parentPath.key + 1).node) {
578 noNext = false;
579 break;
580 }
581 } else {
582 noNext = false;
583 break;
584 }
585 }
586
587 parentPath = parentPath.parentPath;
588 }
589
590 if (noNext) {
591 removeOrVoid(path);
592 }
593 },
594
595 ConditionalExpression(path) {
596 const node = path.node;
597 const evaluateTest = evaluateTruthy(path.get("test"));
598
599 if (evaluateTest === true) {
600 path.replaceWith(node.consequent);
601 } else if (evaluateTest === false) {
602 path.replaceWith(node.alternate);
603 }
604 },
605
606 SwitchStatement: {
607 exit(path) {
608 const evaluated = evaluate(path.get("discriminant"), {
609 tdz: this.tdz
610 });
611 if (!evaluated.confident) return;
612 const discriminant = evaluated.value;
613 const cases = path.get("cases");
614 let matchingCaseIndex = -1;
615 let defaultCaseIndex = -1;
616
617 for (let i = 0; i < cases.length; i++) {
618 const test = cases[i].get("test"); // handle default case
619
620 if (test.node === null) {
621 defaultCaseIndex = i;
622 continue;
623 }
624
625 const testResult = evaluate(test, {
626 tdz: this.tdz
627 }); // if we are not able to deternine a test during
628 // compile time, we terminate immediately
629
630 if (!testResult.confident) return;
631
632 if (testResult.value === discriminant) {
633 matchingCaseIndex = i;
634 break;
635 }
636 }
637
638 let result;
639
640 if (matchingCaseIndex === -1) {
641 if (defaultCaseIndex === -1) {
642 path.skip();
643 path.replaceWithMultiple(extractVars(path));
644 return;
645 } else {
646 result = getStatementsUntilBreak(defaultCaseIndex);
647 }
648 } else {
649 result = getStatementsUntilBreak(matchingCaseIndex);
650 }
651
652 if (result.bail) return; // we extract vars from the entire switch statement
653 // and there will be duplicates which
654 // will be again removed by DCE
655
656 replaceSwitch([...extractVars(path), ...result.statements]);
657
658 function getStatementsUntilBreak(start) {
659 const result = {
660 bail: false,
661 statements: []
662 };
663
664 for (let i = start; i < cases.length; i++) {
665 const consequent = cases[i].get("consequent");
666
667 for (let j = 0; j < consequent.length; j++) {
668 const _isBreaking = isBreaking(consequent[j], path);
669
670 if (_isBreaking.bail) {
671 result.bail = true;
672 return result;
673 }
674
675 if (_isBreaking.break) {
676 // compute no more
677 // exit out of the loop
678 return result;
679 } else {
680 result.statements.push(consequent[j].node);
681 }
682 }
683 }
684
685 return result;
686 }
687
688 function replaceSwitch(statements) {
689 let isBlockRequired = false;
690
691 for (let i = 0; i < statements.length; i++) {
692 if (t.isVariableDeclaration(statements[i], {
693 kind: "let"
694 })) {
695 isBlockRequired = true;
696 break;
697 }
698
699 if (t.isVariableDeclaration(statements[i], {
700 kind: "const"
701 })) {
702 isBlockRequired = true;
703 break;
704 }
705 }
706
707 if (isBlockRequired) {
708 path.replaceWith(t.BlockStatement(statements));
709 } else {
710 path.replaceWithMultiple(statements);
711 }
712 }
713 }
714
715 },
716
717 WhileStatement(path) {
718 const test = path.get("test");
719 const result = evaluate(test, {
720 tdz: this.tdz
721 });
722
723 if (result.confident && test.isPure() && !result.value) {
724 path.remove();
725 }
726 },
727
728 ForStatement(path) {
729 const test = path.get("test");
730 if (!test.isPure()) return;
731 const result = evaluate(test, {
732 tdz: this.tdz
733 });
734
735 if (result.confident) {
736 if (result.value) {
737 test.remove();
738 } else {
739 const init = path.get("init");
740
741 if (init.node && !init.isPure()) {
742 path.replaceWith(init);
743 } else {
744 path.remove();
745 }
746 }
747 }
748 },
749
750 DoWhileStatement(path) {
751 const test = path.get("test");
752 const result = evaluate(test, {
753 tdz: this.tdz
754 });
755
756 if (result.confident && test.isPure() && !result.value) {
757 const body = path.get("body");
758
759 if (body.isBlockStatement()) {
760 const stmts = body.get("body");
761 var _iteratorNormalCompletion4 = true;
762 var _didIteratorError4 = false;
763 var _iteratorError4 = undefined;
764
765 try {
766 for (var _iterator4 = stmts[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) {
767 const stmt = _step4.value;
768
769 const _isBreaking = isBreaking(stmt, path);
770
771 if (_isBreaking.bail || _isBreaking.break) return;
772
773 const _isContinuing = isContinuing(stmt, path);
774
775 if (_isContinuing.bail || isContinuing.continue) return;
776 }
777 } catch (err) {
778 _didIteratorError4 = true;
779 _iteratorError4 = err;
780 } finally {
781 try {
782 if (!_iteratorNormalCompletion4 && _iterator4.return != null) {
783 _iterator4.return();
784 }
785 } finally {
786 if (_didIteratorError4) {
787 throw _iteratorError4;
788 }
789 }
790 }
791
792 path.replaceWith(body.node);
793 } else if (body.isBreakStatement()) {
794 const _isBreaking = isBreaking(body, path);
795
796 if (_isBreaking.bail) return;
797 if (_isBreaking.break) path.remove();
798 } else if (body.isContinueStatement()) {
799 return;
800 } else {
801 path.replaceWith(body.node);
802 }
803 }
804 },
805
806 // Join assignment and definition when in sequence.
807 // var x; x = 1; -> var x = 1;
808 AssignmentExpression(path) {
809 if (!path.get("left").isIdentifier() || !path.parentPath.isExpressionStatement()) {
810 return;
811 }
812
813 const prev = path.parentPath.getSibling(path.parentPath.key - 1);
814
815 if (!(prev && prev.isVariableDeclaration())) {
816 return;
817 }
818
819 const declars = prev.node.declarations;
820
821 if (declars.length !== 1 || declars[0].init || declars[0].id.name !== path.get("left").node.name) {
822 return;
823 }
824
825 declars[0].init = path.node.right;
826 removeOrVoid(path);
827 },
828
829 // Remove named function expression name. While this is dangerous as it changes
830 // `function.name` all minifiers do it and hence became a standard.
831 FunctionExpression(path) {
832 if (!this.keepFnName) {
833 removeUnreferencedId(path);
834 }
835 },
836
837 // remove class names
838 ClassExpression(path) {
839 if (!this.keepClassName) {
840 removeUnreferencedId(path);
841 }
842 },
843
844 // Put the `var` in the left if feasible.
845 ForInStatement(path) {
846 const left = path.get("left");
847
848 if (!left.isIdentifier()) {
849 return;
850 }
851
852 const binding = path.scope.getBinding(left.node.name);
853
854 if (!binding) {
855 return;
856 }
857
858 if (binding.scope.getFunctionParent() !== path.scope.getFunctionParent()) {
859 return;
860 }
861
862 if (!binding.path.isVariableDeclarator()) {
863 return;
864 }
865
866 if (binding.path.parentPath.parentPath.isForInStatement({
867 left: binding.path.parent
868 })) {
869 return;
870 } // If it has company then it's probably more efficient to keep.
871
872
873 if (binding.path.parent.declarations.length > 1) {
874 return;
875 } // meh
876
877
878 if (binding.path.node.init) {
879 return;
880 }
881
882 removeOrVoid(binding.path);
883 path.node.left = t.variableDeclaration("var", [t.variableDeclarator(left.node)]);
884 binding.path = path.get("left").get("declarations")[0];
885 }
886
887 };
888 return {
889 name: "minify-dead-code-elimination",
890 visitor: {
891 Function: {
892 exit(path) {
893 /**
894 * Use exit handler to traverse in a dfs post-order fashion
895 * to remove use strict
896 */
897 const body = path.get("body");
898
899 if (body.isBlockStatement()) {
900 removeUseStrict(body);
901 }
902 }
903
904 },
905 IfStatement: {
906 exit(path, {
907 opts: {
908 tdz = false
909 } = {}
910 }) {
911 const consequent = path.get("consequent");
912 const alternate = path.get("alternate");
913 const test = path.get("test");
914 const evalResult = evaluate(test, {
915 tdz
916 });
917 const isPure = test.isPure();
918 const replacements = [];
919
920 if (evalResult.confident && !isPure && test.isSequenceExpression()) {
921 replacements.push(t.expressionStatement(extractSequenceImpure(test)));
922 } // we can check if a test will be truthy 100% and if so then we can inline
923 // the consequent and completely ignore the alternate
924 //
925 // if (true) { foo; } -> { foo; }
926 // if ("foo") { foo; } -> { foo; }
927 //
928
929
930 if (evalResult.confident && evalResult.value) {
931 path.replaceWithMultiple([...replacements, ...toStatements(consequent), ...extractVars(alternate)]);
932 return;
933 } // we can check if a test will be falsy 100% and if so we can inline the
934 // alternate if there is one and completely remove the consequent
935 //
936 // if ("") { bar; } else { foo; } -> { foo; }
937 // if ("") { bar; } ->
938 //
939
940
941 if (evalResult.confident && !evalResult.value) {
942 if (alternate.node) {
943 path.replaceWithMultiple([...replacements, ...toStatements(alternate), ...extractVars(consequent)]);
944 return;
945 } else {
946 path.replaceWithMultiple([...replacements, ...extractVars(consequent)]);
947 }
948 } // remove alternate blocks that are empty
949 //
950 // if (foo) { foo; } else {} -> if (foo) { foo; }
951 //
952
953
954 if (alternate.isBlockStatement() && !alternate.node.body.length) {
955 alternate.remove(); // For if-statements babel-traverse replaces with an empty block
956
957 path.node.alternate = null;
958 } // if the consequent block is empty turn alternate blocks into a consequent
959 // and flip the test
960 //
961 // if (foo) {} else { bar; } -> if (!foo) { bar; }
962 //
963
964
965 if (consequent.isBlockStatement() && !consequent.node.body.length && alternate.isBlockStatement() && alternate.node.body.length) {
966 consequent.replaceWith(alternate.node);
967 alternate.remove(); // For if-statements babel-traverse replaces with an empty block
968
969 path.node.alternate = null;
970 test.replaceWith(t.unaryExpression("!", test.node, true));
971 }
972 }
973
974 },
975
976 EmptyStatement(path) {
977 if (path.parentPath.isBlockStatement() || path.parentPath.isProgram()) {
978 path.remove();
979 }
980 },
981
982 Program: {
983 exit(path, {
984 opts: {
985 // set defaults
986 optimizeRawSize = false,
987 keepFnName = false,
988 keepClassName = false,
989 keepFnArgs = false,
990 tdz = false
991 } = {}
992 } = {}) {
993 (traverse.clearCache || traverse.cache.clear)();
994 path.scope.crawl();
995 markEvalScopes(path); // We need to run this plugin in isolation.
996
997 path.traverse(main, {
998 functionToBindings: new Map(),
999 optimizeRawSize,
1000 keepFnName,
1001 keepClassName,
1002 keepFnArgs,
1003 tdz
1004 });
1005 }
1006
1007 }
1008 }
1009 };
1010
1011 function toStatements(path) {
1012 const node = path.node;
1013
1014 if (path.isBlockStatement()) {
1015 let hasBlockScoped = false;
1016
1017 for (let i = 0; i < node.body.length; i++) {
1018 const bodyNode = node.body[i];
1019
1020 if (t.isBlockScoped(bodyNode)) {
1021 hasBlockScoped = true;
1022 }
1023 }
1024
1025 if (!hasBlockScoped) {
1026 return node.body;
1027 }
1028 }
1029
1030 return [node];
1031 } // Extracts vars from a path
1032 // Useful for removing blocks or paths that can contain
1033 // variable declarations inside them
1034 // Note:
1035 // drops are inits
1036 // extractVars({ var x = 5, y = x }) => var x, y;
1037
1038
1039 function extractVars(path) {
1040 const declarators = [];
1041
1042 if (path.isVariableDeclaration({
1043 kind: "var"
1044 })) {
1045 var _iteratorNormalCompletion5 = true;
1046 var _didIteratorError5 = false;
1047 var _iteratorError5 = undefined;
1048
1049 try {
1050 for (var _iterator5 = path.node.declarations[Symbol.iterator](), _step5; !(_iteratorNormalCompletion5 = (_step5 = _iterator5.next()).done); _iteratorNormalCompletion5 = true) {
1051 const decl = _step5.value;
1052 const bindingIds = Object.keys(t.getBindingIdentifiers(decl.id));
1053 declarators.push(...bindingIds.map(name => t.variableDeclarator(t.identifier(name))));
1054 }
1055 } catch (err) {
1056 _didIteratorError5 = true;
1057 _iteratorError5 = err;
1058 } finally {
1059 try {
1060 if (!_iteratorNormalCompletion5 && _iterator5.return != null) {
1061 _iterator5.return();
1062 }
1063 } finally {
1064 if (_didIteratorError5) {
1065 throw _iteratorError5;
1066 }
1067 }
1068 }
1069 } else {
1070 path.traverse({
1071 VariableDeclaration(varPath) {
1072 if (!varPath.isVariableDeclaration({
1073 kind: "var"
1074 })) return;
1075 if (!isSameFunctionScope(varPath, path)) return;
1076 var _iteratorNormalCompletion6 = true;
1077 var _didIteratorError6 = false;
1078 var _iteratorError6 = undefined;
1079
1080 try {
1081 for (var _iterator6 = varPath.node.declarations[Symbol.iterator](), _step6; !(_iteratorNormalCompletion6 = (_step6 = _iterator6.next()).done); _iteratorNormalCompletion6 = true) {
1082 const decl = _step6.value;
1083 const bindingIds = Object.keys(t.getBindingIdentifiers(decl.id));
1084 declarators.push(...bindingIds.map(name => t.variableDeclarator(t.identifier(name))));
1085 }
1086 } catch (err) {
1087 _didIteratorError6 = true;
1088 _iteratorError6 = err;
1089 } finally {
1090 try {
1091 if (!_iteratorNormalCompletion6 && _iterator6.return != null) {
1092 _iterator6.return();
1093 }
1094 } finally {
1095 if (_didIteratorError6) {
1096 throw _iteratorError6;
1097 }
1098 }
1099 }
1100 }
1101
1102 });
1103 }
1104
1105 if (declarators.length <= 0) return [];
1106 return [t.variableDeclaration("var", declarators)];
1107 }
1108
1109 function replace(path, options) {
1110 const replacement = options.replacement,
1111 replacementPath = options.replacementPath,
1112 scope = options.scope,
1113 binding = options.binding; // Same name, different binding.
1114
1115 if (scope.getBinding(path.node.name) !== binding) {
1116 return;
1117 } // We don't want to move code around to different scopes because:
1118 // 1. Original bindings that is referenced could be shadowed
1119 // 2. Moving defintions to potentially hot code is bad
1120
1121
1122 if (scope !== path.scope) {
1123 if (t.isClass(replacement) || t.isFunction(replacement)) {
1124 return;
1125 }
1126
1127 let bail = false;
1128 traverse(replacement, {
1129 Function(path) {
1130 if (bail) {
1131 return;
1132 }
1133
1134 bail = true;
1135 path.stop();
1136 }
1137
1138 }, scope);
1139
1140 if (bail) {
1141 return;
1142 }
1143 } // Avoid recursion.
1144
1145
1146 if (path.find(({
1147 node
1148 }) => node === replacement)) {
1149 return;
1150 } // https://github.com/babel/minify/issues/611
1151 // this is valid only for FunctionDeclaration where we convert
1152 // function declaration to expression in the next step
1153
1154
1155 if (replacementPath.isFunctionDeclaration()) {
1156 const fnName = replacementPath.get("id").node.name;
1157
1158 for (let name in replacementPath.scope.bindings) {
1159 if (name === fnName) {
1160 return;
1161 }
1162 }
1163 } // https://github.com/babel/minify/issues/130
1164
1165
1166 if (!t.isExpression(replacement)) {
1167 t.toExpression(replacement);
1168 } // We don't remove fn name here, we let the FnExpr & ClassExpr visitors
1169 // check its references and remove unreferenced ones
1170 // if (t.isFunction(replacement)) {
1171 // replacement.id = null;
1172 // }
1173
1174
1175 path.replaceWith(replacement);
1176 return true;
1177 }
1178
1179 function updateReferences(fnToDeletePath) {
1180 if (!fnToDeletePath.isFunction()) {
1181 return;
1182 }
1183
1184 fnToDeletePath.traverse({
1185 ReferencedIdentifier(path) {
1186 const node = path.node,
1187 scope = path.scope;
1188 const binding = scope.getBinding(node.name);
1189
1190 if (!binding || !binding.path.isFunction() || binding.scope === scope || !binding.constant) {
1191 return;
1192 }
1193
1194 const index = binding.referencePaths.indexOf(path);
1195
1196 if (index === -1) {
1197 return;
1198 }
1199
1200 binding.references--;
1201 binding.referencePaths.splice(index, 1);
1202
1203 if (binding.references === 0) {
1204 binding.referenced = false;
1205 }
1206
1207 if (binding.references <= 1 && binding.scope.path.node) {
1208 binding.scope.path.node[shouldRevisit] = true;
1209 }
1210 }
1211
1212 });
1213 }
1214
1215 function removeUnreferencedId(path) {
1216 const id = path.get("id").node;
1217
1218 if (!id) {
1219 return;
1220 }
1221
1222 const node = path.node,
1223 scope = path.scope;
1224 const binding = scope.getBinding(id.name); // Check if shadowed or is not referenced.
1225
1226 if (binding && (binding.path.node !== node || !binding.referenced)) {
1227 node.id = null;
1228 }
1229 } // path1 -> path2
1230 // is path1 an ancestor of path2
1231
1232
1233 function isAncestor(path1, path2) {
1234 return !!path2.findParent(parent => parent === path1);
1235 }
1236
1237 function isSameFunctionScope(path1, path2) {
1238 return path1.scope.getFunctionParent() === path2.scope.getFunctionParent();
1239 }
1240
1241 function isBreaking(stmt, path) {
1242 return isControlTransfer(stmt, path, "break");
1243 }
1244
1245 function isContinuing(stmt, path) {
1246 return isControlTransfer(stmt, path, "continue");
1247 } // tells if a "stmt" is a break/continue statement
1248
1249
1250 function isControlTransfer(stmt, path, control = "break") {
1251 const _break$continue = {
1252 break: "BreakStatement",
1253 continue: "ContinueStatement"
1254 },
1255 type = _break$continue[control];
1256
1257 if (!type) {
1258 throw new Error("Can only handle break and continue statements");
1259 }
1260
1261 const checker = `is${type}`;
1262
1263 if (stmt[checker]()) {
1264 return _isControlTransfer(stmt, path);
1265 }
1266
1267 let isTransferred = false;
1268 let result = {
1269 [control]: false,
1270 bail: false
1271 };
1272 stmt.traverse({
1273 [type](cPath) {
1274 // if we already detected a break/continue statement,
1275 if (isTransferred) return;
1276 result = _isControlTransfer(cPath, path);
1277
1278 if (result.bail || result[control]) {
1279 isTransferred = true;
1280 }
1281 }
1282
1283 });
1284 return result;
1285
1286 function _isControlTransfer(cPath, path) {
1287 const label = cPath.get("label");
1288
1289 if (label.node !== null) {
1290 // labels are fn scoped and not accessible by inner functions
1291 // path is the switch statement
1292 if (!isSameFunctionScope(path, cPath)) {
1293 // we don't have to worry about this break statement
1294 return {
1295 break: false,
1296 bail: false
1297 };
1298 } // here we handle the break labels
1299 // if they are outside switch, we bail out
1300 // if they are within the case, we keep them
1301
1302
1303 let labelPath;
1304
1305 if (path.scope.getLabel) {
1306 labelPath = getLabel(label.node.name, path);
1307 } else {
1308 labelPath = path.scope.getBinding(label.node.name).path;
1309 }
1310
1311 const _isAncestor = isAncestor(labelPath, path);
1312
1313 return {
1314 bail: _isAncestor,
1315 [control]: _isAncestor
1316 };
1317 } // set the flag that it is indeed breaking
1318
1319
1320 let isCTransfer = true; // this flag is to capture
1321 // switch(0) { case 0: while(1) if (x) break; }
1322
1323 let possibleRunTimeControlTransfer = false; // and compute if it's breaking the correct thing
1324
1325 let parent = cPath.parentPath;
1326
1327 while (parent !== stmt.parentPath) {
1328 // loops and nested switch cases
1329 if (parent.isLoop() || parent.isSwitchCase()) {
1330 // invalidate all the possible runtime breaks captured
1331 // while (1) { if (x) break; }
1332 possibleRunTimeControlTransfer = false; // and set that it's not breaking our switch statement
1333
1334 isCTransfer = false;
1335 break;
1336 } //
1337 // this is a special case and depends on
1338 // the fact that SwitchStatement is handled in the
1339 // exit hook of the traverse
1340 //
1341 // switch (0) {
1342 // case 0: if (x) break;
1343 // }
1344 //
1345 // here `x` is runtime only.
1346 // in this case, we need to bail out. So we depend on exit hook
1347 // of switch so that, it would have visited the IfStatement first
1348 // before the SwitchStatement and would have removed the
1349 // IfStatement if it was a compile time determined
1350 //
1351
1352
1353 if (parent.isIfStatement()) {
1354 possibleRunTimeControlTransfer = true;
1355 }
1356
1357 parent = parent.parentPath;
1358 }
1359
1360 return {
1361 [control]: possibleRunTimeControlTransfer || isCTransfer,
1362 bail: possibleRunTimeControlTransfer
1363 };
1364 }
1365 } // things that are hoisted
1366
1367
1368 function canExistAfterCompletion(path) {
1369 return path.isFunctionDeclaration() || path.isVariableDeclaration({
1370 kind: "var"
1371 });
1372 }
1373
1374 function getLabel(name, _path) {
1375 let label,
1376 path = _path;
1377
1378 do {
1379 label = path.scope.getLabel(name);
1380
1381 if (label) {
1382 return label;
1383 }
1384 } while (path = path.parentPath);
1385
1386 return null;
1387 }
1388
1389 function hasLoopParent(path) {
1390 let parent = path;
1391
1392 do {
1393 if (parent.isLoop()) {
1394 return true;
1395 }
1396 } while (parent = parent.parentPath);
1397
1398 return false;
1399 }
1400
1401 function extractSequenceImpure(seq) {
1402 const expressions = seq.get("expressions");
1403 const result = [];
1404
1405 for (let i = 0; i < expressions.length; i++) {
1406 if (!expressions[i].isPure()) {
1407 result.push(expressions[i].node);
1408 }
1409 }
1410
1411 return t.sequenceExpression(result);
1412 }
1413};
\No newline at end of file