diff --git a/packages/babel-helper-evaluate-path/src/index.js b/packages/babel-helper-evaluate-path/src/index.js index 55bfb892a..4247d3f4d 100644 --- a/packages/babel-helper-evaluate-path/src/index.js +++ b/packages/babel-helper-evaluate-path/src/index.js @@ -1,4 +1,37 @@ +"use strict"; + module.exports = function evaluate(path) { + if (path.isReferencedIdentifier()) { + return evaluateIdentifier(path); + } + + const state = { + confident: true + }; + + // prepare + path.traverse({ + Scope(scopePath) { + scopePath.skip(); + }, + ReferencedIdentifier(idPath) { + const binding = idPath.scope.getBinding(idPath.node.name); + // don't deopt globals + // let babel take care of it + if (!binding) return; + + const evalResult = evaluateIdentifier(idPath); + if (!evalResult.confident) { + state.confident = evalResult.confident; + state.deoptPath = evalResult.deoptPath; + } + } + }); + + if (!state.confident) { + return state; + } + try { return path.evaluate(); } catch (e) { @@ -8,3 +41,166 @@ module.exports = function evaluate(path) { }; } }; + +// Original Source: +// https://github.com/babel/babel/blob/master/packages/babel-traverse/src/path/evaluation.js +// modified for Babili use +function evaluateIdentifier(path) { + if (!path.isReferencedIdentifier()) { + throw new Error(`Expected ReferencedIdentifier. Got ${path.type}`); + } + + const { node } = path; + + const binding = path.scope.getBinding(node.name); + + if (!binding) { + return deopt(path); + } + + if (binding.constantViolations.length > 0) { + return deopt(binding.path); + } + + // referenced in a different scope - deopt + if (shouldDeoptBasedOnScope(binding, path)) { + return deopt(path); + } + + // let/var/const referenced before init + // or "var" referenced in an outer scope + const flowEvalResult = evaluateBasedOnControlFlow(binding, path); + + if (flowEvalResult.confident) { + return flowEvalResult; + } + + if (flowEvalResult.shouldDeopt) { + return deopt(path); + } + + return path.evaluate(); +} + +// check if referenced in a different fn scope +// we can't determine if this function is called sync or async +// if the binding is in program scope +// all it's references inside a different function should be deopted +function shouldDeoptBasedOnScope(binding, refPath) { + if (binding.scope.path.isProgram() && refPath.scope !== binding.scope) { + return true; + } + return false; +} + +function evaluateBasedOnControlFlow(binding, refPath) { + if (binding.kind === "var") { + // early-exit + const declaration = binding.path.parentPath; + if ( + declaration.parentPath.isIfStatement() || + declaration.parentPath.isLoop() || + declaration.parentPath.isSwitchCase() + ) { + return { shouldDeopt: true }; + } + + let blockParent = binding.path.scope.getBlockParent().path; + const fnParent = binding.path.getFunctionParent(); + + if (blockParent === fnParent) { + if (!fnParent.isProgram()) blockParent = blockParent.get("body"); + } + + // detect Usage Outside Init Scope + if (!blockParent.get("body").some(stmt => stmt.isAncestor(refPath))) { + return { shouldDeopt: true }; + } + + // Detect usage before init + const stmts = fnParent.isProgram() + ? fnParent.get("body") + : fnParent.get("body").get("body"); + + const compareResult = compareBindingAndReference({ + binding, + refPath, + stmts + }); + + if (compareResult.reference && compareResult.binding) { + if ( + compareResult.reference.scope === "current" && + compareResult.reference.idx < compareResult.binding.idx + ) { + return { confident: true, value: void 0 }; + } + + return { shouldDeopt: true }; + } + } else if (binding.kind === "let" || binding.kind === "const") { + // binding.path is the declarator + const declarator = binding.path; + let scopePath = declarator.scope.path; + if (scopePath.isFunction()) { + scopePath = scopePath.get("body"); + } + + // Detect Usage before Init + const stmts = scopePath.get("body"); + + const compareResult = compareBindingAndReference({ + binding, + refPath, + stmts + }); + + if (compareResult.reference && compareResult.binding) { + if ( + compareResult.reference.scope === "current" && + compareResult.reference.idx < compareResult.binding.idx + ) { + throw new Error( + `ReferenceError: Used ${refPath.node.name}: ` + + `${binding.kind} binding before declaration` + ); + } + if (compareResult.reference.scope === "other") { + return { shouldDeopt: true }; + } + } + } + + return { confident: false, shouldDeopt: false }; +} + +function compareBindingAndReference({ binding, refPath, stmts }) { + const state = { + binding: null, + reference: null + }; + + for (const [idx, stmt] of stmts.entries()) { + if (stmt.isAncestor(binding.path)) { + state.binding = { idx }; + } + for (const ref of binding.referencePaths) { + if (ref === refPath && stmt.isAncestor(ref)) { + state.reference = { + idx, + scope: binding.path.scope === ref.scope ? "current" : "other" + }; + break; + } + } + } + + return state; +} + +function deopt(deoptPath) { + return { + confident: false, + deoptPath + }; +} diff --git a/packages/babel-plugin-minify-builtins/__tests__/__snapshots__/minify-builtins.js.snap b/packages/babel-plugin-minify-builtins/__tests__/__snapshots__/minify-builtins.js.snap index 444173407..2873bc632 100644 --- a/packages/babel-plugin-minify-builtins/__tests__/__snapshots__/minify-builtins.js.snap +++ b/packages/babel-plugin-minify-builtins/__tests__/__snapshots__/minify-builtins.js.snap @@ -85,7 +85,7 @@ new A();", exports[`minify-builtins should collect and minify no matter any depth 1`] = ` Object { "_source": "function a (){ - Math.max(b, a); + Math.max(c, a); const b = () => { const a = Math.floor(c); Math.min(b, a) * Math.floor(b); @@ -95,7 +95,7 @@ Object { } }", "expected": "function a() { - Math.max(b, a); + Math.max(c, a); const b = () => { var _Mathmin = Math.min; var _Mathfloor = Math.floor; @@ -170,8 +170,8 @@ exports[`minify-builtins should minify builtins to method scope for class declar Object { "_source": "class Test { foo() { - Math.max(c, d) - Math.max(c, d) + Math.max(a, d) + Math.max(a, d) const c = function() { Math.max(c, d) Math.floor(m); @@ -187,8 +187,8 @@ Object { foo() { var _Mathmax = Math.max; - _Mathmax(c, d); - _Mathmax(c, d); + _Mathmax(a, d); + _Mathmax(a, d); const c = function () { var _Mathfloor = Math.floor; diff --git a/packages/babel-plugin-minify-builtins/__tests__/minify-builtins.js b/packages/babel-plugin-minify-builtins/__tests__/minify-builtins.js index 344396c78..7837a6e47 100644 --- a/packages/babel-plugin-minify-builtins/__tests__/minify-builtins.js +++ b/packages/babel-plugin-minify-builtins/__tests__/minify-builtins.js @@ -40,7 +40,7 @@ describe("minify-builtins", () => { "should collect and minify no matter any depth", ` function a (){ - Math.max(b, a); + Math.max(c, a); const b = () => { const a = Math.floor(c); Math.min(b, a) * Math.floor(b); @@ -68,8 +68,8 @@ describe("minify-builtins", () => { ` class Test { foo() { - Math.max(c, d) - Math.max(c, d) + Math.max(a, d) + Math.max(a, d) const c = function() { Math.max(c, d) Math.floor(m); diff --git a/packages/babel-plugin-minify-dead-code-elimination/__tests__/dead-code-elimination-test.js b/packages/babel-plugin-minify-dead-code-elimination/__tests__/dead-code-elimination-test.js index c4b5ab9e3..26d0ef427 100644 --- a/packages/babel-plugin-minify-dead-code-elimination/__tests__/dead-code-elimination-test.js +++ b/packages/babel-plugin-minify-dead-code-elimination/__tests__/dead-code-elimination-test.js @@ -518,59 +518,41 @@ describe("dce-plugin", () => { ); thePlugin( - "should handle orpahaned returns", + "should handle orphaned returns", ` - var a = true; - function foo() { - if (a) return; - x(); - } - `, + var a = true; + function foo() { + if (a) return; + x(); + } ` - var a = true; - function foo() {} - ` ); thePlugin( "should handle orpahaned returns with a value", ` - var a = true; - function foo() { - if (a) return 1; - x(); - } - `, + var a = true; + function foo() { + if (a) return 1; + x(); + } ` - var a = true; - function foo() { - return 1; - } - ` ); thePlugin( "should handle orphaned, redundant returns", ` - var x = true; - function foo() { - if (b) { - if (x) { - z(); - return; + var x = true; + function foo() { + if (b) { + if (x) { + z(); + return; + } + y(); } - y(); } - } - `, ` - var x = true; - function foo() { - if (b) { - z(); - } - } - ` ); thePlugin( @@ -2472,4 +2454,35 @@ describe("dce-plugin", () => { } ` ); + + thePlugin.skip( + "should optimize to void 0 for lets referenced before init declarations", + ` + function foo() { + bar(a); // Should be a ReferenceError + let a = 1; + } + ` + ); + + thePlugin( + "should optimize lets referenced before init declarations - 2", + ` + function foo() { + function bar() { + if (a) console.log(a); + } + let a = 1; + return bar; + } + `, + ` + function foo() { + let a = 1; + return function () { + if (a) console.log(a); + }; + } + ` + ); }); diff --git a/packages/babel-plugin-minify-dead-code-elimination/package.json b/packages/babel-plugin-minify-dead-code-elimination/package.json index 332301d99..8b97fd4d3 100644 --- a/packages/babel-plugin-minify-dead-code-elimination/package.json +++ b/packages/babel-plugin-minify-dead-code-elimination/package.json @@ -8,10 +8,9 @@ "author": "amasad", "license": "MIT", "main": "lib/index.js", - "keywords": [ - "babel-plugin" - ], + "keywords": ["babel-plugin"], "dependencies": { + "babel-helper-evaluate-path": "^0.1.0", "babel-helper-mark-eval-scopes": "^0.1.1", "babel-helper-remove-or-void": "^0.1.1", "lodash.some": "^4.6.0" diff --git a/packages/babel-plugin-minify-dead-code-elimination/src/index.js b/packages/babel-plugin-minify-dead-code-elimination/src/index.js index 73c3abf77..e882b7b94 100644 --- a/packages/babel-plugin-minify-dead-code-elimination/src/index.js +++ b/packages/babel-plugin-minify-dead-code-elimination/src/index.js @@ -3,6 +3,7 @@ const some = require("lodash.some"); const { markEvalScopes, hasEval } = require("babel-helper-mark-eval-scopes"); const removeUseStrict = require("./remove-use-strict"); +const evaluate = require("babel-helper-evaluate-path"); function prevSiblings(path) { const parentPath = path.parentPath; @@ -508,7 +509,7 @@ module.exports = ({ types: t, traverse }) => { SwitchStatement: { exit(path) { - const evaluated = path.get("discriminant").evaluate(); + const evaluated = evaluate(path.get("discriminant")); if (!evaluated.confident) return; @@ -527,7 +528,7 @@ module.exports = ({ types: t, traverse }) => { continue; } - const testResult = test.evaluate(); + const testResult = evaluate(test); // if we are not able to deternine a test during // compile time, we terminate immediately @@ -613,7 +614,7 @@ module.exports = ({ types: t, traverse }) => { WhileStatement(path) { const test = path.get("test"); - const result = test.evaluate(); + const result = evaluate(test); if (result.confident && test.isPure() && !result.value) { path.remove(); } @@ -623,7 +624,7 @@ module.exports = ({ types: t, traverse }) => { const test = path.get("test"); if (!test.isPure()) return; - const result = test.evaluate(); + const result = evaluate(test); if (result.confident) { if (result.value) { test.remove(); @@ -635,7 +636,7 @@ module.exports = ({ types: t, traverse }) => { DoWhileStatement(path) { const test = path.get("test"); - const result = test.evaluate(); + const result = evaluate(test); if (result.confident && test.isPure() && !result.value) { const body = path.get("body"); @@ -771,41 +772,9 @@ module.exports = ({ types: t, traverse }) => { const alternate = path.get("alternate"); const test = path.get("test"); - const evalResult = test.evaluate(); + const evalResult = evaluate(test); const isPure = test.isPure(); - const binding = path.scope.getBinding(test.node.name); - - // Ref - https://github.com/babel/babili/issues/574 - // deopt if var is declared in other scope - // if (a) { var b = blahl;} if (b) { //something } - if ( - binding && - binding.path.parentPath.isVariableDeclaration({ kind: "var" }) - ) { - let ifStatementParent = null; - - const fnParent = - binding.path.getFunctionParent() || - binding.path.getProgramParent(); - - forEachAncestor(binding.path.parentPath, parent => { - if (fnParent === parent) return; - if (parent.isIfStatement()) { - ifStatementParent = parent; - } - }); - - if ( - ifStatementParent && - binding.referencePaths.some( - ref => !ref.isDescendant(ifStatementParent) - ) - ) { - return; - } - } - const replacements = []; if (evalResult.confident && !isPure && test.isSequenceExpression()) { replacements.push(