Repository: babel/minify (js)

Issue:
Anonymous functions sometimes given conflicting names, breaking code.
#### Input Code

index.js:
```js
#!/usr/bin/node
'use strict';

const babel = require('babel-core');

console.log(babel.transform(`
	'use strict';
	(() => {
		const con = console;
		const obj = {
			a() {
				con.log('Hi.');
			}
		};
		obj.a.log = str => {
			con.log('OUCH, should never get here! Should be "' + str + '".');
		};
		obj.a();
	})();
`, {presets: ['minify', 'env']}).code);
```

package.json:
```
{
  "name": "babelbug",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "",
  "private": true,
  "dependencies": {
    "babel-core": "^6.26.3",
    "babel-preset-env": "^1.6.1",
    "babel-preset-minify": "^0.4.0"
  }
}
```

#### Actual Output
<!-- If an error is thrown, please provide the stack trace here -->

```js
'use strict';(function(){var a=console,b={a:function a(){a.log('Hi.')}};b.a.log=function(b){
a.log('OUCH, should never get here! Should be "'+b+'".')},b.a()})();
```
Prints 'OUCH, should never get here! Should be "Hi.".'.

#### Expected Output

```js
'use strict';(function(){var a=console,b={a:function(){a.log('Hi.')}};b.a.log=function(b){
a.log('OUCH, should never get here! Should be "'+b+'".')},b.a()})();
```

Prints 'Hi.'.

#### Details
Note the incorrect `function a()` instead of `function()`, leading to the `con` variable being clobbered.

Same thing if using babel-preset-es2015 with babel-preset-minify.

Can't make a direct link to reproduce online – not sure why. But following this link, clicking on 'Plugins', on 'Only official plugins', typing 'mangle' and selecting minify-mangle-names reproduces it:
https://babeljs.io/repl/#?babili=false&browsers=&build=&builtIns=false&code_lz=JAcgrgzgpgBBAuAnAlgY3iA3AKGACjwEoYBeAPhgG9dhUB7AOwRnodKoBs6BzALhgAepCgEMOURPDwDCAXxzBajZnQBGAK3bVFwEUSo1FrAHRdueEAAlkxkIQWLZNeTTXrjI0z3YJEwgzomZhYA8gCqAMKWADRwABZ0YBwAJjAMUABuEjDcUPAwcRJQAIQwAMoJSamqsABEIDAA1HBITTAgtbb2zg5uHkQKsoQD2EA&debug=false&forceAllTransforms=false&shippedProposals=false&circleciRepo=&evaluate=true&fileSize=false&lineWrap=false&presets=es2015&prettier=false&targets=&version=6.26.0&envVersion=

Not sure whether this is a minify bug or a babel bug. Can't reproduce if running 'env' on the output of 'minify' or when running 'minify' on the output of 'env', only if doing both in the same step.


Pull request description:
fix(mangle): handle inferred names for functions
The t.NOT_LOCAL_BINDING symbol used in plugin-function-name
(https://github.com/babel/babel/blob/bd98041321c60737ddcacd1ad7e056ef6de31879/packages/babel-helper-function-name/src/index.js#L206)
is causing this issue.

This was added in babel/babel#3298

The problem is the binding identifier is NOT registered (skipped) during
crawl and the mangler finds the first safe identifier which ultimately
collides with this name during runtime.

So, we register the binding and don't respect the t.NOT_LOCAL_BINDING.

Fix #829

Patch:
diff --git a/packages/babel-plugin-minify-mangle-names/src/index.js b/packages/babel-plugin-minify-mangle-names/src/index.js
index 1b20fc8..0705010 100644
--- a/packages/babel-plugin-minify-mangle-names/src/index.js
+++ b/packages/babel-plugin-minify-mangle-names/src/index.js
@@ -139,6 +139,27 @@ module.exports = babel => {
           });
         },
 
