diff --git a/packages/babel-plugin-transform-new-target/.npmignore b/packages/babel-plugin-transform-new-target/.npmignore new file mode 100644 index 000000000000..f9806945836e --- /dev/null +++ b/packages/babel-plugin-transform-new-target/.npmignore @@ -0,0 +1,3 @@ +src +test +*.log diff --git a/packages/babel-plugin-transform-new-target/README.md b/packages/babel-plugin-transform-new-target/README.md new file mode 100644 index 000000000000..b7183237f4e7 --- /dev/null +++ b/packages/babel-plugin-transform-new-target/README.md @@ -0,0 +1,104 @@ +# babel-plugin-transform-new-target + +This plugins allows babel to transform `new.target` meta property into a +(correct in most cases) `this.constructor` expression. + +## Example + +```js +function Foo() { + console.log(new.target); +} + +Foo(); // => undefined +new Foo(); // => Foo +``` + +```js +class Foo { + constructor() { + console.log(new.target); + } +} + +class Bar extends Foo { +} + +new Foo(); // => Foo +new Bar(); // => Bar +``` + +### Caveats + +This plugin relies on `this.constructor`, which means `super` must +already have been called when using untransformed classes. + +```js +class Foo {} + +class Bar extends Foo { + constructor() { + // This will be a problem if classes aren't transformed to ES5 + new.target; + super(); + } +} +``` + +Additionally, this plugin cannot transform all `Reflect.construct` cases +when using `newTarget` with ES5 function classes (transformed ES6 classes). + +```js +function Foo() { + console.log(new.target); +} + +// Bar extends Foo in ES5 +function Bar() { + Foo.call(this); +} +Bar.prototype = Object.create(Foo.prototype); +Bar.prototype.constructor = Bar; + +// Baz does not extend Foo +function Baz() {} + +Reflect.construct(Foo, []); // => Foo (correct) +Reflect.construct(Foo, [], Bar); // => Bar (correct) + +Reflect.construct(Bar, []); // => Bar (incorrect, though this is how ES5 + // inheritience is commonly implemented.) +Reflect.construct(Foo, [], Baz); // => undefined (incorrect) +``` + +## Installation + +```sh +npm install --save-dev babel-plugin-transform-new-target +``` + +## Usage + +### Via `.babelrc` (Recommended) + +**.babelrc** + +```json +{ + "plugins": ["transform-new-target"] +} +``` + +### Via CLI + +```sh +babel --plugins transform-new-target script.js +``` + +### Via Node API + +```javascript +require("babel-core").transform("code", { + plugins: ["transform-new-target"] +}); +``` diff --git a/packages/babel-plugin-transform-new-target/package.json b/packages/babel-plugin-transform-new-target/package.json new file mode 100644 index 000000000000..9b68063f85f5 --- /dev/null +++ b/packages/babel-plugin-transform-new-target/package.json @@ -0,0 +1,18 @@ +{ + "name": "babel-plugin-transform-new-target", + "version": "7.0.0-alpha.12", + "description": "Transforms new.target meta property", + "repository": "https://github.com/babel/babel/tree/master/packages/babel-plugin-transform-new-target", + "license": "MIT", + "main": "lib/index.js", + "keywords": [ + "babel-plugin" + ], + "dependencies": { + }, + "devDependencies": { + "babel-helper-plugin-test-runner": "7.0.0-alpha.12", + "babel-plugin-transform-class-properties": "7.0.0-alpha.12", + "babel-plugin-transform-es2015-arrow-functions": "7.0.0-alpha.12" + } +} diff --git a/packages/babel-plugin-transform-new-target/src/index.js b/packages/babel-plugin-transform-new-target/src/index.js new file mode 100644 index 000000000000..3c0b4c648cd4 --- /dev/null +++ b/packages/babel-plugin-transform-new-target/src/index.js @@ -0,0 +1,64 @@ +export default function({ types: t }) { + return { + name: "transform-new-target", + + visitor: { + MetaProperty(path) { + const meta = path.get("meta"); + const property = path.get("property"); + const { scope } = path; + + if ( + meta.isIdentifier({ name: "new" }) && + property.isIdentifier({ name: "target" }) + ) { + const func = path.findParent(path => { + if (path.isClass()) return true; + if (path.isFunction() && !path.isArrowFunctionExpression()) { + if (path.isClassMethod({ kind: "constructor" })) { + return false; + } + + return true; + } + return false; + }); + + if (!func) { + throw path.buildCodeFrameError( + "new.target must be under a (non-arrow) function or a class.", + ); + } + + const { node } = func; + if (!node.id) { + if (func.isMethod()) { + path.replaceWith(scope.buildUndefinedNode()); + return; + } + + node.id = scope.generateUidIdentifier("target"); + } + + const constructor = t.memberExpression( + t.thisExpression(), + t.identifier("constructor"), + ); + + if (func.isClass()) { + path.replaceWith(constructor); + return; + } + + path.replaceWith( + t.conditionalExpression( + t.binaryExpression("instanceof", t.thisExpression(), node.id), + constructor, + scope.buildUndefinedNode(), + ), + ); + } + }, + }, + }; +} diff --git a/packages/babel-plugin-transform-new-target/test/fixtures/errors/new-target-arrow/actual.js b/packages/babel-plugin-transform-new-target/test/fixtures/errors/new-target-arrow/actual.js new file mode 100644 index 000000000000..47bd62071480 --- /dev/null +++ b/packages/babel-plugin-transform-new-target/test/fixtures/errors/new-target-arrow/actual.js @@ -0,0 +1,5 @@ +"use strict"; + +const a = () => { + new.target; +}; diff --git a/packages/babel-plugin-transform-new-target/test/fixtures/errors/new-target-arrow/options.json b/packages/babel-plugin-transform-new-target/test/fixtures/errors/new-target-arrow/options.json new file mode 100644 index 000000000000..d0d6a8aae23c --- /dev/null +++ b/packages/babel-plugin-transform-new-target/test/fixtures/errors/new-target-arrow/options.json @@ -0,0 +1,3 @@ +{ + "throws": "new.target must be under a (non-arrow) function or a class." +} diff --git a/packages/babel-plugin-transform-new-target/test/fixtures/exec/class-extended.js b/packages/babel-plugin-transform-new-target/test/fixtures/exec/class-extended.js new file mode 100644 index 000000000000..b72fa4f96f79 --- /dev/null +++ b/packages/babel-plugin-transform-new-target/test/fixtures/exec/class-extended.js @@ -0,0 +1,22 @@ +"use strict"; + +const targets = []; +class Foo { + constructor() { + targets.push(new.target); + } +} + +class Bar extends Foo { + constructor() { + super(); + targets.push(new.target); + } +} + +new Foo; +new Bar; + +assert.equal(targets[0], Foo); +assert.equal(targets[1], Bar); +assert.equal(targets[2], Bar); diff --git a/packages/babel-plugin-transform-new-target/test/fixtures/exec/class.js b/packages/babel-plugin-transform-new-target/test/fixtures/exec/class.js new file mode 100644 index 000000000000..415106cfc005 --- /dev/null +++ b/packages/babel-plugin-transform-new-target/test/fixtures/exec/class.js @@ -0,0 +1,12 @@ +"use strict"; + +const targets = []; +class Foo { + constructor() { + targets.push(new.target); + } +} + +new Foo; + +assert.equal(targets[0], Foo); diff --git a/packages/babel-plugin-transform-new-target/test/fixtures/exec/function-class-extended.js b/packages/babel-plugin-transform-new-target/test/fixtures/exec/function-class-extended.js new file mode 100644 index 000000000000..8bfe9fd4a532 --- /dev/null +++ b/packages/babel-plugin-transform-new-target/test/fixtures/exec/function-class-extended.js @@ -0,0 +1,16 @@ +"use strict"; + +const targets = []; +function Foo() { + targets.push(new.target); +} + +function Bar() { + Foo.call(this); +} + +new Foo; +new Bar(); + +assert.equal(targets[0], Foo); +assert.equal(targets[1], undefined); diff --git a/packages/babel-plugin-transform-new-target/test/fixtures/exec/function-class.js b/packages/babel-plugin-transform-new-target/test/fixtures/exec/function-class.js new file mode 100644 index 000000000000..ba44daa1d8d6 --- /dev/null +++ b/packages/babel-plugin-transform-new-target/test/fixtures/exec/function-class.js @@ -0,0 +1,10 @@ +"use strict"; + +const targets = []; +function Foo() { + targets.push(new.target); +} + +new Foo; + +assert.equal(targets[0], Foo); diff --git a/packages/babel-plugin-transform-new-target/test/fixtures/exec/function.js b/packages/babel-plugin-transform-new-target/test/fixtures/exec/function.js new file mode 100644 index 000000000000..95240e44b552 --- /dev/null +++ b/packages/babel-plugin-transform-new-target/test/fixtures/exec/function.js @@ -0,0 +1,12 @@ +"use strict"; + +const targets = []; +function foo() { + targets.push(new.target); +} + +foo(); +foo.call({}); + +assert.equal(targets[0], undefined); +assert.equal(targets[1], undefined); diff --git a/packages/babel-plugin-transform-new-target/test/fixtures/exec/reflect-class/exec.js b/packages/babel-plugin-transform-new-target/test/fixtures/exec/reflect-class/exec.js new file mode 100644 index 000000000000..5897b56a8f58 --- /dev/null +++ b/packages/babel-plugin-transform-new-target/test/fixtures/exec/reflect-class/exec.js @@ -0,0 +1,27 @@ +const targets = []; +class Foo { + constructor() { + targets.push(new.target); + } +} + +class Bar extends Foo { +} +class Baz { +} + +Reflect.construct(Foo, []); +Reflect.construct(Foo, [], Bar); +Reflect.construct(Bar, []); +Reflect.construct(Bar, [], Baz); +Reflect.construct(Foo, [], Baz); + +assert.equal(targets[0], Foo); + +assert.equal(targets[1], Bar); + +assert.equal(targets[2], Bar); + +assert.equal(targets[3], Baz); + +assert.equal(targets[4], Baz); diff --git a/packages/babel-plugin-transform-new-target/test/fixtures/exec/reflect-class/options.json b/packages/babel-plugin-transform-new-target/test/fixtures/exec/reflect-class/options.json new file mode 100644 index 000000000000..7d8c3c204cc8 --- /dev/null +++ b/packages/babel-plugin-transform-new-target/test/fixtures/exec/reflect-class/options.json @@ -0,0 +1,3 @@ +{ + "minNodeVersion": "6.0.0" +} diff --git a/packages/babel-plugin-transform-new-target/test/fixtures/exec/reflect-function/exec.js b/packages/babel-plugin-transform-new-target/test/fixtures/exec/reflect-function/exec.js new file mode 100644 index 000000000000..c96514ec4dbd --- /dev/null +++ b/packages/babel-plugin-transform-new-target/test/fixtures/exec/reflect-function/exec.js @@ -0,0 +1,40 @@ +const targets = []; +function Foo() { + targets.push(new.target); +} + +function Bar() { + Foo.call(this); +} +Bar.prototype = Object.create(Foo.prototype, { + constructor: { + value: Bar, + writable: true, + configurable: true, + } +}); + +function Baz() {} + +Reflect.construct(Foo, []); +Reflect.construct(Foo, [], Bar); +Reflect.construct(Bar, []); +Reflect.construct(Bar, [], Baz); +Reflect.construct(Foo, [], Baz); + +assert.equal(targets[0], Foo); + +assert.equal(targets[1], Bar); + +assert.throws(() => { + // Wish we could support this... + // Then again, this is what a transformed class does. + assert.equal(targets[2], undefined); +}); + +assert.equal(targets[3], undefined); + +assert.throws(() => { + // Wish we could support this... + assert.equal(targets[4], Baz); +}); diff --git a/packages/babel-plugin-transform-new-target/test/fixtures/exec/reflect-function/options.json b/packages/babel-plugin-transform-new-target/test/fixtures/exec/reflect-function/options.json new file mode 100644 index 000000000000..7d8c3c204cc8 --- /dev/null +++ b/packages/babel-plugin-transform-new-target/test/fixtures/exec/reflect-function/options.json @@ -0,0 +1,3 @@ +{ + "minNodeVersion": "6.0.0" +} diff --git a/packages/babel-plugin-transform-new-target/test/fixtures/general/arrow/actual.js b/packages/babel-plugin-transform-new-target/test/fixtures/general/arrow/actual.js new file mode 100644 index 000000000000..902735c13a41 --- /dev/null +++ b/packages/babel-plugin-transform-new-target/test/fixtures/general/arrow/actual.js @@ -0,0 +1,13 @@ +function Foo() { + const a = () => { + new.target; + }; +} + +class Bar { + constructor() { + const a = () => { + new.target; + }; + } +} diff --git a/packages/babel-plugin-transform-new-target/test/fixtures/general/arrow/expected.js b/packages/babel-plugin-transform-new-target/test/fixtures/general/arrow/expected.js new file mode 100644 index 000000000000..e491e29d72f9 --- /dev/null +++ b/packages/babel-plugin-transform-new-target/test/fixtures/general/arrow/expected.js @@ -0,0 +1,18 @@ +function Foo() { + var _newtarget = this instanceof Foo ? this.constructor : void 0; + + const a = function () { + _newtarget; + }; +} + +class Bar { + constructor() { + var _newtarget2 = this.constructor; + + const a = function () { + _newtarget2; + }; + } + +} diff --git a/packages/babel-plugin-transform-new-target/test/fixtures/general/arrow/options.json b/packages/babel-plugin-transform-new-target/test/fixtures/general/arrow/options.json new file mode 100644 index 000000000000..038e9c57f4b6 --- /dev/null +++ b/packages/babel-plugin-transform-new-target/test/fixtures/general/arrow/options.json @@ -0,0 +1,3 @@ +{ + "plugins": ["transform-new-target", "transform-es2015-arrow-functions"] +} diff --git a/packages/babel-plugin-transform-new-target/test/fixtures/general/class-properties/actual.js b/packages/babel-plugin-transform-new-target/test/fixtures/general/class-properties/actual.js new file mode 100644 index 000000000000..bdd27d7a4910 --- /dev/null +++ b/packages/babel-plugin-transform-new-target/test/fixtures/general/class-properties/actual.js @@ -0,0 +1,9 @@ +class Foo { + test = function() { + new.target; + }; + + test2 = () => { + new.target; + } +} diff --git a/packages/babel-plugin-transform-new-target/test/fixtures/general/class-properties/expected.js b/packages/babel-plugin-transform-new-target/test/fixtures/general/class-properties/expected.js new file mode 100644 index 000000000000..4aa6b556b902 --- /dev/null +++ b/packages/babel-plugin-transform-new-target/test/fixtures/general/class-properties/expected.js @@ -0,0 +1,14 @@ +class Foo { + constructor() { + var _newtarget = this.constructor; + + this.test = function _target() { + this instanceof _target ? this.constructor : void 0; + }; + + this.test2 = function () { + _newtarget; + }; + } + +} diff --git a/packages/babel-plugin-transform-new-target/test/fixtures/general/class-properties/options.json b/packages/babel-plugin-transform-new-target/test/fixtures/general/class-properties/options.json new file mode 100644 index 000000000000..97c1f2220384 --- /dev/null +++ b/packages/babel-plugin-transform-new-target/test/fixtures/general/class-properties/options.json @@ -0,0 +1,3 @@ +{ + "plugins": ["transform-new-target", "transform-es2015-arrow-functions", "transform-class-properties"] +} diff --git a/packages/babel-plugin-transform-new-target/test/fixtures/general/class/actual.js b/packages/babel-plugin-transform-new-target/test/fixtures/general/class/actual.js new file mode 100644 index 000000000000..eb27600e9125 --- /dev/null +++ b/packages/babel-plugin-transform-new-target/test/fixtures/general/class/actual.js @@ -0,0 +1,9 @@ +class Foo { + constructor() { + new.target; + } + + test() { + new.target; + } +} diff --git a/packages/babel-plugin-transform-new-target/test/fixtures/general/class/expected.js b/packages/babel-plugin-transform-new-target/test/fixtures/general/class/expected.js new file mode 100644 index 000000000000..1e4f43172c6d --- /dev/null +++ b/packages/babel-plugin-transform-new-target/test/fixtures/general/class/expected.js @@ -0,0 +1,10 @@ +class Foo { + constructor() { + this.constructor; + } + + test() { + void 0; + } + +} diff --git a/packages/babel-plugin-transform-new-target/test/fixtures/general/extended-class/actual.js b/packages/babel-plugin-transform-new-target/test/fixtures/general/extended-class/actual.js new file mode 100644 index 000000000000..420f160b66f5 --- /dev/null +++ b/packages/babel-plugin-transform-new-target/test/fixtures/general/extended-class/actual.js @@ -0,0 +1,20 @@ +class Foo { + constructor() { + new.target; + } +} + +class Bar extends Foo { + constructor() { + // This is probably bad... + new.target; + super(); + } +} + +class Baz extends Foo { + constructor() { + super(); + new.target; + } +} diff --git a/packages/babel-plugin-transform-new-target/test/fixtures/general/extended-class/expected.js b/packages/babel-plugin-transform-new-target/test/fixtures/general/extended-class/expected.js new file mode 100644 index 000000000000..f13f146bbda9 --- /dev/null +++ b/packages/babel-plugin-transform-new-target/test/fixtures/general/extended-class/expected.js @@ -0,0 +1,23 @@ +class Foo { + constructor() { + this.constructor; + } + +} + +class Bar extends Foo { + constructor() { + // This is probably bad... + this.constructor; + super(); + } + +} + +class Baz extends Foo { + constructor() { + super(); + this.constructor; + } + +} \ No newline at end of file diff --git a/packages/babel-plugin-transform-new-target/test/fixtures/general/function/actual.js b/packages/babel-plugin-transform-new-target/test/fixtures/general/function/actual.js new file mode 100644 index 000000000000..67fd631d5ac5 --- /dev/null +++ b/packages/babel-plugin-transform-new-target/test/fixtures/general/function/actual.js @@ -0,0 +1,11 @@ +function Foo() { + new.target; +} + +Foo.prototype.test = function() { + new.target; +}; + +var Bar = function() { + new.target; +}; diff --git a/packages/babel-plugin-transform-new-target/test/fixtures/general/function/expected.js b/packages/babel-plugin-transform-new-target/test/fixtures/general/function/expected.js new file mode 100644 index 000000000000..da441520ffab --- /dev/null +++ b/packages/babel-plugin-transform-new-target/test/fixtures/general/function/expected.js @@ -0,0 +1,11 @@ +function Foo() { + this instanceof Foo ? this.constructor : void 0; +} + +Foo.prototype.test = function _target() { + this instanceof _target ? this.constructor : void 0; +}; + +var Bar = function _target2() { + this instanceof _target2 ? this.constructor : void 0; +}; \ No newline at end of file diff --git a/packages/babel-plugin-transform-new-target/test/fixtures/general/object/actual.js b/packages/babel-plugin-transform-new-target/test/fixtures/general/object/actual.js new file mode 100644 index 000000000000..c231a30a581c --- /dev/null +++ b/packages/babel-plugin-transform-new-target/test/fixtures/general/object/actual.js @@ -0,0 +1,11 @@ +"use strict"; + +const object = { + test() { + new.target; + }, + + test2: function() { + new.target; + }, +} diff --git a/packages/babel-plugin-transform-new-target/test/fixtures/general/object/expected.js b/packages/babel-plugin-transform-new-target/test/fixtures/general/object/expected.js new file mode 100644 index 000000000000..624556217298 --- /dev/null +++ b/packages/babel-plugin-transform-new-target/test/fixtures/general/object/expected.js @@ -0,0 +1,11 @@ +"use strict"; + +const object = { + test() { + void 0; + }, + + test2: function _target() { + this instanceof _target ? this.constructor : void 0; + } +}; \ No newline at end of file diff --git a/packages/babel-plugin-transform-new-target/test/fixtures/options.json b/packages/babel-plugin-transform-new-target/test/fixtures/options.json new file mode 100644 index 000000000000..1cf7776014ef --- /dev/null +++ b/packages/babel-plugin-transform-new-target/test/fixtures/options.json @@ -0,0 +1,4 @@ +{ + "plugins": ["transform-new-target"], + "minNodeVersion": "4.0.0" +} diff --git a/packages/babel-plugin-transform-new-target/test/index.js b/packages/babel-plugin-transform-new-target/test/index.js new file mode 100644 index 000000000000..09cfbc31f54e --- /dev/null +++ b/packages/babel-plugin-transform-new-target/test/index.js @@ -0,0 +1,3 @@ +import runner from "babel-helper-plugin-test-runner"; + +runner(__dirname);