From 0a36f1cdb59c0fde3c5e7f403fca836fdc3c2372 Mon Sep 17 00:00:00 2001 From: Boopathi Rajaa Date: Sat, 5 Nov 2016 10:51:14 +0100 Subject: [PATCH] Reuse removed vars in mangler ResetNext identifier only when reuse is true Fix tests - add keepClassName Reuse vars as default I dont know why it works - #326 Extract tracker to a separate file, Add topLevel Option --- .../__tests__/mangle-names-reuse-test.js | 1342 +++++++++++++++++ .../__tests__/mangle-names-test.js | 74 +- .../src/charset.js | 51 + .../src/counted-set.js | 28 + .../src/index.js | 323 ++-- .../src/is-label-identifier.js | 8 + .../src/scope-tracker.js | 157 ++ .../__tests__/preset-tests.js | 48 +- 8 files changed, 1877 insertions(+), 154 deletions(-) create mode 100644 packages/babel-plugin-minify-mangle-names/__tests__/mangle-names-reuse-test.js create mode 100644 packages/babel-plugin-minify-mangle-names/src/charset.js create mode 100644 packages/babel-plugin-minify-mangle-names/src/counted-set.js create mode 100644 packages/babel-plugin-minify-mangle-names/src/is-label-identifier.js create mode 100644 packages/babel-plugin-minify-mangle-names/src/scope-tracker.js diff --git a/packages/babel-plugin-minify-mangle-names/__tests__/mangle-names-reuse-test.js b/packages/babel-plugin-minify-mangle-names/__tests__/mangle-names-reuse-test.js new file mode 100644 index 000000000..58ccd9741 --- /dev/null +++ b/packages/babel-plugin-minify-mangle-names/__tests__/mangle-names-reuse-test.js @@ -0,0 +1,1342 @@ +jest.autoMockOff(); + +const traverse = require("babel-traverse").default; +const babel = require("babel-core"); +const unpad = require("../../../utils/unpad"); + +function transform(code, options = {}, sourceType = "script") { + options.reuse = true; + return babel.transform(code, { + sourceType, + plugins: [ + [require("../src/index"), options], + ], + }).code; +} + +function transformWithSimplify(code, options = {}, sourceType = "script") { + options.reuse = true; + return babel.transform(code, { + sourceType, + plugins: [ + require("../../babel-plugin-minify-simplify/src/index"), + [require("../src/index"), options] + ] + }).code; +} + +describe("mangle-names", () => { + it("should not mangle names in the global namespace", () => { + const source = unpad(` + var Foo = 1; + `); + const expected = unpad(` + var Foo = 1; + `); + + expect(transform(source)).toBe(expected); + }); + + it("should mangle names", () => { + const source = unpad(` + function foo() { + var xxx = 1; + if (xxx) { + console.log(xxx); + } + } + `); + const expected = unpad(` + function foo() { + var a = 1; + if (a) { + console.log(a); + } + } + `); + + expect(transform(source)).toBe(expected); + }); + + it("should handle name collisions", () => { + const source = unpad(` + function foo() { + var x = 2; + var xxx = 1; + if (xxx) { + console.log(xxx + x); + } + } + `); + const expected = unpad(` + function foo() { + var a = 2; + var b = 1; + if (b) { + console.log(b + a); + } + } + `); + + expect(transform(source)).toBe(expected); + }); + + it("should be fine with shadowing", () => { + const source = unpad(` + var a = 1; + function foo() { + var xxx = 1; + if (xxx) { + console.log(xxx); + } + } + `); + const expected = unpad(` + var a = 1; + function foo() { + var a = 1; + if (a) { + console.log(a); + } + } + `); + + expect(transform(source)).toBe(expected); + }); + + it("should not shadow outer references", () => { + const source = unpad(` + function bar() { + function foo(a, b, c) { + lol(a,b,c); + } + + function lol() {} + } + `); + const expected = unpad(` + function bar() { + function a(d, a, e) { + b(d, a, e); + } + + function b() {} + } + `); + + expect(transform(source)).toBe(expected); + }); + + it("should mangle args", () => { + const source = unpad(` + function foo(xxx) { + if (xxx) { + console.log(xxx); + } + } + `); + const expected = unpad(` + function foo(a) { + if (a) { + console.log(a); + } + } + `); + + expect(transform(source)).toBe(expected); + }); + + it("should ignore labels", () => { + const source = unpad(` + function foo() { + meh: for (;;) { + continue meh; + } + } + `); + + const expected = unpad(` + function foo() { + meh: for (;;) { + continue meh; + } + } + `); + + expect(transform(source)).toBe(expected); + }); + + it("should not have labels conflicting with bindings", () => { + const source = unpad(` + function foo() { + meh: for (;;) { + var meh; + break meh; + } + } + `); + + const expected = unpad(` + function foo() { + meh: for (;;) { + var a; + break meh; + } + } + `); + + expect(transform(source)).toBe(expected); + }); + + // https://phabricator.babeljs.io/T6957 + it("labels should not shadow bindings", () => { + const source = unpad(` + function foo() { + var meh; + meh: for (;;) { + break meh; + } + return meh; + } + `); + + const expected = unpad(` + function foo() { + var a; + meh: for (;;) { + break meh; + } + return a; + } + `); + + expect(transform(source)).toBe(expected); + }); + + it("labels should not shadow bindings 2", () => { + const source = unpad(` + function f(a) { + try { + a: { + console.log(a); + } + } catch ($a) { } + } + `); + const expected = unpad(` + function f(b) { + try { + a: { + console.log(b); + } + } catch (a) {} + } + `); + expect(transform(source)).toBe(expected); + }); + + it("should be order independent", () => { + const source = unpad(` + function foo() { + function bar(aaa, bbb, ccc) { + baz(aaa, bbb, ccc); + } + function baz() { + var baz = who(); + baz.bam(); + } + bar(); + } + `); + + const expected = unpad(` + function foo() { + function a(a, c, d) { + b(a, c, d); + } + function b() { + var a = who(); + a.bam(); + } + a(); + } + `); + + expect(transform(source)).toBe(expected); + }); + + it("should be order independent 2", () => { + const source = unpad(` + function foo() { + (function bar() { + bar(); + return function() { + var bar = wow(); + bar.woo(); + }; + })(); + } + `); + + const expected = unpad(` + function foo() { + (function a() { + a(); + return function () { + var a = wow(); + a.woo(); + }; + })(); + } + `); + + expect(transform(source)).toBe(expected); + }); + + it("should handle only think in function scopes", () => { + const source = unpad(` + function foo() { + function xx(bar, baz) { + if (1) { + yy(bar, baz); + } + } + function yy(){} + } + `); + const expected = unpad(` + function foo() { + function a(a, c) { + if (1) { + b(a, c); + } + } + function b() {} + } + `); + + expect(transform(source)).toBe(expected); + }); + + it("should be fine with shadowing 2", () => { + const source = unpad(` + function foo() { + function xx(bar, baz) { + return function(boo, foo) { + bar(boo, foo); + }; + } + function yy(){} + } + `); + const expected = unpad(` + function foo() { + function a(a, b) { + return function (b, c) { + a(b, c); + }; + } + function b() {} + } + `); + + expect(transform(source)).toBe(expected); + }); + + it("should not be confused by scopes", () => { + const source = unpad(` + function foo() { + function bar() { + var baz; + if (baz) { + bam(); + } + } + function bam() {} + } + `); + const expected = unpad(` + function foo() { + function a() { + var a; + if (a) { + b(); + } + } + function b() {} + } + `); + + expect(transform(source)).toBe(expected); + }); + + it("should not be confused by scopes (closures)", () => { + const source = unpad(` + function foo() { + function bar(baz) { + return function() { + bam(); + }; + } + function bam() {} + } + `); + const expected = unpad(` + function foo() { + function a(a) { + return function () { + b(); + }; + } + function b() {} + } + `); + + expect(transform(source)).toBe(expected); + }); + + it("should handle recursion", () => { + const source = unpad(` + function bar() { + function foo(a, b, c) { + foo(a,b,c); + } + } + `); + const expected = unpad(` + function bar() { + function a(d, e, b) { + a(d, e, b); + } + } + `); + + expect(transform(source)).toBe(expected); + }); + + it("should handle global name conflict", () => { + const source = unpad(` + function e() { + function foo() { + b = bar(); + } + function bar() {} + } + `); + const expected = unpad(` + function e() { + function a() { + b = c(); + } + function c() {} + } + `); + + expect(transform(source)).toBe(expected); + }); + + it("should handle global name", () => { + const source = unpad(` + function foo() { + var bar = 1; + var baz = 2; + } + `); + + const expected = unpad(` + function foo() { + var bar = 1; + var a = 2; + } + `); + expect(transform(source, { blacklist: {foo: true, bar: true }})).toBe(expected); + }); + + it("should handle deeply nested paths with no bindings", () => { + const source = unpad(` + function xoo() { + function foo(zz, xx, yy) { + function bar(zip, zap, zop) { + return function(bar) { + zap(); + return function() { + zip(); + } + } + } + } + } + `); + const expected = unpad(` + function xoo() { + function a(a, b, c) { + function d(a, b, c) { + return function (c) { + b(); + return function () { + a(); + }; + }; + } + } + } + `); + expect(transform(source)).toBe(expected); + }); + + it("should handle try/catch", () => { + const source = unpad(` + function xoo() { + var e; + try {} catch (e) { + + } + } + `); + const expected = unpad(` + function xoo() { + var a; + try {} catch (a) {} + } + `); + expect(transform(source)).toBe(expected); + }); + + it("should not mangle vars in scope with eval", () => { + const source = unpad(` + function foo() { + var inScopeOuter = 1; + (function () { + var inScopeInner = 2; + eval("inScopeInner + inScopeOuter"); + (function () { + var outOfScope = 1; + })(); + })(); + } + `); + const expected = unpad(` + function foo() { + var inScopeOuter = 1; + (function () { + var inScopeInner = 2; + eval("inScopeInner + inScopeOuter"); + (function () { + var a = 1; + })(); + })(); + } + `); + expect(transform(source)).toBe(expected); + }); + + it("should mangle names with local eval bindings", () => { + const source = unpad(` + function eval() {} + function foo() { + var bar = 1; + eval('...'); + } + `); + const expected = unpad(` + function eval() {} + function foo() { + var a = 1; + eval('...'); + } + `); + expect(transform(source)).toBe(expected); + }); + + it("should mangle names with option eval = true", () => { + const source = unpad(` + function foo() { + var inScopeOuter = 1; + (function () { + var inScopeInner = 2; + eval("..."); + (function () { + var outOfScope = 1; + })(); + })(); + } + `); + const expected = unpad(` + function foo() { + var a = 1; + (function () { + var a = 2; + eval("..."); + (function () { + var a = 1; + })(); + })(); + } + `); + expect(transform(source, { eval: true })).toBe(expected); + }); + + it("should integrate with block scoping plugin", () => { + const srcTxt = unpad(` + function f(x) { + for (let i = 0; i; i++) { + let n; + if (n) { + return; + } + g(() => n); + } + } + `); + + const first = babel.transform(srcTxt, { + plugins: ["transform-es2015-block-scoping"], + code: false, + }); + + traverse.clearCache(); + + const actual = babel.transformFromAst(first.ast, null, { + plugins: [[require("../src/index"), { reuse: true }]], + }).code; + + const expected = unpad(` + function f(a) { + var b = function (a) { + var b = void 0; + if (b) { + return { + v: void 0 + }; + } + g(() => b); + }; + + for (var d = 0; d; d++) { + var c = b(d); + if (typeof c === "object") return c.v; + } + } + `); + + expect(actual).toBe(expected); + }); + + it("should integrate with block scoping plugin 2", () => { + const srcTxt = unpad(` + (function () { + function bar() { + if (smth) { + let entries = blah(); + entries(); + } + foo(); + } + function foo() { } + module.exports = { bar }; + })(); + `); + + const first = babel.transform(srcTxt, { + plugins: ["transform-es2015-block-scoping"], + code: false, + }); + + traverse.clearCache(); + + const actual = babel.transformFromAst(first.ast, null, { + plugins: [[require("../src/index"), { reuse: true }]], + }).code; + + const expected = unpad(` + (function () { + function a() { + if (smth) { + var a = blah(); + a(); + } + b(); + } + function b() {} + module.exports = { bar: a }; + })(); + `); + + expect(actual).toBe(expected); + }); + + it("should keep mangled named consistent across scopes when defined later on", () => { + const source = unpad(` + (function() { + function foo() { + { + var baz = true; + + { + bar(); + } + } + } + + function bar() {} + }()); + `); + + const expected = unpad(` + (function () { + function a() { + { + var a = true; + + { + b(); + } + } + } + + function b() {} + })(); + `); + + expect(transform(source)).toBe(expected); + }); + + it("should correctly mangle in nested loops", () => { + const source = unpad(` + (function () { + for (let x in foo) { + for (let y in foo[x]) { + alert(foo[x][y]); + } + } + })(); + `); + + const expected = unpad(` + (function () { + for (let a in foo) { + for (let b in foo[a]) { + alert(foo[a][b]); + } + } + })(); + `); + + expect(transform(source)).toBe(expected); + }); + + // #issue55, #issue57 + it("should correctly mangle function declarations in different order", () => { + const source = unpad(` + (function(){ + (function() { + for (let x in y) y[x]; + f(() => { g() }); + })(); + function g() {} + })(); + `); + + const ast = babel.transform(source, { + presets: ["env"], + sourceType: "script", + code: false + }).ast; + + traverse.clearCache(); + + const actual = babel.transformFromAst(ast, null, { + sourceType: "script", + plugins: [require("../src/index")] + }).code; + + const expected = unpad(` + "use strict"; + + (function () { + (function () { + for (var b in y) { + y[b]; + }f(function () { + a(); + }); + })(); + function a() {} + })(); + `); + + expect(actual).toBe(expected); + }); + + it("should NOT mangle functions when keepFnName is true", () => { + const source = unpad(` + (function() { + var foo = function foo() { + foo(); + } + function bar() { + foo(); + } + bar(); + var baz = foo; + baz(); + })(); + `); + const expected = unpad(` + (function () { + var a = function foo() { + foo(); + }; + function bar() { + a(); + } + bar(); + var b = a; + b(); + })(); + `); + expect(transform(source, {keepFnName: true})).toBe(expected); + }); + + it("should NOT mangle classes when keepClassName is true", () => { + const source = unpad(` + (function() { + class Foo {} + const Bar = class Bar extends Foo {} + var foo = class Baz {} + function bar() { + new foo(); + } + bar(); + })(); + `); + const expected = unpad(` + (function () { + class Foo {} + const b = class Bar extends Foo {}; + var c = class Baz {}; + function a() { + new c(); + } + a(); + })(); + `); + expect(transform(source, {keepClassName: true})).toBe(expected); + }); + + it("should mangle variable re-declaration / K violations", () => { + const source = unpad(` + !function () { + var foo = 1; + foo++; + var foo = 2; + foo++; + } + `); + const expected = unpad(` + !function () { + var a = 1; + a++; + var a = 2; + a++; + }; + `); + expect(transform(source)).toBe(expected); + }); + + it("should handle K violations - 2", () => { + const source = unpad(` + !function () { + var bar = 1; + bar--; + var bar = 10; + foo(bar) + function foo() { + var foo = 10; + foo++; + var foo = 20; + foo(foo); + } + } + `); + const expected = unpad(` + !function () { + var b = 1; + b--; + var b = 10; + a(b); + function a() { + var a = 10; + a++; + var a = 20; + a(a); + } + }; + `); + expect(transform(source)).toBe(expected); + }); + + it("should work with redeclarations", () => { + const source = unpad(` + (function () { + var x = y; + x = z; + x; + })(); + `); + const expected = unpad(` + (function () { + var a = y; + a = z; + a; + })(); + `); + expect(transform(source)).toBe(expected); + }); + + it("should reuse removed vars", () => { + const source = unpad(` + function Foo() { + var a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z; + var A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z; + var $, _; + a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z; + A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z; + $, _; + function Foo() { + var a, b, c, d, e, f, g, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z; + var A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z; + var $, _; + a, b, c, d, e, f, g, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z; + A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z; + $, _; + function Foo() { + var a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z; + var A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z; + var $, _; + a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z; + A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z; + $, _; + } + Foo(); + } + Foo(); + } + `); + const expected = unpad(` + function Foo() { + var ba, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y; + var z, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y; + var Z, $; + ba, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y; + z, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y; + Z, $; + function aa() { + var aa, a, b, c, d, e, f, g, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y; + var z, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y; + var Z, $; + aa, a, b, c, d, e, f, g, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y; + z, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y; + Z, $; + function h() { + var aa, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y; + var z, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y; + var Z, $; + aa, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y; + z, A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y; + Z, $; + } + h(); + } + aa(); + } + `); + expect(transform(source)).toBe(expected); + }); + + it("should mangle both referenced and binding identifiers with K violations", () => { + const source = unpad(` + (function () { + var foo = bar, + foo = baz; + foo; + })(); + `); + const expected = unpad(` + (function () { + var a = bar, + a = baz; + a; + })(); + `); + expect(transform(source)).toBe(expected); + }); + + it("should handle export declarations", () => { + const source = unpad(` + const foo = 1; + export { foo }; + export const bar = 2; + export function baz(bar, foo) { + bar(); + foo(); + }; + export default function (bar, baz) { + bar(); + baz(); + } + `); + const expected = unpad(` + const foo = 1; + export { foo }; + export const bar = 2; + export function baz(a, b) { + a(); + b(); + }; + export default function (a, b) { + a(); + b(); + } + `); + expect(transform(source, {}, "module")).toBe(expected); + }); + + it("should find global scope properly", () => { + const source = unpad(` + class A {} + class B extends A {} + (function () { + class C { + constructor() { + new A(); + new B(); + C; + } + } + })(); + `); + const expected = unpad(` + class A {} + class B extends A {} + (function () { + class a { + constructor() { + new A(); + new B(); + a; + } + } + })(); + `); + expect(transform(source)).toBe(expected); + }); + + it("should mangle classes properly", () => { + const source = unpad(` + class A {} + class B {} + new A(); + new B(); + function a() { + class A {} + class B {} + new A(); + new B(); + } + `); + const expected = unpad(` + class A {} + class B {} + new A(); + new B(); + function a() { + class a {} + class b {} + new a(); + new b(); + } + `); + expect(transform(source)).toBe(expected); + }); + + // https://github.com/babel/babili/issues/138 + it("should handle class exports in modules - issue#138", () => { + const source = unpad(` + export class App extends Object {}; + `); + const expected = source; + expect(transform(source, {}, "module")).toBe(expected); + }); + + it("should not mangle the name arguments", () => { + const source = unpad(` + (function () { + var arguments = void 0; + (function () { + console.log(arguments); + })("argument"); + })(); + `); + const expected = source; + expect(transform(source)).toBe(expected); + }); + + it("should handle constant violations across multiple blocks", () => { + const source = unpad(` + function foo() { + var x;x;x; + { + var x;x;x; + function y() { + var x;x;x; + { + var x;x;x; + } + } + } + } + `); + const expected = unpad(` + function foo() { + var a;a;a; + { + var a;a;a; + function b() { + var a;a;a; + { + var a;a;a; + } + } + } + } + `); + expect(transform(source)).toBe(expected); + }); + + it("should work with if_return optimization changing fn scope", () => { + const source = unpad(` + function foo() { + if (x) + return; + function bar() {} + bar(a); + } + `); + const expected = unpad(` + function foo() { + function b() {} + x || b(a); + } + `); + expect(transformWithSimplify(source)).toBe(expected); + }); + + it("should fix #326, #369 - destructuring", () => { + const source = unpad(` + // issue#326 + function a() { + let foo, bar, baz; + ({foo, bar, baz} = {}); + return {foo, bar, baz}; + } + // issue#369 + function decodeMessage(message){ + let namespace; + let name; + let value = null; + + [, namespace, name, value] = message.split(',') || []; + console.log(name); + } + `); + const expected = unpad(` + // issue#326 + function a() { + let a, b, c; + ({ foo: a, bar: b, baz: c } = {}); + return { foo: a, bar: b, baz: c }; + } + // issue#369 + function decodeMessage(a) { + let b; + let c; + let d = null; + + [, b, c, d] = a.split(',') || []; + console.log(c); + } + `); + expect(transform(source)).toBe(expected); + }); + + it("should mangle topLevel when topLevel option is true", () => { + const source = unpad(` + function foo() { + if (FOO_ENV === "production") { + HELLO_WORLD.call(); + } + } + const FOO_ENV = "production"; + var HELLO_WORLD = function bar() { + new AbstractClass({ + [FOO_ENV]: "foo", + a: foo(HELLO_WORLD) + }); + }; + class AbstractClass {} + foo(); + `); + + const expected = unpad(` + function a() { + if (b === "production") { + c.call(); + } + } + const b = "production"; + var c = function e() { + new d({ + [b]: "foo", + a: a(c) + }); + }; + class d {} + a(); + `); + + expect(transform(source, { topLevel: true })).toBe(expected); + }); + + it("should fix #326, #369 - destructuring", () => { + const source = unpad(` + // issue#326 + function a() { + let foo, bar, baz; + ({foo, bar, baz} = {}); + return {foo, bar, baz}; + } + // issue#369 + function decodeMessage(message){ + let namespace; + let name; + let value = null; + + [, namespace, name, value] = message.split(',') || []; + console.log(name); + } + `); + const expected = unpad(` + // issue#326 + function a() { + let a, b, c; + ({ foo: a, bar: b, baz: c } = {}); + return { foo: a, bar: b, baz: c }; + } + // issue#369 + function decodeMessage(a) { + let b; + let c; + let d = null; + + [, b, c, d] = a.split(',') || []; + console.log(c); + } + `); + expect(transform(source)).toBe(expected); + }); + + it("should rename binding.identifier - issue#411", () => { + const source = unpad(` + !function () { + function e(e) { + foo(e); + } + return function () { + return e(); + }; + }(); + `); + const expected = unpad(` + !function () { + function a(a) { + foo(a); + } + return function () { + return a(); + }; + }(); + `); + expect(transform(source)).toBe(expected); + }); + + it("should fix issue#365 - classDeclaration with unsafe parent scope", () => { + const source = unpad(` + function foo() { + eval(""); + class A {} + class B {} + } + `); + expect(transform(source)).toBe(source); + }); + + it("should fix classDeclaration with unsafe program scope", () => { + const source = unpad(` + class A {} + class B {} + eval(""); + `); + expect(transform(source, { topLevel: true })).toBe(source); + }); + + it("should handle constant violations across multiple blocks", () => { + const source = unpad(` + function foo() { + var x;x;x; + { + var x;x;x; + function y() { + var x;x;x; + { + var x;x;x; + } + } + } + } + `); + const expected = unpad(` + function foo() { + var a;a;a; + { + var a;a;a; + function b() { + var a;a;a; + { + var a;a;a; + } + } + } + } + `); + expect(transform(source)).toBe(expected); + }); + + it("should work with if_return optimization changing fn scope", () => { + const source = unpad(` + function foo() { + if (x) + return; + function bar() {} + bar(a); + } + `); + const expected = unpad(` + function foo() { + function b() {} + x || b(a); + } + `); + expect(transformWithSimplify(source)).toBe(expected); + }); +}); diff --git a/packages/babel-plugin-minify-mangle-names/__tests__/mangle-names-test.js b/packages/babel-plugin-minify-mangle-names/__tests__/mangle-names-test.js index 2299ee5b3..91d66f825 100644 --- a/packages/babel-plugin-minify-mangle-names/__tests__/mangle-names-test.js +++ b/packages/babel-plugin-minify-mangle-names/__tests__/mangle-names-test.js @@ -5,12 +5,24 @@ const babel = require("babel-core"); const unpad = require("../../../utils/unpad"); function transform(code, options = {}, sourceType = "script") { + options.reuse = false; return babel.transform(code, { sourceType, plugins: [[require("../src/index"), options]] }).code; } +function transformWithSimplify(code, options = {}, sourceType = "script") { + options.reuse = false; + return babel.transform(code, { + sourceType, + plugins: [ + require("../../babel-plugin-minify-simplify/src/index"), + [require("../src/index"), options] + ] + }).code; +} + describe("mangle-names", () => { it("should not mangle names in the global namespace", () => { const source = unpad( @@ -685,20 +697,20 @@ describe("mangle-names", () => { traverse.clearCache(); const actual = babel.transformFromAst(first.ast, null, { - plugins: [require("../src/index")] + plugins: [[require("../src/index"), { reuse: false }]] }).code; const expected = unpad( ` function f(a) { - var b = function (d) { - var e = void 0; - if (e) { + var b = function (e) { + var h = void 0; + if (h) { return { v: void 0 }; } - g(() => e); + g(() => h); }; for (var d = 0; d; d++) { @@ -737,7 +749,7 @@ describe("mangle-names", () => { traverse.clearCache(); const actual = babel.transformFromAst(first.ast, null, { - plugins: [require("../src/index")] + plugins: [[require("../src/index"), { reuse: false }]] }).code; const expected = unpad( @@ -1366,4 +1378,54 @@ describe("mangle-names", () => { ); expect(transform(source, { topLevel: true }, "module")).toBe(expected); }); + + it("should handle constant violations across multiple blocks", () => { + const source = unpad(` + function foo() { + var x;x;x; + { + var x;x;x; + function y() { + var x;x;x; + { + var x;x;x; + } + } + } + } + `); + const expected = unpad(` + function foo() { + var a;a;a; + { + var a;a;a; + function b() { + var c;c;c; + { + var c;c;c; + } + } + } + } + `); + expect(transform(source)).toBe(expected); + }); + + it("should work with if_return optimization changing fn scope", () => { + const source = unpad(` + function foo() { + if (x) + return; + function bar() {} + bar(a); + } + `); + const expected = unpad(` + function foo() { + function b() {} + x || b(a); + } + `); + expect(transformWithSimplify(source)).toBe(expected); + }); }); diff --git a/packages/babel-plugin-minify-mangle-names/src/charset.js b/packages/babel-plugin-minify-mangle-names/src/charset.js new file mode 100644 index 000000000..161d9129d --- /dev/null +++ b/packages/babel-plugin-minify-mangle-names/src/charset.js @@ -0,0 +1,51 @@ +"use strict"; + +const CHARSET = ("abcdefghijklmnopqrstuvwxyz" + + "ABCDEFGHIJKLMNOPQRSTUVWXYZ$_").split(""); + +module.exports = class Charset { + constructor(shouldConsider) { + this.shouldConsider = shouldConsider; + this.chars = CHARSET.slice(); + this.frequency = {}; + this.chars.forEach((c) => { this.frequency[c] = 0; }); + this.finalized = false; + } + + consider(str) { + if (!this.shouldConsider) { + return; + } + + str.split("").forEach((c) => { + if (this.frequency[c] != null) { + this.frequency[c]++; + } + }); + } + + sort() { + if (this.shouldConsider) { + this.chars = this.chars.sort( + (a, b) => this.frequency[b] - this.frequency[a] + ); + } + + this.finalized = true; + } + + getIdentifier(num) { + if (!this.finalized) { + throw new Error("Should sort first"); + } + + let ret = ""; + num++; + do { + num--; + ret += this.chars[num % this.chars.length]; + num = Math.floor(num / this.chars.length); + } while (num > 0); + return ret; + } +}; diff --git a/packages/babel-plugin-minify-mangle-names/src/counted-set.js b/packages/babel-plugin-minify-mangle-names/src/counted-set.js new file mode 100644 index 000000000..684cb80bf --- /dev/null +++ b/packages/babel-plugin-minify-mangle-names/src/counted-set.js @@ -0,0 +1,28 @@ +// Set that counts +module.exports = class CountedSet { + constructor() { + // because you can't simply extend Builtins yet + this.map = new Map; + } + keys() { + return [...this.map.keys()]; + } + has(value) { + return this.map.has(value); + } + add(value) { + if (!this.has(value)) { + this.map.set(value, 0); + } + this.map.set(value, this.map.get(value) + 1); + } + delete(value) { + if (!this.has(value)) return; + const count = this.map.get(value); + if (count <= 1) { + this.map.delete(value); + } else { + this.map.set(value, count - 1); + } + } +}; diff --git a/packages/babel-plugin-minify-mangle-names/src/index.js b/packages/babel-plugin-minify-mangle-names/src/index.js index 77e108248..7e08c901d 100644 --- a/packages/babel-plugin-minify-mangle-names/src/index.js +++ b/packages/babel-plugin-minify-mangle-names/src/index.js @@ -1,4 +1,6 @@ -"use strict"; +const Charset = require("./charset"); +const ScopeTracker = require("./scope-tracker"); +const isLabelIdentifier = require("./is-label-identifier"); const { markEvalScopes, @@ -6,8 +8,6 @@ const { hasEval } = require("babel-helper-mark-eval-scopes"); -const PATH_RENAME_MARKER = Symbol("PATH_RENAME_MARKER"); - module.exports = ({ types: t, traverse }) => { const hop = Object.prototype.hasOwnProperty; @@ -18,9 +18,10 @@ module.exports = ({ types: t, traverse }) => { { blacklist = {}, keepFnName = false, + keepClassName = false, eval: _eval = false, topLevel = false, - keepClassName = false + reuse = true } = {} ) { this.charset = charset; @@ -28,64 +29,123 @@ module.exports = ({ types: t, traverse }) => { this.blacklist = toObject(blacklist); this.keepFnName = keepFnName; this.keepClassName = keepClassName; - this.eval = _eval; this.topLevel = topLevel; + this.eval = _eval; + this.reuse = reuse; - this.unsafeScopes = new Set(); this.visitedScopes = new Set(); - - this.referencesToUpdate = new Map(); + this.scopeTracker = new ScopeTracker({ reuse }); + this.renamedNodes = new Set(); } run() { - this.cleanup(); + this.crawlScope(); this.collect(); this.charset.sort(); this.mangle(); } - cleanup() { - traverse.clearCache(); - this.program.scope.crawl(); - } - isBlacklist(name) { return hop.call(this.blacklist, name) && this.blacklist[name]; } - markUnsafeScopes(scope) { - let evalScope = scope; - do { - this.unsafeScopes.add(evalScope); - } while ((evalScope = evalScope.parent)); + crawlScope() { + traverse.clearCache(); + this.program.scope.crawl(); } collect() { const mangler = this; + const { scopeTracker } = mangler; + + scopeTracker.addScope(this.program.scope); if (!isEvalScopesMarked(mangler.program.scope)) { markEvalScopes(mangler.program); } - if (this.charset.shouldConsider) { - const collectVisitor = { - Identifier(path) { - const { node } = path; - - if ( - path.parentPath.isMemberExpression({ property: node }) || - path.parentPath.isObjectProperty({ key: node }) - ) { - mangler.charset.consider(node.name); + const collectVisitor = { + Scopable({ scope }) { + scopeTracker.addScope(scope); + Object.keys(scope.bindings).forEach(name => { + scopeTracker.addBinding(scope.bindings[name]); + }); + }, + ReferencedIdentifier(path) { + if (isLabelIdentifier(path)) return; + const { scope, node: { name } } = path; + 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") { + return; } - }, - Literal({ node }) { - mangler.charset.consider(String(node.value)); + 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; + throw new Error("binding not found " + name); + } + scopeTracker.addBinding(binding); + } + } + }; - mangler.program.traverse(collectVisitor); + if (this.charset.shouldConsider) { + collectVisitor.Identifier = function Identifer(path) { + const { node } = path; + + if ( + path.parentPath.isMemberExpression({ property: node }) || + path.parentPath.isObjectProperty({ key: node }) + ) { + mangler.charset.consider(node.name); + } + }; + collectVisitor.Literal = function Literal({ node }) { + mangler.charset.consider(String(node.value)); + }; } + + mangler.program.traverse(collectVisitor); } isExportedWithName(binding) { @@ -108,6 +168,7 @@ module.exports = ({ types: t, traverse }) => { mangleScope(scope) { const mangler = this; + const { scopeTracker } = mangler; if (!mangler.eval && hasEval(scope)) return; @@ -125,26 +186,20 @@ module.exports = ({ types: t, traverse }) => { // => var aa, a, b ,c; // instead of // => var aa, ab, ...; - // TODO: - // Re-enable after enabling this feature - // This doesn't work right now as we are concentrating - // on performance improvements - // function resetNext() { - // i = 0; - // } + function resetNext() { + i = 0; + } - const bindings = scope.getAllBindings(); - const names = Object.keys(bindings); + const bindings = scopeTracker.bindings.get(scope); + const names = [...bindings.keys()]; for (let i = 0; i < names.length; i++) { const oldName = names[i]; - const binding = bindings[oldName]; + const binding = bindings.get(oldName); if ( // arguments oldName === "arguments" || - // other scope bindings - !scope.hasOwnBinding(oldName) || // labels binding.path.isLabeledStatement() || // ClassDeclaration has binding in two scopes @@ -169,15 +224,16 @@ module.exports = ({ types: t, traverse }) => { next = getNext(); } while ( !t.isValidIdentifier(next) || - hop.call(bindings, next) || + scopeTracker.hasBinding(scope, next) || scope.hasGlobal(next) || - scope.hasReference(next) + scopeTracker.hasReference(scope, next) || + !scopeTracker.canUseInReferencedScopes(binding, next) ); - // TODO: - // re-enable this - check above - // resetNext(); - mangler.rename(scope, oldName, next); + if (mangler.reuse) { + resetNext(); + } + mangler.rename(scope, binding, oldName, next); } } @@ -185,7 +241,7 @@ module.exports = ({ types: t, traverse }) => { const mangler = this; if (mangler.topLevel) { - mangler.mangleScope(mangler.program.scope); + mangler.mangleScope(this.program.scope); } this.program.traverse({ @@ -195,50 +251,58 @@ module.exports = ({ types: t, traverse }) => { }); } - rename(scope, oldName, newName) { - const binding = scope.getBinding(oldName); - - // rename at the declaration level - const bindingPaths = binding.path.getBindingIdentifierPaths(true, false); - - // we traverse through all bindingPaths because, - // there is no binding.identifierPath in babel - for (const name in bindingPaths) { + renameBindingIds(path, oldName, newName, predicate = () => true) { + const bindingIds = path.getBindingIdentifierPaths(true, false); + for (const name in bindingIds) { if (name !== oldName) continue; - for (const idPath of bindingPaths[name]) { - if (binding.identifier === idPath.node) { + for (const idPath of bindingIds[name]) { + if (predicate(idPath)) { + this.renamedNodes.add(idPath.node); idPath.replaceWith(t.identifier(newName)); - binding.identifier = idPath.node; - idPath[PATH_RENAME_MARKER] = true; + this.renamedNodes.add(idPath.node); } } } + } - const { bindings } = scope; - bindings[newName] = binding; - delete bindings[oldName]; + rename(scope, binding, oldName, newName) { + const mangler = this; + const { scopeTracker } = mangler; + + // rename at the declaration level + this.renameBindingIds( + binding.path, + oldName, + newName, + idPath => idPath.node === binding.identifier + ); + + // update Tracking + scopeTracker.renameBinding(scope, oldName, newName); // update all constant violations & redeclarations const violations = binding.constantViolations; for (let i = 0; i < violations.length; i++) { if (violations[i].isLabeledStatement()) continue; - const bindings = violations[i].getBindingIdentifierPaths(); - Object.keys(bindings).map(b => { - if (b === oldName && !bindings[b][PATH_RENAME_MARKER]) { - bindings[b].replaceWith(t.identifier(newName)); - bindings[b][PATH_RENAME_MARKER] = true; - } - }); + // const bindings = violations[i].getBindingIdentifierPaths(); + // Object.keys(bindings).map(b => { + // if (b === oldName && !bindings[b][PATH_RENAME_MARKER]) { + // bindings[b].replaceWith(t.identifier(newName)); + // bindings[b][PATH_RENAME_MARKER] = true; + // } + // }); + + this.renameBindingIds(violations[i], oldName, newName); } // update all referenced places const refs = binding.referencePaths; for (let i = 0; i < refs.length; i++) { const path = refs[i]; - if (path[PATH_RENAME_MARKER]) continue; const { node } = path; + if (!path.isIdentifier()) { // Ideally, this should not happen // it happens in these places now - @@ -250,19 +314,49 @@ module.exports = ({ types: t, traverse }) => { // replacement in dce from `x` to `!x` gives referencePath as `!x` path.traverse({ ReferencedIdentifier(refPath) { - if ( - refPath.node.name === oldName && - refPath.scope === scope && - !refPath[PATH_RENAME_MARKER] - ) { - refPath.node.name = newName; + if (refPath.node.name !== oldName) { + return; + } + const actualBinding = refPath.scope.getBinding(oldName); + if (actualBinding !== binding) { + return; } + mangler.renamedNodes.add(refPath.node); + refPath.replaceWith(t.identifier(newName)); + mangler.renamedNodes.add(refPath.node); + + scopeTracker.updateReference( + refPath.scope, + binding, + oldName, + newName + ); } }); } else if (!isLabelIdentifier(path)) { - node.name = newName; + if (path.node.name === oldName) { + mangler.renamedNodes.add(path.node); + path.replaceWith(t.identifier(newName)); + mangler.renamedNodes.add(path.node); + + scopeTracker.updateReference(path.scope, binding, oldName, newName); + } else if (mangler.renamedNodes.has(path.node)) { + // already renamed, + // just update the references + scopeTracker.updateReference(path.scope, binding, oldName, newName); + } else { + throw new Error( + `Unexpected Error - Trying to replace ${node.name}: from ${oldName} to ${newName}` + ); + } } + // else label } + + // update babel's scope tracking + const { bindings } = scope; + bindings[newName] = binding; + delete bindings[oldName]; } } @@ -287,58 +381,6 @@ module.exports = ({ types: t, traverse }) => { }; }; -const CHARSET = ("abcdefghijklmnopqrstuvwxyz" + - "ABCDEFGHIJKLMNOPQRSTUVWXYZ$_").split(""); - -class Charset { - constructor(shouldConsider) { - this.shouldConsider = shouldConsider; - this.chars = CHARSET.slice(); - this.frequency = {}; - this.chars.forEach(c => { - this.frequency[c] = 0; - }); - this.finalized = false; - } - - consider(str) { - if (!this.shouldConsider) { - return; - } - - str.split("").forEach(c => { - if (this.frequency[c] != null) { - this.frequency[c]++; - } - }); - } - - sort() { - if (this.shouldConsider) { - this.chars = this.chars.sort( - (a, b) => this.frequency[b] - this.frequency[a] - ); - } - - this.finalized = true; - } - - getIdentifier(num) { - if (!this.finalized) { - throw new Error("Should sort first"); - } - - let ret = ""; - num++; - do { - num--; - ret += this.chars[num % this.chars.length]; - num = Math.floor(num / this.chars.length); - } while (num > 0); - return ret; - } -} - // convert value to object function toObject(value) { if (!Array.isArray(value)) { @@ -361,12 +403,3 @@ function isFunction(path) { function isClass(path) { return path.isClassExpression() || path.isClassDeclaration(); } - -function isLabelIdentifier(path) { - const { node } = path; - return ( - path.parentPath.isLabeledStatement({ label: node }) || - path.parentPath.isBreakStatement({ label: node }) || - path.parentPath.isContinueStatement({ label: node }) - ); -} diff --git a/packages/babel-plugin-minify-mangle-names/src/is-label-identifier.js b/packages/babel-plugin-minify-mangle-names/src/is-label-identifier.js new file mode 100644 index 000000000..28d6ffe2c --- /dev/null +++ b/packages/babel-plugin-minify-mangle-names/src/is-label-identifier.js @@ -0,0 +1,8 @@ +module.exports = isLabelIdentifier; + +function isLabelIdentifier(path) { + const {node} = path; + return path.parentPath.isLabeledStatement({ label: node }) + || path.parentPath.isBreakStatement({ label: node }) + || path.parentPath.isContinueStatement({ label: node }); +} diff --git a/packages/babel-plugin-minify-mangle-names/src/scope-tracker.js b/packages/babel-plugin-minify-mangle-names/src/scope-tracker.js new file mode 100644 index 000000000..54675282d --- /dev/null +++ b/packages/babel-plugin-minify-mangle-names/src/scope-tracker.js @@ -0,0 +1,157 @@ +const CountedSet = require("./counted-set"); +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; + } + + // Register a new Scope and initiliaze it with empty sets + addScope(scope) { + if (!this.references.has(scope)) { + this.references.set(scope, new CountedSet); + } + if (!this.bindings.has(scope)) { + this.bindings.set(scope, new Map); + } + } + + addReference(scope, binding, name) { + let parent = scope; + do { + if (!this.references.has(parent)) { + this.addScope(parent); + this.updateScope(parent); + } + this.references.get(parent).add(name); + + // here binding is undefined for globals, + // so we just add to all scopes up + if (binding && binding.scope === parent) { + break; + } + } 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); + } + return this.references.get(scope).has(name); + } + + canUseInReferencedScopes(binding, next) { + const tracker = this; + + if (tracker.hasReference(binding.scope, next)) { + return false; + } + + for (let i = 0; i < binding.constantViolations.length; i++) { + const violation = binding.constantViolations[i]; + if (tracker.hasReference(violation.scope, next)) { + return false; + } + } + + for (let i = 0; i < binding.referencePaths; i++) { + const ref = binding.referencePaths[i]; + if (!ref.isIdentifier()) { + let canUse = true; + ref.traverse({ + ReferencedIdentifier(path) { + if (path.node.name !== next) return; + if (tracker.hasReference(path.scope, next)) { + canUse = false; + } + } + }); + if (!canUse) { + return canUse; + } + } else if (!isLabelIdentifier(ref)) { + if (tracker.hasReference(ref.scope, next)) { + return false; + } + } + } + + return true; + } + + updateReference(scope, binding, oldName, newName) { + let parent = scope; + do { + if (!this.references.has(parent)) { + this.addScope(parent); + this.updateScope(parent); + } + + // update + const ref = this.references.get(parent); + if (ref.has(oldName)) { + ref.delete(oldName); + ref.add(newName); + } + // else already renamed + + if (binding.scope === parent) { + break; + } + } while (parent = parent.parent); + } + + addBinding(binding) { + if (!binding) { + return; + } + const bindings = this.bindings.get(binding.scope); + if (!bindings.has(binding.identifier.name)) { + bindings.set(binding.identifier.name, binding); + } + } + + hasBinding(scope, name) { + if (!this.reuse) { + return scope.hasBinding(name); + } + return this.bindings.get(scope).has(name); + } + + renameBinding(scope, oldName, newName) { + const bindings = this.bindings.get(scope); + bindings.set(newName, bindings.get(oldName)); + bindings.delete(oldName); + } + + // This is a fallback option and is used when something happens - + // during traversal and checks we find that a scope doesn't + // exist in the tracker + // + // This should NOT happen ultimately. Just used as a fallback + // 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"); + + const tracker = this; + scope.path.traverse({ + ReferencedIdentifier(path) { + if (path.scope === scope) { + const binding = scope.getBinding(path.node.name); + tracker.addReference(scope, binding, path.node.name); + } + } + }); + } +}; diff --git a/packages/babel-preset-babili/__tests__/preset-tests.js b/packages/babel-preset-babili/__tests__/preset-tests.js index cb90a9eab..7c57f8162 100644 --- a/packages/babel-preset-babili/__tests__/preset-tests.js +++ b/packages/babel-preset-babili/__tests__/preset-tests.js @@ -7,7 +7,7 @@ function transform(code, options = {}, sourceType = "script") { return babel.transform(code, { sourceType, minified: false, - presets: [require("../src/index")] + presets: [[require("../src/index"), options]] }).code; } @@ -34,8 +34,8 @@ describe("preset", () => { const expected = unpad( ` function foo() { - var d, e, f; - d ? e && f : e || f; + var d, a, b; + d ? a && b : a || b; } ` ); @@ -137,4 +137,46 @@ describe("preset", () => { ); expect(transform(source)).toBe(expected); }); + + it("should fix bug#326 - object destructuring", () => { + const source = unpad(` + function a() { + let foo, bar, baz; + ({foo, bar, baz} = {}); + return {foo, bar, baz}; + } + `); + const expected = unpad(` + function a() { + let a, b, c; + + return ({ foo: a, bar: b, baz: c } = {}), { foo: a, bar: b, baz: c }; + } + `); + expect(transform(source)).toBe(expected); + }); + + it("should fix bug#326 - object destructuring", () => { + const source = unpad(` + function a() { + let foo, bar, baz; + ({foo, bar, baz} = {}); + return {foo, bar, baz}; + } + `); + const expected = unpad(` + function a() { + let b, c, d; + + return ({ foo: b, bar: c, baz: d } = {}), { foo: b, bar: c, baz: d }; + } + `); + expect( + transform(source, { + mangle: { + reuse: false + } + }) + ).toBe(expected); + }); });