+        /**
+         * This is required because after function name transformation
+         * plugin (part of es2015), the function name is NOT added to the
+         * scope's bindings. So to fix this issue, we simply add a hack to
+         * handle that case - fix it to the scope tree.
+         *
+         * Related:
+         * - https://github.com/babel/minify/issues/829
+         */
+        BindingIdentifier(path) {
+          if (
+            // the parent has this id as the name
+            (path.parentPath.isFunctionExpression({ id: path.node }) ||
+              path.parentPath.isClassExpression({ id: path.node })) &&
+            // and the id isn't yet added to the scope
+            !hop.call(path.parentPath.scope.bindings, path.node.name)
+          ) {
+            path.parentPath.scope.registerBinding("local", path.parentPath);
+          }
+        },
+
         /**
          * This is necessary because, in Babel, the scope.references
          * does NOT contain the references in that scope. Only the program
diff --git a/packages/babel-plugin-minify-simplify/src/index.js b/packages/babel-plugin-minify-simplify/src/index.js
index 426634f..7a1de96 100644
--- a/packages/babel-plugin-minify-simplify/src/index.js
+++ b/packages/babel-plugin-minify-simplify/src/index.js
@@ -119,6 +119,13 @@ module.exports = ({ types: t }) => {
         ]
       },
 
+      BinaryExpression(path) {
+        if (["!=", "=="].indexOf(path.node.operator) !== -1) {
+          undefinedToNull(path.get("left"));
+          undefinedToNull(path.get("right"));
+        }
+      },
+
       LogicalExpression: {
         exit: logicalExpression.simplifyPatterns
       },
@@ -1141,4 +1148,21 @@ module.exports = ({ types: t }) => {
   function isAncestor(path1, path2) {
     return !!path2.findParent(parent => parent === path1);
   }
+
+  function isPureVoid(path) {
+    return path.isUnaryExpression({ operator: "void" }) && path.isPure();
+  }
+
+  function isGlobalUndefined(path) {
+    return (
+      path.isIdentifier({ name: "undefined" }) &&
+      !path.scope.getBinding("undefined")
+    );
+  }
+
+  function undefinedToNull(path) {
+    if (isGlobalUndefined(path) || isPureVoid(path)) {
+      path.replaceWith(t.nullLiteral());
+    }
+  }
 };
diff --git a/packages/babel-plugin-transform-simplify-comparison-operators/src/index.js b/packages/babel-plugin-transform-simplify-comparison-operators/src/index.js
index 28891b9..d543432 100644
--- a/packages/babel-plugin-transform-simplify-comparison-operators/src/index.js
+++ b/packages/babel-plugin-transform-simplify-comparison-operators/src/index.js
@@ -70,6 +70,7 @@ module.exports = function({ types: t }) {
       BinaryExpression(path) {
         const { node } = path;
         const op = node.operator;
+
         if (op !== "===" && op !== "!==") {
           return;
         }


Test patch:
diff --git a/packages/babel-plugin-minify-simplify/__tests__/fixtures/eqnull/actual.js b/packages/babel-plugin-minify-simplify/__tests__/fixtures/eqnull/actual.js
new file mode 100644
index 0000000..6858e76
--- /dev/null
+++ b/packages/babel-plugin-minify-simplify/__tests__/fixtures/eqnull/actual.js
@@ -0,0 +1,3 @@
+x == undefined;
+x == void 0;
+x === undefined;
diff --git a/packages/babel-plugin-minify-simplify/__tests__/fixtures/eqnull/expected.js b/packages/babel-plugin-minify-simplify/__tests__/fixtures/eqnull/expected.js
new file mode 100644
index 0000000..9ef47da
--- /dev/null
+++ b/packages/babel-plugin-minify-simplify/__tests__/fixtures/eqnull/expected.js
@@ -0,0 +1,1 @@
+x == null, x == null, x === undefined;
\ No newline at end of file
diff --git a/packages/babel-preset-minify/__tests__/minify-env-tests.js b/packages/babel-preset-minify/__tests__/minify-env-tests.js
index b175e8b..99c6036 100644
--- a/packages/babel-preset-minify/__tests__/minify-env-tests.js
+++ b/packages/babel-preset-minify/__tests__/minify-env-tests.js
@@ -196,4 +196,59 @@ describe("preset along with env", () => {
       console.log(foo);
     `
   );
+
+  thePlugin(
+    "should fix issue#829 mangling after function name",
+    `
+      function foo() {
+        let con = console;
+
+        return {
+          a(bar) {
+            con.log(bar);
+          }
+        };
+      }
+    `,
+    `
+      function foo() {
+        var b = console;
+        return {
+          a: function d(c) {
+            b.log(c);
+          }
+        };
+      }
+    `
+  );
+
+  thePlugin(
+    "should fix issue#829 mangling after function name 2",
+    `
+      function bar() {
+        var b = console;
+        return {
+          a: class {
+            constructor(bar) {
+              b.log(bar);
+            }
+          }
+        };
+      }
+    `,
+    `
+      function _classCallCheck(a, b) { if (!(a instanceof b)) throw new TypeError("Cannot call a class as a function"); }
+
+      function bar() {
+        var c = console;
+        return {
+          a: function b(a) {
+            "use strict";
+
+            _classCallCheck(this, b), c.log(a);
+          }
+        };
+      }
+    `
+  );
 });