diff --git a/packages/babel-plugin-transform-remove-undefined/.npmignore b/packages/babel-plugin-transform-remove-undefined/.npmignore new file mode 100644 index 000000000..22250660e --- /dev/null +++ b/packages/babel-plugin-transform-remove-undefined/.npmignore @@ -0,0 +1,4 @@ +src +__tests__ +node_modules +*.log diff --git a/packages/babel-plugin-transform-remove-undefined/README.md b/packages/babel-plugin-transform-remove-undefined/README.md new file mode 100644 index 000000000..781812ce9 --- /dev/null +++ b/packages/babel-plugin-transform-remove-undefined/README.md @@ -0,0 +1,59 @@ +# babel-plugin-transform-remove-undefined + +For variable assignments, this removes rvals that evaluate to `undefined` +(`var`s in functions only). +For functions, this removes return arguments that evaluate to `undefined`. + +## Example + +**In** + +```javascript +let a = void 0; +function foo() { + var b = undefined; + return undefined; +} +``` + +**Out** + +```javascript +let a; +function foo() { + var b; + return; +} +``` + +## Installation + +```sh +$ npm install babel-plugin-transform-remove-undefined +``` + +## Usage + +### Via `.babelrc` (Recommended) + +**.babelrc** + +```json +{ + "plugins": ["babel-plugin-transform-remove-undefined"] +} +``` + +### Via CLI + +```sh +$ babel --plugins babel-plugin-transform-remove-undefined script.js +``` + +### Via Node API + +```javascript +require("babel-core").transform("code", { + plugins: ["babel-plugin-transform-remove-undefined"] +}); +``` diff --git a/packages/babel-plugin-transform-remove-undefined/__tests__/babel-plugin-transform-remove-undefined-test.js b/packages/babel-plugin-transform-remove-undefined/__tests__/babel-plugin-transform-remove-undefined-test.js new file mode 100644 index 000000000..6320a17cb --- /dev/null +++ b/packages/babel-plugin-transform-remove-undefined/__tests__/babel-plugin-transform-remove-undefined-test.js @@ -0,0 +1,174 @@ +jest.autoMockOff(); + +const babel = require("babel-core"); +const plugin = require("../src/index"); +const unpad = require("../../../utils/unpad"); + +function transform(code) { + return babel.transform(code, { + plugins: [plugin], + }).code; +} + +describe("transform-remove-undefined-plugin", () => { + it("should remove multiple undefined assignments in 1 statement", () => { + const source = "let a = undefined, b = 3, c = undefined, d;"; + const expected = "let a,\n b = 3,\n c,\n d;"; + expect(transform(source)).toBe(expected); + }); + + it("should remove let-assignments to undefined", () => { + const source = "let a = undefined;"; + const expected = "let a;"; + expect(transform(source)).toBe(expected); + }); + + it("should remove let-assignments to void 0", () => { + const source = "let a = void 0;"; + const expected = "let a;"; + expect(transform(source)).toBe(expected); + }); + + it("should not remove const-assignments to undefined", () => { + const source = "const a = undefined;"; + expect(transform(source)).toBe(source); + }); + + it("should remove undefined return value", () => { + const source = unpad(` + function foo() { + return undefined; + }`); + const expected = unpad(` + function foo() { + return; + }`); + expect(transform(source)).toBe(expected); + }); + + it("should remove var declarations in functions", () => { + const source = unpad(` + function foo() { + var a = undefined; + }`); + const expected = unpad(` + function foo() { + var a; + }`); + expect(transform(source)).toBe(expected); + }); + + it("should remove let-assignments in inner blocks", () => { + const source = unpad(` + let a = 1; + { + let a = undefined; + }`); + const expected = unpad(` + let a = 1; + { + let a; + }`); + expect(transform(source)).toBe(expected); + }); + + it("should remove var-assignments in loops if no modify", () => { + const source = unpad(` + for (var a = undefined;;) { + var b = undefined; + }`); + const expected = unpad(` + for (var a;;) { + var b; + }`); + expect(transform(source)).toBe(expected); + }); + + it("should not remove var-assignments in loops if modify", () => { + const source = unpad(` + for (var a;;) { + var b = undefined; + console.log(b); + b = 3; + }`); + expect(transform(source)).toBe(source); + }); + + it("should not remove var-assignments if referenced before", () => { + const source = unpad(` + function foo() { + a = 3; + var a = undefined; + }`); + expect(transform(source)).toBe(source); + }); + + it("should not remove nested var-assignments if referenced before", () => { + const source = unpad(` + function foo() { + aa = 3; + var { a: aa, b: bb } = undefined; + }`); + expect(transform(source)).toBe(source); + }); + + it("should not remove if lval is reference before via a function", () => { + const source = unpad(` + function foo() { + bar(); + var x = undefined; + console.log(x); + function bar() { + x = 3; + } + }`); + expect(transform(source)).toBe(source); + }); + + it("should remove if not referenced in any way before", () => { + const source = unpad(` + function foo() { + var x = undefined; + bar(); + console.log(x); + function bar() { + x = 3; + } + }`); + const expected = unpad(` + function foo() { + var x; + bar(); + console.log(x); + function bar() { + x = 3; + } + }`); + expect(transform(source)).toBe(expected); + }); + + it("should not remove on mutually recursive function", () => { + const source = unpad(` + function foo() { + a(); + var c = undefined; + function a() { + b(); + } + function b() { + a(); + c = 3; + } + }`); + expect(transform(source)).toBe(source); + }); + + it("should not remove if rval has side effects", () => { + const source = unpad(` + function foo() { + var a = void b(); + return void bar(); + }`); + expect(transform(source)).toBe(source); + }); +}); diff --git a/packages/babel-plugin-transform-remove-undefined/package.json b/packages/babel-plugin-transform-remove-undefined/package.json new file mode 100644 index 000000000..310aeeb0e --- /dev/null +++ b/packages/babel-plugin-transform-remove-undefined/package.json @@ -0,0 +1,16 @@ +{ + "name": "babel-plugin-transform-remove-undefined", + "version": "0.0.1", + "description": "This removes rvals that are equivalent to undefined wherever possible", + "homepage": "https://github.com/babel/babili#readme", + "repository": "https://github.com/babel/babili/tree/master/packages/babel-plugin-transform-remove-undefined", + "bugs": "https://github.com/babel/babili/issues", + "author": "shinew", + "license": "MIT", + "main": "lib/index.js", + "keywords": [ + "babel-plugin" + ], + "dependencies": {}, + "devDependencies": {} +} diff --git a/packages/babel-plugin-transform-remove-undefined/src/index.js b/packages/babel-plugin-transform-remove-undefined/src/index.js new file mode 100644 index 000000000..262dad84d --- /dev/null +++ b/packages/babel-plugin-transform-remove-undefined/src/index.js @@ -0,0 +1,120 @@ +"use strict"; + +function isPureAndUndefined(rval) { + if (rval.isIdentifier() && + rval.node.name === "undefined") { + return true; + } + if (!rval.isPure()) { + return false; + } + const evaluation = rval.evaluate(); + return evaluation.confident === true && evaluation.value === undefined; +} + +function getLoopParent(path, scopeParent) { + const parent = path.findParent((p) => p.isLoop() || p === scopeParent); + // don't traverse higher than the function the var is defined in. + return parent === scopeParent ? null : parent; +} + +function getFunctionParent(path, scopeParent) { + const parent = path.findParent((p) => p.isFunction()); + // don't traverse higher than the function the var is defined in. + return parent === scopeParent ? null : parent; +} + +function getFunctionReferences(path, scopeParent, references = new Set()) { + for (let func = getFunctionParent(path, scopeParent); func; func = getFunctionParent(func, scopeParent)) { + const id = func.node.id; + const binding = id && func.scope.getBinding(id.name); + + if (!binding) { + continue; + } + + binding.referencePaths.forEach((path) => { + if (!references.has(path)) { + references.add(path); + getFunctionReferences(path, scopeParent, references); + } + }); + } + return references; +} + +function hasViolation(declarator, scope, start) { + const binding = scope.getBinding(declarator.node.id.name); + if (!binding) { + return true; + } + + const scopeParent = declarator.getFunctionParent(); + + const violation = binding.constantViolations.some((v) => { + // return 'true' if we cannot guarantee the violation references + // the initialized identifier after + const violationStart = v.node.start; + if (violationStart === undefined || violationStart < start) { + return true; + } + + const references = getFunctionReferences(v, scopeParent); + for (const ref of references) { + if (ref.node.start === undefined || ref.node.start < start) { + return true; + } + } + + for (let loop = getLoopParent(declarator, scopeParent); loop; loop = getLoopParent(loop, scopeParent)) { + if (loop.node.end === undefined || loop.node.end > violationStart) { + return true; + } + } + }); + + return violation; +} + +module.exports = function() { + return { + name: "transform-remove-undefined", + visitor: { + ReturnStatement(path) { + if (path.node.argument !== null) { + if (isPureAndUndefined(path.get("argument"))) { + path.node.argument = null; + } + } + }, + + VariableDeclaration(path) { + switch (path.node.kind) { + case "const": + break; + case "let": + for (const declarator of path.get("declarations")) { + if (isPureAndUndefined(declarator.get("init"))) { + declarator.node.init = null; + } + } + break; + case "var": + const start = path.node.start; + if (start === undefined) { + // This is common for plugin-generated nodes + break; + } + const scope = path.scope; + for (const declarator of path.get("declarations")) { + if (isPureAndUndefined(declarator.get("init")) && + !hasViolation(declarator, scope, start)) { + declarator.node.init = null; + } + } + break; + } + }, + }, + }; +}; diff --git a/packages/babel-preset-babili/package.json b/packages/babel-preset-babili/package.json index 777463fbc..ea2ae1fb2 100644 --- a/packages/babel-preset-babili/package.json +++ b/packages/babel-preset-babili/package.json @@ -27,6 +27,7 @@ "babel-plugin-transform-minify-booleans": "^6.8.0", "babel-plugin-transform-property-literals": "^6.8.0", "babel-plugin-transform-regexp-constructors": "^0.0.1", + "babel-plugin-transform-remove-undefined": "^0.0.1", "babel-plugin-transform-simplify-comparison-operators": "^6.8.0", "babel-plugin-transform-undefined-to-void": "^6.8.0" }, diff --git a/packages/babel-preset-babili/src/index.js b/packages/babel-preset-babili/src/index.js index bb8db23f4..0f5cafd98 100644 --- a/packages/babel-preset-babili/src/index.js +++ b/packages/babel-preset-babili/src/index.js @@ -16,6 +16,7 @@ module.exports = { require("babel-plugin-transform-minify-booleans"), require("babel-plugin-transform-property-literals"), require("babel-plugin-transform-regexp-constructors"), + require("babel-plugin-transform-remove-undefined"), require("babel-plugin-transform-simplify-comparison-operators"), require("babel-plugin-transform-undefined-to-void"), ],