Add new.target transform (#5906)

* Add new.target transform

* Catch new.target under only an arrow function

* More unsupported reflect.construct cases

* Fix node 4 test

* Do not transform Methods

* More tests

* Properly setup function inheritance test

* Tests tests tests

* Fix ES6 class's new.target

* Remove expected output thats supposed to throw.
This commit is contained in:
Justin Ridgewell 2017-07-07 14:28:19 -04:00 committed by GitHub
parent 003b8918c2
commit 628061c501
31 changed files with 515 additions and 0 deletions

View File

@ -0,0 +1,3 @@
src
test
*.log

View File

@ -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"]
});
```

View File

@ -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"
}
}

View File

@ -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(),
),
);
}
},
},
};
}

View File

@ -0,0 +1,5 @@
"use strict";
const a = () => {
new.target;
};

View File

@ -0,0 +1,3 @@
{
"throws": "new.target must be under a (non-arrow) function or a class."
}

View File

@ -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);

View File

@ -0,0 +1,12 @@
"use strict";
const targets = [];
class Foo {
constructor() {
targets.push(new.target);
}
}
new Foo;
assert.equal(targets[0], Foo);

View File

@ -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);

View File

@ -0,0 +1,10 @@
"use strict";
const targets = [];
function Foo() {
targets.push(new.target);
}
new Foo;
assert.equal(targets[0], Foo);

View File

@ -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);

View File

@ -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);

View File

@ -0,0 +1,3 @@
{
"minNodeVersion": "6.0.0"
}

View File

@ -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);
});

View File

@ -0,0 +1,3 @@
{
"minNodeVersion": "6.0.0"
}

View File

@ -0,0 +1,13 @@
function Foo() {
const a = () => {
new.target;
};
}
class Bar {
constructor() {
const a = () => {
new.target;
};
}
}

View File

@ -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;
};
}
}

View File

@ -0,0 +1,3 @@
{
"plugins": ["transform-new-target", "transform-es2015-arrow-functions"]
}

View File

@ -0,0 +1,9 @@
class Foo {
test = function() {
new.target;
};
test2 = () => {
new.target;
}
}

View File

@ -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;
};
}
}

View File

@ -0,0 +1,3 @@
{
"plugins": ["transform-new-target", "transform-es2015-arrow-functions", "transform-class-properties"]
}

View File

@ -0,0 +1,9 @@
class Foo {
constructor() {
new.target;
}
test() {
new.target;
}
}

View File

@ -0,0 +1,10 @@
class Foo {
constructor() {
this.constructor;
}
test() {
void 0;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,11 @@
function Foo() {
new.target;
}
Foo.prototype.test = function() {
new.target;
};
var Bar = function() {
new.target;
};

View File

@ -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;
};

View File

@ -0,0 +1,11 @@
"use strict";
const object = {
test() {
new.target;
},
test2: function() {
new.target;
},
}

View File

@ -0,0 +1,11 @@
"use strict";
const object = {
test() {
void 0;
},
test2: function _target() {
this instanceof _target ? this.constructor : void 0;
}
};

View File

@ -0,0 +1,4 @@
{
"plugins": ["transform-new-target"],
"minNodeVersion": "4.0.0"
}

View File

@ -0,0 +1,3 @@
import runner from "babel-helper-plugin-test-runner";
runner(__dirname);