"use strict"; function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; } function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; } 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 evaluateTruthy(path) { const res = evaluate(path); if (res.confident) return !!res.value; } 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 _iterator = _createForOfIteratorHelper(declarPath.node.declarations), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { 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) { _iterator.e(err); } finally { _iterator.f(); } if (assignmentSequence.length) { mutations.push(() => declarPath.replaceWith(t.sequenceExpression(assignmentSequence))); } else { mutations.push(() => removeOrVoid(declarPath)); } } if (declars.length) { mutations.forEach(f => f()); var _iterator2 = _createForOfIteratorHelper(node.body.body), _step2; try { for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { const statement = _step2.value; if (t.isVariableDeclaration(statement)) { statement.declarations.push(...declars); return; } } } catch (err) { _iterator2.e(err); } finally { _iterator2.f(); } 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()) { if (t.isArrayPattern(p.node.left) || t.isObjectPattern(p.node.left)) { bail = true; } else if (p.get("right").isPure()) { mutations.push(() => removeOrVoid(p)); } else { mutations.push(() => p.replaceWith(p.get("right"))); } } }); 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 _iterator3 = _createForOfIteratorHelper(binding.referencePaths), _step3; try { for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) { const ref = _step3.value; if (!ref.find(p => p.node === fun.node)) { allInside = false; break; } } } catch (err) { _iterator3.e(err); } finally { _iterator3.f(); } 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 if (replacementPath.isThisExpression()) { bail = true; } else { replacementPath.traverse({ Function(path) { path.skip(); }, ThisExpression(path) { bail = true; path.stop(); }, ReferencedIdentifier({ node }) { const binding = scope.getBinding(node.name); if (binding && refPath.scope.getBinding(node.name) === binding) { bail = binding.constantViolations.length > 0; if (bail) { path.stop(); } } } }); } 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) || t.isRegExpLiteral(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 = evaluateTruthy(path.get("test")); if (evaluateTest === true) { path.replaceWith(node.consequent); } else if (evaluateTest === false) { path.replaceWith(node.alternate); } }, SwitchStatement: { exit(path) { const discriminantPath = path.get("discriminant"); const evaluated = evaluate(discriminantPath, { tdz: this.tdz }); if (!evaluated.confident) return; // the simplify transformation might have brought in the previous // expressions into the switch's test expression and instead of // bailing out of impure path, we collect the impurities of it's // a sequence expression and bail out if the primary test itself // is impure let beforeTest = []; if (t.isSequenceExpression(discriminantPath.node)) { const expressions = discriminantPath.get("expressions"); const lastExpression = expressions[expressions.length - 1]; if (!lastExpression.isPure()) { return; } beforeTest = [t.expressionStatement(t.sequenceExpression(expressions.slice(0, expressions.length - 1).map(path => path.node)))]; } else if (!discriminantPath.isPure()) { 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), ...beforeTest, ...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.replaceWithMultiple(extractVars(path.get("body"))); } }, 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 { const init = path.get("init"); if (init.node && !init.isPure()) { path.replaceWith(init); } 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 _iterator4 = _createForOfIteratorHelper(stmts), _step4; try { for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) { 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) { _iterator4.e(err); } finally { _iterator4.f(); } 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 _iterator5 = _createForOfIteratorHelper(path.node.declarations), _step5; try { for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) { const decl = _step5.value; const bindingIds = Object.keys(t.getBindingIdentifiers(decl.id)); declarators.push(...bindingIds.map(name => t.variableDeclarator(t.identifier(name)))); } } catch (err) { _iterator5.e(err); } finally { _iterator5.f(); } } else { path.traverse({ VariableDeclaration(varPath) { if (!varPath.isVariableDeclaration({ kind: "var" })) return; if (!isSameFunctionScope(varPath, path)) return; var _iterator6 = _createForOfIteratorHelper(varPath.node.declarations), _step6; try { for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) { const decl = _step6.value; const bindingIds = Object.keys(t.getBindingIdentifiers(decl.id)); declarators.push(...bindingIds.map(name => t.variableDeclarator(t.identifier(name)))); } } catch (err) { _iterator6.e(err); } finally { _iterator6.f(); } } }); } 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); } };