diff --git a/packages/babel-plugin-minify-mangle-names/src/bfs-traverse.js b/packages/babel-plugin-minify-mangle-names/src/bfs-traverse.js new file mode 100644 index 000000000..439f8a1b2 --- /dev/null +++ b/packages/babel-plugin-minify-mangle-names/src/bfs-traverse.js @@ -0,0 +1,48 @@ +"use strict"; + +module.exports = function bfsTraverseCreator({ types: t, traverse }) { + function getFields(path) { + return t.VISITOR_KEYS[path.type]; + } + + return function bfsTraverse(path, _visitor) { + if (!path.node) { + throw new Error("Not a valid path"); + } + const visitor = traverse.explode(_visitor); + + const queue = [path]; + let current; // current depth + + while (queue.length > 0) { + current = queue.shift(); + + // call + if ( + visitor && + visitor[current.type] && + Array.isArray(visitor[current.type].enter) + ) { + const fns = visitor[current.type].enter; + for (const fn of fns) { + if (typeof fn === "function") fn(current); + } + } + + const fields = getFields(current); + + for (const field of fields) { + const child = current.get(field); + + if (Array.isArray(child)) { + // visit container left to right + for (const c of child) { + if (c.node) queue.push(c); + } + } else { + if (child.node) queue.push(child); + } + } + } + }; +}; diff --git a/packages/babel-plugin-minify-mangle-names/src/index.js b/packages/babel-plugin-minify-mangle-names/src/index.js index ae318d684..5120bc48c 100644 --- a/packages/babel-plugin-minify-mangle-names/src/index.js +++ b/packages/babel-plugin-minify-mangle-names/src/index.js @@ -1,6 +1,7 @@ const Charset = require("./charset"); const ScopeTracker = require("./scope-tracker"); const isLabelIdentifier = require("./is-label-identifier"); +const bfsTraverseCreator = require("./bfs-traverse"); const { markEvalScopes, @@ -8,7 +9,9 @@ const { hasEval } = require("babel-helper-mark-eval-scopes"); -module.exports = ({ types: t, traverse }) => { +module.exports = babel => { + const { types: t, traverse } = babel; + const bfsTraverse = bfsTraverseCreator(babel); const hop = Object.prototype.hasOwnProperty; class Mangler { @@ -20,8 +23,7 @@ module.exports = ({ types: t, traverse }) => { keepFnName = false, keepClassName = false, eval: _eval = false, - topLevel = false, - reuse = true + topLevel = false } = {} ) { this.charset = charset; @@ -31,15 +33,15 @@ module.exports = ({ types: t, traverse }) => { this.keepClassName = keepClassName; this.topLevel = topLevel; this.eval = _eval; - this.reuse = reuse; this.visitedScopes = new Set(); - this.scopeTracker = new ScopeTracker({ reuse }); + this.scopeTracker = new ScopeTracker(); this.renamedNodes = new Set(); } run() { this.crawlScope(); + this.fixup(); this.collect(); this.charset.sort(); this.mangle(); @@ -54,6 +56,48 @@ module.exports = ({ types: t, traverse }) => { this.program.scope.crawl(); } + fixup() { + this.program.traverse({ + // this fixes a bug where converting let to var + // doesn't change the binding's scope to function scope + // https://github.com/babel/babel/issues/4818 + VariableDeclaration(path) { + if (path.node.kind !== "var") { + return; + } + const ids = path.getOuterBindingIdentifiers(); + const fnScope = path.scope.getFunctionParent(); + Object.keys(ids).forEach(id => { + const binding = path.scope.getBinding(id); + + if (binding.scope !== fnScope) { + const existingBinding = fnScope.bindings[id]; + if (!existingBinding) { + // move binding to the function scope + fnScope.bindings[id] = binding; + binding.scope = fnScope; + delete binding.scope.bindings[id]; + } else { + // we need a new binding that's valid in both the scopes + // binding.scope and fnScope + const newName = fnScope.generateUid( + binding.scope.generateUid(id) + ); + + // rename binding in the original scope + mangler.rename(binding.scope, binding, id, newName); + + // move binding to fnScope as newName + fnScope.bindings[newName] = binding; + binding.scope = fnScope; + delete binding.scope.bindings[newName]; + } + } + }); + } + }); + } + collect() { const mangler = this; const { scopeTracker } = mangler; @@ -77,62 +121,23 @@ module.exports = ({ types: t, traverse }) => { const binding = scope.getBinding(name); scopeTracker.addReference(scope, binding, name); }, - // this fixes a bug where converting let to var - // doesn't change the binding's scope to function scope - VariableDeclaration: { - enter(path) { - if (path.node.kind !== "var") { + + BindingIdentifier(path) { + if (isLabelIdentifier(path)) return; + + const { scope, node: { name } } = path; + const binding = scope.getBinding(name); + if (!binding) { + if (scope.hasGlobal(name)) return; + if ( + path.parentPath.isExportSpecifier() && + path.parentKey === "exported" + ) { return; } - const ids = path.getOuterBindingIdentifiers(); - const fnScope = path.scope.getFunctionParent(); - Object.keys(ids).forEach(id => { - const binding = path.scope.getBinding(id); - - if (binding.scope !== fnScope) { - const existingBinding = fnScope.bindings[id]; - if (!existingBinding) { - // move binding to the function scope - fnScope.bindings[id] = binding; - binding.scope = fnScope; - delete binding.scope.bindings[id]; - } else { - // we need a new binding that's valid in both the scopes - // binding.scope and fnScope - const newName = fnScope.generateUid( - binding.scope.generateUid(id) - ); - - // rename binding in the original scope - mangler.rename(binding.scope, binding, id, newName); - - // move binding to fnScope as newName - fnScope.bindings[newName] = binding; - binding.scope = fnScope; - delete binding.scope.bindings[newName]; - } - } - }); - } - }, - BindingIdentifier: { - exit(path) { - if (isLabelIdentifier(path)) return; - const { scope, node: { name } } = path; - const binding = scope.getBinding(name); - if (!binding) { - if (scope.hasGlobal(name)) return; - if ( - path.parentPath.isExportSpecifier() && - path.parentKey === "exported" - ) { - return; - } - console.log(scope.globals); - throw new Error("binding not found " + name); - } - scopeTracker.addBinding(binding); + throw new Error("binding not found " + name); } + scopeTracker.addBinding(binding); } }; @@ -152,7 +157,7 @@ module.exports = ({ types: t, traverse }) => { }; } - mangler.program.traverse(collectVisitor); + bfsTraverse(mangler.program, collectVisitor); } isExportedWithName(binding) { @@ -237,9 +242,8 @@ module.exports = ({ types: t, traverse }) => { !scopeTracker.canUseInReferencedScopes(binding, next) ); - if (mangler.reuse) { - resetNext(); - } + resetNext(); + mangler.rename(scope, binding, oldName, next); } } @@ -247,13 +251,10 @@ module.exports = ({ types: t, traverse }) => { mangle() { const mangler = this; - if (mangler.topLevel) { - mangler.mangleScope(this.program.scope); - } - - this.program.traverse({ + bfsTraverse(this.program, { Scopable(path) { - mangler.mangleScope(path.scope); + if (!path.isProgram() || mangler.topLevel) + mangler.mangleScope(path.scope); } }); } diff --git a/packages/babel-plugin-minify-mangle-names/src/scope-tracker.js b/packages/babel-plugin-minify-mangle-names/src/scope-tracker.js index 54675282d..1b7c52ba4 100644 --- a/packages/babel-plugin-minify-mangle-names/src/scope-tracker.js +++ b/packages/babel-plugin-minify-mangle-names/src/scope-tracker.js @@ -5,20 +5,18 @@ const isLabelIdentifier = require("./is-label-identifier"); * Scope - References, Bindings */ module.exports = class ScopeTracker { - constructor({ reuse }) { - this.references = new Map; - this.bindings = new Map; - - this.reuse = reuse; + constructor() { + this.references = new Map(); + this.bindings = new Map(); } // Register a new Scope and initiliaze it with empty sets addScope(scope) { if (!this.references.has(scope)) { - this.references.set(scope, new CountedSet); + this.references.set(scope, new CountedSet()); } if (!this.bindings.has(scope)) { - this.bindings.set(scope, new Map); + this.bindings.set(scope, new Map()); } } @@ -36,13 +34,10 @@ module.exports = class ScopeTracker { if (binding && binding.scope === parent) { break; } - } while (parent = parent.parent); + } while ((parent = parent.parent)); } hasReference(scope, name) { - if (!this.reuse) { - return scope.hasReference(name); - } if (!this.references.has(scope)) { this.addScope(scope); this.updateScope(scope); @@ -108,7 +103,7 @@ module.exports = class ScopeTracker { if (binding.scope === parent) { break; } - } while (parent = parent.parent); + } while ((parent = parent.parent)); } addBinding(binding) { @@ -122,9 +117,6 @@ module.exports = class ScopeTracker { } hasBinding(scope, name) { - if (!this.reuse) { - return scope.hasBinding(name); - } return this.bindings.get(scope).has(name); } @@ -142,7 +134,9 @@ module.exports = class ScopeTracker { // with a throw statement. This helps in understanding where it // happens to debug it. updateScope(scope) { - throw new Error("Tracker received a scope it doesn't know about yet. Please report this - https://github.com/babel/babili/issues/new"); + throw new Error( + "Tracker received a scope it doesn't know about yet. Please report this - https://github.com/babel/babili/issues/new" + ); const tracker = this; scope.path.traverse({