mirror of
https://github.com/S2-/minifyfromhtml.git
synced 2025-08-03 20:30:04 +02:00
1402 lines
41 KiB
JavaScript
1402 lines
41 KiB
JavaScript
"use strict";
|
|
|
|
const some = require("lodash.some");
|
|
|
|
const _require = require("babel-helper-mark-eval-scopes"),
|
|
markEvalScopes = _require.markEvalScopes,
|
|
hasEval = _require.hasEval;
|
|
|
|
const removeUseStrict = require("./remove-use-strict");
|
|
|
|
const evaluate = require("babel-helper-evaluate-path");
|
|
|
|
function prevSiblings(path) {
|
|
const parentPath = path.parentPath;
|
|
const siblings = [];
|
|
let key = parentPath.key;
|
|
|
|
while ((path = parentPath.getSibling(--key)).type) {
|
|
siblings.push(path);
|
|
}
|
|
|
|
return siblings;
|
|
}
|
|
|
|
function forEachAncestor(path, callback) {
|
|
while (path = path.parentPath) {
|
|
callback(path);
|
|
}
|
|
}
|
|
|
|
module.exports = ({
|
|
types: t,
|
|
traverse
|
|
}) => {
|
|
const removeOrVoid = require("babel-helper-remove-or-void")(t);
|
|
|
|
const shouldRevisit = Symbol("shouldRevisit"); // this is used for tracking fn params that can be removed
|
|
// as traversal takes place from left and
|
|
// unused params can be removed only on the right
|
|
|
|
const markForRemoval = Symbol("markForRemoval");
|
|
const main = {
|
|
// remove side effectless statement
|
|
ExpressionStatement(path) {
|
|
if (path.get("expression").isPure()) {
|
|
removeOrVoid(path);
|
|
}
|
|
},
|
|
|
|
Function: {
|
|
// Let's take all the vars in a function that are not in the top level scope and hoist them
|
|
// with the first var declaration in the top-level scope. This transform in itself may
|
|
// not yield much returns (or even can be marginally harmful to size). However it's great
|
|
// for taking away statements from blocks that can be only expressions which the `simplify`
|
|
// plugin can turn into other things (e.g. if => conditional).
|
|
exit(path) {
|
|
// This hurts gzip size.
|
|
if (!this.optimizeRawSize) {
|
|
return;
|
|
}
|
|
|
|
const node = path.node,
|
|
scope = path.scope;
|
|
const seen = new Set();
|
|
const declars = [];
|
|
const mutations = [];
|
|
|
|
for (const name in scope.bindings) {
|
|
const binding = scope.bindings[name];
|
|
|
|
if (!binding.path.isVariableDeclarator()) {
|
|
continue;
|
|
}
|
|
|
|
const declarPath = binding.path.parentPath;
|
|
|
|
if (seen.has(declarPath)) {
|
|
continue;
|
|
}
|
|
|
|
seen.add(declarPath);
|
|
|
|
if (declarPath.parentPath.isForInStatement()) {
|
|
continue;
|
|
}
|
|
|
|
if (declarPath.parentPath.parentPath.isFunction()) {
|
|
continue;
|
|
}
|
|
|
|
if (!declarPath.node || !declarPath.node.declarations) {
|
|
continue;
|
|
}
|
|
|
|
const assignmentSequence = [];
|
|
var _iteratorNormalCompletion = true;
|
|
var _didIteratorError = false;
|
|
var _iteratorError = undefined;
|
|
|
|
try {
|
|
for (var _iterator = declarPath.node.declarations[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
|
|
const declar = _step.value;
|
|
declars.push(declar);
|
|
|
|
if (declar.init) {
|
|
assignmentSequence.push(t.assignmentExpression("=", declar.id, declar.init));
|
|
mutations.push(() => {
|
|
declar.init = null;
|
|
});
|
|
}
|
|
}
|
|
} catch (err) {
|
|
_didIteratorError = true;
|
|
_iteratorError = err;
|
|
} finally {
|
|
try {
|
|
if (!_iteratorNormalCompletion && _iterator.return != null) {
|
|
_iterator.return();
|
|
}
|
|
} finally {
|
|
if (_didIteratorError) {
|
|
throw _iteratorError;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (assignmentSequence.length) {
|
|
mutations.push(() => declarPath.replaceWith(t.sequenceExpression(assignmentSequence)));
|
|
} else {
|
|
mutations.push(() => removeOrVoid(declarPath));
|
|
}
|
|
}
|
|
|
|
if (declars.length) {
|
|
mutations.forEach(f => f());
|
|
var _iteratorNormalCompletion2 = true;
|
|
var _didIteratorError2 = false;
|
|
var _iteratorError2 = undefined;
|
|
|
|
try {
|
|
for (var _iterator2 = node.body.body[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
|
|
const statement = _step2.value;
|
|
|
|
if (t.isVariableDeclaration(statement)) {
|
|
statement.declarations.push(...declars);
|
|
return;
|
|
}
|
|
}
|
|
} catch (err) {
|
|
_didIteratorError2 = true;
|
|
_iteratorError2 = err;
|
|
} finally {
|
|
try {
|
|
if (!_iteratorNormalCompletion2 && _iterator2.return != null) {
|
|
_iterator2.return();
|
|
}
|
|
} finally {
|
|
if (_didIteratorError2) {
|
|
throw _iteratorError2;
|
|
}
|
|
}
|
|
}
|
|
|
|
const varDecl = t.variableDeclaration("var", declars);
|
|
node.body.body.unshift(varDecl);
|
|
}
|
|
}
|
|
|
|
},
|
|
// Remove bindings with no references.
|
|
Scope: {
|
|
exit(path) {
|
|
if (path.node[shouldRevisit]) {
|
|
delete path.node[shouldRevisit];
|
|
path.visit();
|
|
}
|
|
},
|
|
|
|
enter(path) {
|
|
if (path.isProgram()) {
|
|
return;
|
|
}
|
|
|
|
if (hasEval(path.scope)) {
|
|
return;
|
|
}
|
|
|
|
const scope = path.scope; // if the scope is created by a function, we obtain its
|
|
// parameter list
|
|
|
|
const canRemoveParams = path.isFunction() && path.node.kind !== "set";
|
|
const paramsList = canRemoveParams ? path.get("params") : [];
|
|
|
|
for (let i = paramsList.length - 1; i >= 0; i--) {
|
|
const param = paramsList[i];
|
|
|
|
if (param.isIdentifier()) {
|
|
const binding = scope.bindings[param.node.name];
|
|
if (!binding) continue;
|
|
|
|
if (binding.referenced) {
|
|
// when the first binding is referenced (right to left)
|
|
// exit without marking anything after this
|
|
break;
|
|
}
|
|
|
|
binding[markForRemoval] = true;
|
|
continue;
|
|
} else if (param.isAssignmentPattern()) {
|
|
const left = param.get("left");
|
|
const right = param.get("right");
|
|
|
|
if (left.isIdentifier() && right.isPure()) {
|
|
const binding = scope.bindings[left.node.name];
|
|
|
|
if (binding.referenced) {
|
|
// when the first binding is referenced (right to left)
|
|
// exit without marking anything after this
|
|
break;
|
|
}
|
|
|
|
binding[markForRemoval] = true;
|
|
continue;
|
|
}
|
|
} // other patterns - assignment, object have side-effects
|
|
// and cannot be safely removed
|
|
|
|
|
|
break;
|
|
}
|
|
|
|
for (const name in scope.bindings) {
|
|
const binding = scope.bindings[name];
|
|
|
|
if (!binding.referenced && binding.kind !== "module") {
|
|
if (binding.kind === "param" && (this.keepFnArgs || !binding[markForRemoval])) {
|
|
continue;
|
|
} else if (binding.path.isVariableDeclarator()) {
|
|
const declaration = binding.path.parentPath;
|
|
const maybeBlockParent = declaration.parentPath;
|
|
|
|
if (maybeBlockParent && maybeBlockParent.isForXStatement({
|
|
left: declaration.node
|
|
})) {
|
|
// Can't remove if in a for-in/for-of/for-await statement `for (var x in wat)`.
|
|
continue;
|
|
}
|
|
} else if (!scope.isPure(binding.path.node)) {
|
|
// TODO: AssignmentPattern are marked as impure and unused ids aren't removed yet
|
|
continue;
|
|
} else if (binding.path.isFunctionExpression() || binding.path.isClassExpression()) {
|
|
// `bar(function foo() {})` foo is not referenced but it's used.
|
|
continue;
|
|
} else if ( // ClassDeclaration has binding in two scopes
|
|
// 1. The scope in which it is declared
|
|
// 2. The class's own scope
|
|
binding.path.isClassDeclaration() && binding.path === scope.path) {
|
|
continue;
|
|
}
|
|
|
|
const mutations = [];
|
|
let bail = false; // Make sure none of the assignments value is used
|
|
|
|
binding.constantViolations.forEach(p => {
|
|
if (bail || p === binding.path) {
|
|
return;
|
|
}
|
|
|
|
if (!p.parentPath.isExpressionStatement()) {
|
|
bail = true;
|
|
}
|
|
|
|
if (p.isAssignmentExpression() && !p.get("right").isPure()) {
|
|
mutations.push(() => p.replaceWith(p.get("right")));
|
|
} else {
|
|
mutations.push(() => removeOrVoid(p));
|
|
}
|
|
});
|
|
|
|
if (bail) {
|
|
continue;
|
|
}
|
|
|
|
if (binding.path.isVariableDeclarator()) {
|
|
if (!binding.path.get("id").isIdentifier()) {
|
|
// deopt for object and array pattern
|
|
continue;
|
|
} // if declarator has some impure init expression
|
|
// var x = foo();
|
|
// => foo();
|
|
|
|
|
|
if (binding.path.node.init && !scope.isPure(binding.path.node.init) && binding.path.parentPath.node.declarations) {
|
|
// binding path has more than one declarations
|
|
if (binding.path.parentPath.node.declarations.length !== 1) {
|
|
continue;
|
|
}
|
|
|
|
binding.path.parentPath.replaceWith(binding.path.node.init);
|
|
} else {
|
|
updateReferences(binding.path, this);
|
|
removeOrVoid(binding.path);
|
|
}
|
|
} else {
|
|
updateReferences(binding.path, this);
|
|
removeOrVoid(binding.path);
|
|
}
|
|
|
|
mutations.forEach(f => f());
|
|
scope.removeBinding(name);
|
|
} else if (binding.constant) {
|
|
if (binding.path.isFunctionDeclaration() || binding.path.isVariableDeclarator() && binding.path.get("init").isFunction()) {
|
|
const fun = binding.path.isFunctionDeclaration() ? binding.path : binding.path.get("init");
|
|
let allInside = true;
|
|
var _iteratorNormalCompletion3 = true;
|
|
var _didIteratorError3 = false;
|
|
var _iteratorError3 = undefined;
|
|
|
|
try {
|
|
for (var _iterator3 = binding.referencePaths[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {
|
|
const ref = _step3.value;
|
|
|
|
if (!ref.find(p => p.node === fun.node)) {
|
|
allInside = false;
|
|
break;
|
|
}
|
|
}
|
|
} catch (err) {
|
|
_didIteratorError3 = true;
|
|
_iteratorError3 = err;
|
|
} finally {
|
|
try {
|
|
if (!_iteratorNormalCompletion3 && _iterator3.return != null) {
|
|
_iterator3.return();
|
|
}
|
|
} finally {
|
|
if (_didIteratorError3) {
|
|
throw _iteratorError3;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (allInside) {
|
|
scope.removeBinding(name);
|
|
updateReferences(binding.path, this);
|
|
removeOrVoid(binding.path);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (binding.references === 1 && binding.kind !== "param" && binding.kind !== "module" && binding.constant) {
|
|
let replacement = binding.path.node;
|
|
let replacementPath = binding.path;
|
|
let isReferencedBefore = false;
|
|
const refPath = binding.referencePaths[0];
|
|
|
|
if (t.isVariableDeclarator(replacement)) {
|
|
const _prevSiblings = prevSiblings(replacementPath); // traverse ancestors of a reference checking if it's before declaration
|
|
|
|
|
|
forEachAncestor(refPath, ancestor => {
|
|
if (_prevSiblings.indexOf(ancestor) > -1) {
|
|
isReferencedBefore = true;
|
|
}
|
|
}); // deopt if reference is in different scope than binding
|
|
// since we don't know if it's sync or async execution
|
|
// (i.e. whether value has been assigned to a reference or not)
|
|
|
|
if (isReferencedBefore && refPath.scope !== binding.scope) {
|
|
continue;
|
|
} // simulate hoisting by replacing value
|
|
// with undefined if declaration is after reference
|
|
|
|
|
|
replacement = isReferencedBefore ? t.unaryExpression("void", t.numericLiteral(0), true) : replacement.init; // Bail out for ArrayPattern and ObjectPattern
|
|
// TODO: maybe a more intelligent approach instead of simply bailing out
|
|
|
|
if (!replacementPath.get("id").isIdentifier()) {
|
|
continue;
|
|
}
|
|
|
|
replacementPath = replacementPath.get("init");
|
|
}
|
|
|
|
if (!replacement) {
|
|
continue;
|
|
}
|
|
|
|
if (!scope.isPure(replacement, true) && !isReferencedBefore) {
|
|
continue;
|
|
}
|
|
|
|
let bail = false;
|
|
|
|
if (replacementPath.isIdentifier()) {
|
|
const binding = scope.getBinding(replacement.name); // the reference should be in the same scope
|
|
// and the replacement should be a constant - this is to
|
|
// ensure that the duplication of replacement is not affected
|
|
// https://github.com/babel/minify/issues/685
|
|
|
|
bail = !(binding && refPath.scope.getBinding(replacement.name) === binding && binding.constantViolations.length === 0);
|
|
} else {
|
|
replacementPath.traverse({
|
|
Function(path) {
|
|
path.skip();
|
|
},
|
|
|
|
ReferencedIdentifier({
|
|
node
|
|
}) {
|
|
if (bail) {
|
|
return;
|
|
}
|
|
|
|
const binding = scope.getBinding(node.name);
|
|
|
|
if (binding && refPath.scope.getBinding(node.name) === binding) {
|
|
bail = binding.constantViolations.length > 0;
|
|
}
|
|
}
|
|
|
|
});
|
|
}
|
|
|
|
if (bail) {
|
|
continue;
|
|
}
|
|
|
|
let parent = binding.path.parent;
|
|
|
|
if (t.isVariableDeclaration(parent)) {
|
|
parent = binding.path.parentPath.parent;
|
|
} // 1. Make sure we share the parent with the node. In other words it's lexically defined
|
|
// and not in an if statement or otherwise.
|
|
// 2. If the replacement is an object then we have to make sure we are not in a loop or a function
|
|
// because otherwise we'll be inlining and doing a lot more allocation than we have to
|
|
// which would also could affect correctness in that they are not the same reference.
|
|
|
|
|
|
let mayLoop = false;
|
|
const sharesRoot = refPath.find(({
|
|
node
|
|
}) => {
|
|
if (!mayLoop) {
|
|
mayLoop = t.isWhileStatement(node) || t.isFor(node) || t.isFunction(node);
|
|
}
|
|
|
|
return node === parent;
|
|
}); // Anything that inherits from Object.
|
|
|
|
const isObj = n => t.isFunction(n) || t.isObjectExpression(n) || t.isArrayExpression(n);
|
|
|
|
const isReplacementObj = isObj(replacement) || some(replacement, isObj);
|
|
|
|
if (!sharesRoot || isReplacementObj && mayLoop) {
|
|
continue;
|
|
} // check if it's safe to replace
|
|
// To solve https://github.com/babel/minify/issues/691
|
|
// Here we bail for property checks using the "in" operator
|
|
// This is because - `in` is a side-effect-free operation but the property
|
|
// could be deleted between the replacementPath and referencePath
|
|
// It is expensive to compute the delete operation and we bail for
|
|
// all the binary "in" operations
|
|
|
|
|
|
let inExpression = replacementPath.isBinaryExpression({
|
|
operator: "in"
|
|
});
|
|
|
|
if (!inExpression) {
|
|
replacementPath.traverse({
|
|
Function(path) {
|
|
path.skip();
|
|
},
|
|
|
|
BinaryExpression(path) {
|
|
if (path.node.operator === "in") {
|
|
inExpression = true;
|
|
path.stop();
|
|
}
|
|
}
|
|
|
|
});
|
|
}
|
|
|
|
if (inExpression) {
|
|
continue;
|
|
}
|
|
|
|
const replaced = replace(binding.referencePaths[0], {
|
|
binding,
|
|
scope,
|
|
replacement,
|
|
replacementPath
|
|
});
|
|
|
|
if (replaced) {
|
|
scope.removeBinding(name);
|
|
|
|
if (binding.path.node) {
|
|
removeOrVoid(binding.path);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} // end-for-of
|
|
|
|
}
|
|
|
|
},
|
|
|
|
// Remove unreachable code.
|
|
BlockStatement(path) {
|
|
const paths = path.get("body");
|
|
let purge = false;
|
|
|
|
for (let i = 0; i < paths.length; i++) {
|
|
const p = paths[i];
|
|
|
|
if (!purge && p.isCompletionStatement()) {
|
|
purge = true;
|
|
continue;
|
|
}
|
|
|
|
if (purge && !canExistAfterCompletion(p)) {
|
|
removeOrVoid(p);
|
|
}
|
|
}
|
|
},
|
|
|
|
// Double check unreachable code and remove return statements that
|
|
// have no semantic meaning
|
|
ReturnStatement(path) {
|
|
const node = path.node;
|
|
|
|
if (!path.inList) {
|
|
return;
|
|
} // Not last in its block? (See BlockStatement visitor)
|
|
|
|
|
|
if (path.container.length - 1 !== path.key && !canExistAfterCompletion(path.getSibling(path.key + 1)) && path.parentPath.isBlockStatement()) {
|
|
// This is probably a new oppurtinity by some other transform
|
|
// let's call the block visitor on this again before proceeding.
|
|
path.parentPath.pushContext(path.context);
|
|
path.parentPath.visit();
|
|
path.parentPath.popContext();
|
|
return;
|
|
}
|
|
|
|
if (node.argument) {
|
|
return;
|
|
}
|
|
|
|
let noNext = true;
|
|
let parentPath = path.parentPath;
|
|
|
|
while (parentPath && !parentPath.isFunction() && noNext) {
|
|
// https://github.com/babel/minify/issues/265
|
|
if (hasLoopParent(parentPath)) {
|
|
noNext = false;
|
|
break;
|
|
}
|
|
|
|
const nextPath = parentPath.getSibling(parentPath.key + 1);
|
|
|
|
if (nextPath.node) {
|
|
if (nextPath.isReturnStatement()) {
|
|
nextPath.pushContext(path.context);
|
|
nextPath.visit();
|
|
nextPath.popContext();
|
|
|
|
if (parentPath.getSibling(parentPath.key + 1).node) {
|
|
noNext = false;
|
|
break;
|
|
}
|
|
} else {
|
|
noNext = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
parentPath = parentPath.parentPath;
|
|
}
|
|
|
|
if (noNext) {
|
|
removeOrVoid(path);
|
|
}
|
|
},
|
|
|
|
ConditionalExpression(path) {
|
|
const node = path.node;
|
|
const evaluateTest = path.get("test").evaluateTruthy();
|
|
|
|
if (evaluateTest === true) {
|
|
path.replaceWith(node.consequent);
|
|
} else if (evaluateTest === false) {
|
|
path.replaceWith(node.alternate);
|
|
}
|
|
},
|
|
|
|
SwitchStatement: {
|
|
exit(path) {
|
|
const evaluated = evaluate(path.get("discriminant"), {
|
|
tdz: this.tdz
|
|
});
|
|
if (!evaluated.confident) return;
|
|
const discriminant = evaluated.value;
|
|
const cases = path.get("cases");
|
|
let matchingCaseIndex = -1;
|
|
let defaultCaseIndex = -1;
|
|
|
|
for (let i = 0; i < cases.length; i++) {
|
|
const test = cases[i].get("test"); // handle default case
|
|
|
|
if (test.node === null) {
|
|
defaultCaseIndex = i;
|
|
continue;
|
|
}
|
|
|
|
const testResult = evaluate(test, {
|
|
tdz: this.tdz
|
|
}); // if we are not able to deternine a test during
|
|
// compile time, we terminate immediately
|
|
|
|
if (!testResult.confident) return;
|
|
|
|
if (testResult.value === discriminant) {
|
|
matchingCaseIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
let result;
|
|
|
|
if (matchingCaseIndex === -1) {
|
|
if (defaultCaseIndex === -1) {
|
|
path.skip();
|
|
path.replaceWithMultiple(extractVars(path));
|
|
return;
|
|
} else {
|
|
result = getStatementsUntilBreak(defaultCaseIndex);
|
|
}
|
|
} else {
|
|
result = getStatementsUntilBreak(matchingCaseIndex);
|
|
}
|
|
|
|
if (result.bail) return; // we extract vars from the entire switch statement
|
|
// and there will be duplicates which
|
|
// will be again removed by DCE
|
|
|
|
replaceSwitch([...extractVars(path), ...result.statements]);
|
|
|
|
function getStatementsUntilBreak(start) {
|
|
const result = {
|
|
bail: false,
|
|
statements: []
|
|
};
|
|
|
|
for (let i = start; i < cases.length; i++) {
|
|
const consequent = cases[i].get("consequent");
|
|
|
|
for (let j = 0; j < consequent.length; j++) {
|
|
const _isBreaking = isBreaking(consequent[j], path);
|
|
|
|
if (_isBreaking.bail) {
|
|
result.bail = true;
|
|
return result;
|
|
}
|
|
|
|
if (_isBreaking.break) {
|
|
// compute no more
|
|
// exit out of the loop
|
|
return result;
|
|
} else {
|
|
result.statements.push(consequent[j].node);
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function replaceSwitch(statements) {
|
|
let isBlockRequired = false;
|
|
|
|
for (let i = 0; i < statements.length; i++) {
|
|
if (t.isVariableDeclaration(statements[i], {
|
|
kind: "let"
|
|
})) {
|
|
isBlockRequired = true;
|
|
break;
|
|
}
|
|
|
|
if (t.isVariableDeclaration(statements[i], {
|
|
kind: "const"
|
|
})) {
|
|
isBlockRequired = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (isBlockRequired) {
|
|
path.replaceWith(t.BlockStatement(statements));
|
|
} else {
|
|
path.replaceWithMultiple(statements);
|
|
}
|
|
}
|
|
}
|
|
|
|
},
|
|
|
|
WhileStatement(path) {
|
|
const test = path.get("test");
|
|
const result = evaluate(test, {
|
|
tdz: this.tdz
|
|
});
|
|
|
|
if (result.confident && test.isPure() && !result.value) {
|
|
path.remove();
|
|
}
|
|
},
|
|
|
|
ForStatement(path) {
|
|
const test = path.get("test");
|
|
if (!test.isPure()) return;
|
|
const result = evaluate(test, {
|
|
tdz: this.tdz
|
|
});
|
|
|
|
if (result.confident) {
|
|
if (result.value) {
|
|
test.remove();
|
|
} else {
|
|
path.remove();
|
|
}
|
|
}
|
|
},
|
|
|
|
DoWhileStatement(path) {
|
|
const test = path.get("test");
|
|
const result = evaluate(test, {
|
|
tdz: this.tdz
|
|
});
|
|
|
|
if (result.confident && test.isPure() && !result.value) {
|
|
const body = path.get("body");
|
|
|
|
if (body.isBlockStatement()) {
|
|
const stmts = body.get("body");
|
|
var _iteratorNormalCompletion4 = true;
|
|
var _didIteratorError4 = false;
|
|
var _iteratorError4 = undefined;
|
|
|
|
try {
|
|
for (var _iterator4 = stmts[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) {
|
|
const stmt = _step4.value;
|
|
|
|
const _isBreaking = isBreaking(stmt, path);
|
|
|
|
if (_isBreaking.bail || _isBreaking.break) return;
|
|
|
|
const _isContinuing = isContinuing(stmt, path);
|
|
|
|
if (_isContinuing.bail || isContinuing.continue) return;
|
|
}
|
|
} catch (err) {
|
|
_didIteratorError4 = true;
|
|
_iteratorError4 = err;
|
|
} finally {
|
|
try {
|
|
if (!_iteratorNormalCompletion4 && _iterator4.return != null) {
|
|
_iterator4.return();
|
|
}
|
|
} finally {
|
|
if (_didIteratorError4) {
|
|
throw _iteratorError4;
|
|
}
|
|
}
|
|
}
|
|
|
|
path.replaceWith(body.node);
|
|
} else if (body.isBreakStatement()) {
|
|
const _isBreaking = isBreaking(body, path);
|
|
|
|
if (_isBreaking.bail) return;
|
|
if (_isBreaking.break) path.remove();
|
|
} else if (body.isContinueStatement()) {
|
|
return;
|
|
} else {
|
|
path.replaceWith(body.node);
|
|
}
|
|
}
|
|
},
|
|
|
|
// Join assignment and definition when in sequence.
|
|
// var x; x = 1; -> var x = 1;
|
|
AssignmentExpression(path) {
|
|
if (!path.get("left").isIdentifier() || !path.parentPath.isExpressionStatement()) {
|
|
return;
|
|
}
|
|
|
|
const prev = path.parentPath.getSibling(path.parentPath.key - 1);
|
|
|
|
if (!(prev && prev.isVariableDeclaration())) {
|
|
return;
|
|
}
|
|
|
|
const declars = prev.node.declarations;
|
|
|
|
if (declars.length !== 1 || declars[0].init || declars[0].id.name !== path.get("left").node.name) {
|
|
return;
|
|
}
|
|
|
|
declars[0].init = path.node.right;
|
|
removeOrVoid(path);
|
|
},
|
|
|
|
// Remove named function expression name. While this is dangerous as it changes
|
|
// `function.name` all minifiers do it and hence became a standard.
|
|
FunctionExpression(path) {
|
|
if (!this.keepFnName) {
|
|
removeUnreferencedId(path);
|
|
}
|
|
},
|
|
|
|
// remove class names
|
|
ClassExpression(path) {
|
|
if (!this.keepClassName) {
|
|
removeUnreferencedId(path);
|
|
}
|
|
},
|
|
|
|
// Put the `var` in the left if feasible.
|
|
ForInStatement(path) {
|
|
const left = path.get("left");
|
|
|
|
if (!left.isIdentifier()) {
|
|
return;
|
|
}
|
|
|
|
const binding = path.scope.getBinding(left.node.name);
|
|
|
|
if (!binding) {
|
|
return;
|
|
}
|
|
|
|
if (binding.scope.getFunctionParent() !== path.scope.getFunctionParent()) {
|
|
return;
|
|
}
|
|
|
|
if (!binding.path.isVariableDeclarator()) {
|
|
return;
|
|
}
|
|
|
|
if (binding.path.parentPath.parentPath.isForInStatement({
|
|
left: binding.path.parent
|
|
})) {
|
|
return;
|
|
} // If it has company then it's probably more efficient to keep.
|
|
|
|
|
|
if (binding.path.parent.declarations.length > 1) {
|
|
return;
|
|
} // meh
|
|
|
|
|
|
if (binding.path.node.init) {
|
|
return;
|
|
}
|
|
|
|
removeOrVoid(binding.path);
|
|
path.node.left = t.variableDeclaration("var", [t.variableDeclarator(left.node)]);
|
|
binding.path = path.get("left").get("declarations")[0];
|
|
}
|
|
|
|
};
|
|
return {
|
|
name: "minify-dead-code-elimination",
|
|
visitor: {
|
|
Function: {
|
|
exit(path) {
|
|
/**
|
|
* Use exit handler to traverse in a dfs post-order fashion
|
|
* to remove use strict
|
|
*/
|
|
const body = path.get("body");
|
|
|
|
if (body.isBlockStatement()) {
|
|
removeUseStrict(body);
|
|
}
|
|
}
|
|
|
|
},
|
|
IfStatement: {
|
|
exit(path, {
|
|
opts: {
|
|
tdz = false
|
|
} = {}
|
|
}) {
|
|
const consequent = path.get("consequent");
|
|
const alternate = path.get("alternate");
|
|
const test = path.get("test");
|
|
const evalResult = evaluate(test, {
|
|
tdz
|
|
});
|
|
const isPure = test.isPure();
|
|
const replacements = [];
|
|
|
|
if (evalResult.confident && !isPure && test.isSequenceExpression()) {
|
|
replacements.push(t.expressionStatement(extractSequenceImpure(test)));
|
|
} // we can check if a test will be truthy 100% and if so then we can inline
|
|
// the consequent and completely ignore the alternate
|
|
//
|
|
// if (true) { foo; } -> { foo; }
|
|
// if ("foo") { foo; } -> { foo; }
|
|
//
|
|
|
|
|
|
if (evalResult.confident && evalResult.value) {
|
|
path.replaceWithMultiple([...replacements, ...toStatements(consequent), ...extractVars(alternate)]);
|
|
return;
|
|
} // we can check if a test will be falsy 100% and if so we can inline the
|
|
// alternate if there is one and completely remove the consequent
|
|
//
|
|
// if ("") { bar; } else { foo; } -> { foo; }
|
|
// if ("") { bar; } ->
|
|
//
|
|
|
|
|
|
if (evalResult.confident && !evalResult.value) {
|
|
if (alternate.node) {
|
|
path.replaceWithMultiple([...replacements, ...toStatements(alternate), ...extractVars(consequent)]);
|
|
return;
|
|
} else {
|
|
path.replaceWithMultiple([...replacements, ...extractVars(consequent)]);
|
|
}
|
|
} // remove alternate blocks that are empty
|
|
//
|
|
// if (foo) { foo; } else {} -> if (foo) { foo; }
|
|
//
|
|
|
|
|
|
if (alternate.isBlockStatement() && !alternate.node.body.length) {
|
|
alternate.remove(); // For if-statements babel-traverse replaces with an empty block
|
|
|
|
path.node.alternate = null;
|
|
} // if the consequent block is empty turn alternate blocks into a consequent
|
|
// and flip the test
|
|
//
|
|
// if (foo) {} else { bar; } -> if (!foo) { bar; }
|
|
//
|
|
|
|
|
|
if (consequent.isBlockStatement() && !consequent.node.body.length && alternate.isBlockStatement() && alternate.node.body.length) {
|
|
consequent.replaceWith(alternate.node);
|
|
alternate.remove(); // For if-statements babel-traverse replaces with an empty block
|
|
|
|
path.node.alternate = null;
|
|
test.replaceWith(t.unaryExpression("!", test.node, true));
|
|
}
|
|
}
|
|
|
|
},
|
|
|
|
EmptyStatement(path) {
|
|
if (path.parentPath.isBlockStatement() || path.parentPath.isProgram()) {
|
|
path.remove();
|
|
}
|
|
},
|
|
|
|
Program: {
|
|
exit(path, {
|
|
opts: {
|
|
// set defaults
|
|
optimizeRawSize = false,
|
|
keepFnName = false,
|
|
keepClassName = false,
|
|
keepFnArgs = false,
|
|
tdz = false
|
|
} = {}
|
|
} = {}) {
|
|
(traverse.clearCache || traverse.cache.clear)();
|
|
path.scope.crawl();
|
|
markEvalScopes(path); // We need to run this plugin in isolation.
|
|
|
|
path.traverse(main, {
|
|
functionToBindings: new Map(),
|
|
optimizeRawSize,
|
|
keepFnName,
|
|
keepClassName,
|
|
keepFnArgs,
|
|
tdz
|
|
});
|
|
}
|
|
|
|
}
|
|
}
|
|
};
|
|
|
|
function toStatements(path) {
|
|
const node = path.node;
|
|
|
|
if (path.isBlockStatement()) {
|
|
let hasBlockScoped = false;
|
|
|
|
for (let i = 0; i < node.body.length; i++) {
|
|
const bodyNode = node.body[i];
|
|
|
|
if (t.isBlockScoped(bodyNode)) {
|
|
hasBlockScoped = true;
|
|
}
|
|
}
|
|
|
|
if (!hasBlockScoped) {
|
|
return node.body;
|
|
}
|
|
}
|
|
|
|
return [node];
|
|
} // Extracts vars from a path
|
|
// Useful for removing blocks or paths that can contain
|
|
// variable declarations inside them
|
|
// Note:
|
|
// drops are inits
|
|
// extractVars({ var x = 5, y = x }) => var x, y;
|
|
|
|
|
|
function extractVars(path) {
|
|
const declarators = [];
|
|
|
|
if (path.isVariableDeclaration({
|
|
kind: "var"
|
|
})) {
|
|
var _iteratorNormalCompletion5 = true;
|
|
var _didIteratorError5 = false;
|
|
var _iteratorError5 = undefined;
|
|
|
|
try {
|
|
for (var _iterator5 = path.node.declarations[Symbol.iterator](), _step5; !(_iteratorNormalCompletion5 = (_step5 = _iterator5.next()).done); _iteratorNormalCompletion5 = true) {
|
|
const decl = _step5.value;
|
|
const bindingIds = Object.keys(t.getBindingIdentifiers(decl.id));
|
|
declarators.push(...bindingIds.map(name => t.variableDeclarator(t.identifier(name))));
|
|
}
|
|
} catch (err) {
|
|
_didIteratorError5 = true;
|
|
_iteratorError5 = err;
|
|
} finally {
|
|
try {
|
|
if (!_iteratorNormalCompletion5 && _iterator5.return != null) {
|
|
_iterator5.return();
|
|
}
|
|
} finally {
|
|
if (_didIteratorError5) {
|
|
throw _iteratorError5;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
path.traverse({
|
|
VariableDeclaration(varPath) {
|
|
if (!varPath.isVariableDeclaration({
|
|
kind: "var"
|
|
})) return;
|
|
if (!isSameFunctionScope(varPath, path)) return;
|
|
var _iteratorNormalCompletion6 = true;
|
|
var _didIteratorError6 = false;
|
|
var _iteratorError6 = undefined;
|
|
|
|
try {
|
|
for (var _iterator6 = varPath.node.declarations[Symbol.iterator](), _step6; !(_iteratorNormalCompletion6 = (_step6 = _iterator6.next()).done); _iteratorNormalCompletion6 = true) {
|
|
const decl = _step6.value;
|
|
const bindingIds = Object.keys(t.getBindingIdentifiers(decl.id));
|
|
declarators.push(...bindingIds.map(name => t.variableDeclarator(t.identifier(name))));
|
|
}
|
|
} catch (err) {
|
|
_didIteratorError6 = true;
|
|
_iteratorError6 = err;
|
|
} finally {
|
|
try {
|
|
if (!_iteratorNormalCompletion6 && _iterator6.return != null) {
|
|
_iterator6.return();
|
|
}
|
|
} finally {
|
|
if (_didIteratorError6) {
|
|
throw _iteratorError6;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
});
|
|
}
|
|
|
|
if (declarators.length <= 0) return [];
|
|
return [t.variableDeclaration("var", declarators)];
|
|
}
|
|
|
|
function replace(path, options) {
|
|
const replacement = options.replacement,
|
|
replacementPath = options.replacementPath,
|
|
scope = options.scope,
|
|
binding = options.binding; // Same name, different binding.
|
|
|
|
if (scope.getBinding(path.node.name) !== binding) {
|
|
return;
|
|
} // We don't want to move code around to different scopes because:
|
|
// 1. Original bindings that is referenced could be shadowed
|
|
// 2. Moving defintions to potentially hot code is bad
|
|
|
|
|
|
if (scope !== path.scope) {
|
|
if (t.isClass(replacement) || t.isFunction(replacement)) {
|
|
return;
|
|
}
|
|
|
|
let bail = false;
|
|
traverse(replacement, {
|
|
Function(path) {
|
|
if (bail) {
|
|
return;
|
|
}
|
|
|
|
bail = true;
|
|
path.stop();
|
|
}
|
|
|
|
}, scope);
|
|
|
|
if (bail) {
|
|
return;
|
|
}
|
|
} // Avoid recursion.
|
|
|
|
|
|
if (path.find(({
|
|
node
|
|
}) => node === replacement)) {
|
|
return;
|
|
} // https://github.com/babel/minify/issues/611
|
|
// this is valid only for FunctionDeclaration where we convert
|
|
// function declaration to expression in the next step
|
|
|
|
|
|
if (replacementPath.isFunctionDeclaration()) {
|
|
const fnName = replacementPath.get("id").node.name;
|
|
|
|
for (let name in replacementPath.scope.bindings) {
|
|
if (name === fnName) {
|
|
return;
|
|
}
|
|
}
|
|
} // https://github.com/babel/minify/issues/130
|
|
|
|
|
|
if (!t.isExpression(replacement)) {
|
|
t.toExpression(replacement);
|
|
} // We don't remove fn name here, we let the FnExpr & ClassExpr visitors
|
|
// check its references and remove unreferenced ones
|
|
// if (t.isFunction(replacement)) {
|
|
// replacement.id = null;
|
|
// }
|
|
|
|
|
|
path.replaceWith(replacement);
|
|
return true;
|
|
}
|
|
|
|
function updateReferences(fnToDeletePath) {
|
|
if (!fnToDeletePath.isFunction()) {
|
|
return;
|
|
}
|
|
|
|
fnToDeletePath.traverse({
|
|
ReferencedIdentifier(path) {
|
|
const node = path.node,
|
|
scope = path.scope;
|
|
const binding = scope.getBinding(node.name);
|
|
|
|
if (!binding || !binding.path.isFunction() || binding.scope === scope || !binding.constant) {
|
|
return;
|
|
}
|
|
|
|
const index = binding.referencePaths.indexOf(path);
|
|
|
|
if (index === -1) {
|
|
return;
|
|
}
|
|
|
|
binding.references--;
|
|
binding.referencePaths.splice(index, 1);
|
|
|
|
if (binding.references === 0) {
|
|
binding.referenced = false;
|
|
}
|
|
|
|
if (binding.references <= 1 && binding.scope.path.node) {
|
|
binding.scope.path.node[shouldRevisit] = true;
|
|
}
|
|
}
|
|
|
|
});
|
|
}
|
|
|
|
function removeUnreferencedId(path) {
|
|
const id = path.get("id").node;
|
|
|
|
if (!id) {
|
|
return;
|
|
}
|
|
|
|
const node = path.node,
|
|
scope = path.scope;
|
|
const binding = scope.getBinding(id.name); // Check if shadowed or is not referenced.
|
|
|
|
if (binding && (binding.path.node !== node || !binding.referenced)) {
|
|
node.id = null;
|
|
}
|
|
} // path1 -> path2
|
|
// is path1 an ancestor of path2
|
|
|
|
|
|
function isAncestor(path1, path2) {
|
|
return !!path2.findParent(parent => parent === path1);
|
|
}
|
|
|
|
function isSameFunctionScope(path1, path2) {
|
|
return path1.scope.getFunctionParent() === path2.scope.getFunctionParent();
|
|
}
|
|
|
|
function isBreaking(stmt, path) {
|
|
return isControlTransfer(stmt, path, "break");
|
|
}
|
|
|
|
function isContinuing(stmt, path) {
|
|
return isControlTransfer(stmt, path, "continue");
|
|
} // tells if a "stmt" is a break/continue statement
|
|
|
|
|
|
function isControlTransfer(stmt, path, control = "break") {
|
|
const _break$continue = {
|
|
break: "BreakStatement",
|
|
continue: "ContinueStatement"
|
|
},
|
|
type = _break$continue[control];
|
|
|
|
if (!type) {
|
|
throw new Error("Can only handle break and continue statements");
|
|
}
|
|
|
|
const checker = `is${type}`;
|
|
|
|
if (stmt[checker]()) {
|
|
return _isControlTransfer(stmt, path);
|
|
}
|
|
|
|
let isTransferred = false;
|
|
let result = {
|
|
[control]: false,
|
|
bail: false
|
|
};
|
|
stmt.traverse({
|
|
[type](cPath) {
|
|
// if we already detected a break/continue statement,
|
|
if (isTransferred) return;
|
|
result = _isControlTransfer(cPath, path);
|
|
|
|
if (result.bail || result[control]) {
|
|
isTransferred = true;
|
|
}
|
|
}
|
|
|
|
});
|
|
return result;
|
|
|
|
function _isControlTransfer(cPath, path) {
|
|
const label = cPath.get("label");
|
|
|
|
if (label.node !== null) {
|
|
// labels are fn scoped and not accessible by inner functions
|
|
// path is the switch statement
|
|
if (!isSameFunctionScope(path, cPath)) {
|
|
// we don't have to worry about this break statement
|
|
return {
|
|
break: false,
|
|
bail: false
|
|
};
|
|
} // here we handle the break labels
|
|
// if they are outside switch, we bail out
|
|
// if they are within the case, we keep them
|
|
|
|
|
|
let labelPath;
|
|
|
|
if (path.scope.getLabel) {
|
|
labelPath = getLabel(label.node.name, path);
|
|
} else {
|
|
labelPath = path.scope.getBinding(label.node.name).path;
|
|
}
|
|
|
|
const _isAncestor = isAncestor(labelPath, path);
|
|
|
|
return {
|
|
bail: _isAncestor,
|
|
[control]: _isAncestor
|
|
};
|
|
} // set the flag that it is indeed breaking
|
|
|
|
|
|
let isCTransfer = true; // this flag is to capture
|
|
// switch(0) { case 0: while(1) if (x) break; }
|
|
|
|
let possibleRunTimeControlTransfer = false; // and compute if it's breaking the correct thing
|
|
|
|
let parent = cPath.parentPath;
|
|
|
|
while (parent !== stmt.parentPath) {
|
|
// loops and nested switch cases
|
|
if (parent.isLoop() || parent.isSwitchCase()) {
|
|
// invalidate all the possible runtime breaks captured
|
|
// while (1) { if (x) break; }
|
|
possibleRunTimeControlTransfer = false; // and set that it's not breaking our switch statement
|
|
|
|
isCTransfer = false;
|
|
break;
|
|
} //
|
|
// this is a special case and depends on
|
|
// the fact that SwitchStatement is handled in the
|
|
// exit hook of the traverse
|
|
//
|
|
// switch (0) {
|
|
// case 0: if (x) break;
|
|
// }
|
|
//
|
|
// here `x` is runtime only.
|
|
// in this case, we need to bail out. So we depend on exit hook
|
|
// of switch so that, it would have visited the IfStatement first
|
|
// before the SwitchStatement and would have removed the
|
|
// IfStatement if it was a compile time determined
|
|
//
|
|
|
|
|
|
if (parent.isIfStatement()) {
|
|
possibleRunTimeControlTransfer = true;
|
|
}
|
|
|
|
parent = parent.parentPath;
|
|
}
|
|
|
|
return {
|
|
[control]: possibleRunTimeControlTransfer || isCTransfer,
|
|
bail: possibleRunTimeControlTransfer
|
|
};
|
|
}
|
|
} // things that are hoisted
|
|
|
|
|
|
function canExistAfterCompletion(path) {
|
|
return path.isFunctionDeclaration() || path.isVariableDeclaration({
|
|
kind: "var"
|
|
});
|
|
}
|
|
|
|
function getLabel(name, _path) {
|
|
let label,
|
|
path = _path;
|
|
|
|
do {
|
|
label = path.scope.getLabel(name);
|
|
|
|
if (label) {
|
|
return label;
|
|
}
|
|
} while (path = path.parentPath);
|
|
|
|
return null;
|
|
}
|
|
|
|
function hasLoopParent(path) {
|
|
let parent = path;
|
|
|
|
do {
|
|
if (parent.isLoop()) {
|
|
return true;
|
|
}
|
|
} while (parent = parent.parentPath);
|
|
|
|
return false;
|
|
}
|
|
|
|
function extractSequenceImpure(seq) {
|
|
const expressions = seq.get("expressions");
|
|
const result = [];
|
|
|
|
for (let i = 0; i < expressions.length; i++) {
|
|
if (!expressions[i].isPure()) {
|
|
result.push(expressions[i].node);
|
|
}
|
|
}
|
|
|
|
return t.sequenceExpression(result);
|
|
}
|
|
}; |