Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new.target transform #5906

Merged
merged 10 commits into from Jul 7, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/babel-plugin-transform-new-target/.npmignore
@@ -0,0 +1,3 @@
src
test
*.log
104 changes: 104 additions & 0 deletions 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"]
});
```
18 changes: 18 additions & 0 deletions 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"
}
}
64 changes: 64 additions & 0 deletions 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(),
),
);
}
},
},
};
}
@@ -0,0 +1,5 @@
"use strict";

const a = () => {
new.target;
};
@@ -0,0 +1,3 @@
{
"throws": "new.target must be under a (non-arrow) function or a class."
}
@@ -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);
@@ -0,0 +1,12 @@
"use strict";

const targets = [];
class Foo {
constructor() {
targets.push(new.target);
}
}

new Foo;

assert.equal(targets[0], Foo);
@@ -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);
@@ -0,0 +1,10 @@
"use strict";

const targets = [];
function Foo() {
targets.push(new.target);
}

new Foo;

assert.equal(targets[0], Foo);
@@ -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);
@@ -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);
@@ -0,0 +1,3 @@
{
"minNodeVersion": "6.0.0"
}
@@ -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);
});
@@ -0,0 +1,3 @@
{
"minNodeVersion": "6.0.0"
}
@@ -0,0 +1,13 @@
function Foo() {
const a = () => {
new.target;
};
}

class Bar {
constructor() {
const a = () => {
new.target;
};
}
}
@@ -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;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why this variable should have a 2 suffix?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's how babel generateUidIdentifier works, it's unique globally.


const a = function () {
_newtarget2;
};
}

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