Skip to content

Commit

Permalink
Fix: correctly transform this.#m?.(...arguments) (#12350)
Browse files Browse the repository at this point in the history
* add tests on @babel/helper-optimise-call-expression

* fix: correctly optimise `a.b?.(...arguments)`

* add integration test with properties transform
  • Loading branch information
JLHwung committed Nov 16, 2020
1 parent 7850682 commit f54f1ee
Show file tree
Hide file tree
Showing 12 changed files with 277 additions and 2 deletions.
4 changes: 4 additions & 0 deletions packages/babel-helper-optimise-call-expression/package.json
Expand Up @@ -14,5 +14,9 @@
"main": "lib/index.js",
"dependencies": {
"@babel/types": "workspace:^7.10.4"
},
"devDependencies": {
"@babel/generator": "workspace:*",
"@babel/parser": "workspace:*"
}
}
30 changes: 28 additions & 2 deletions packages/babel-helper-optimise-call-expression/src/index.js
@@ -1,24 +1,50 @@
import * as t from "@babel/types";

export default function (callee, thisNode, args, optional) {
/**
* A helper function that generates a new call expression with given thisNode.
It will also optimize `(...arguments)` to `.apply(arguments)`
*
* @export
* @param {Node} callee The callee of call expression
* @param {Node} thisNode The desired this of call expression
* @param {Node[]} args The arguments of call expression
* @param {boolean} optional Whether the call expression is optional
* @returns {CallExpression | OptionalCallExpression} The generated new call expression
*/
export default function (
callee: Node,
thisNode: Node,
args: Node[],
optional: boolean,
): CallExpression | OptionalCallExpression {
if (
args.length === 1 &&
t.isSpreadElement(args[0]) &&
t.isIdentifier(args[0].argument, { name: "arguments" })
) {
// eg. super(...arguments);
// a.b?.(...arguments);
if (optional) {
return t.optionalCallExpression(
t.optionalMemberExpression(callee, t.identifier("apply"), false, true),
[thisNode, args[0].argument],
false,
);
}
// a.b(...arguments);
return t.callExpression(t.memberExpression(callee, t.identifier("apply")), [
thisNode,
args[0].argument,
]);
} else {
// a.b?.(arg1, arg2)
if (optional) {
return t.optionalCallExpression(
t.optionalMemberExpression(callee, t.identifier("call"), false, true),
[thisNode, ...args],
false,
);
}
// a.b(arg1, arg2)
return t.callExpression(t.memberExpression(callee, t.identifier("call")), [
thisNode,
...args,
Expand Down
103 changes: 103 additions & 0 deletions packages/babel-helper-optimise-call-expression/test/index.js
@@ -0,0 +1,103 @@
import { parse } from "@babel/parser";
import generator from "@babel/generator";
import * as t from "@babel/types";
import optimizeCallExpression from "..";

function transformInput(input, thisIdentifier) {
const ast = parse(input);
const callExpression = ast.program.body[0].expression;
return generator(
optimizeCallExpression(
callExpression.callee,
thisIdentifier
? t.identifier(thisIdentifier)
: callExpression.callee.object,
callExpression.arguments,
callExpression.type === "OptionalCallExpression",
),
).code;
}

describe("@babel/helper-optimise-call-expression", () => {
test("optimizeCallExpression should work when thisNode is implied from callee", () => {
expect(transformInput("a.b(...arguments)")).toMatchInlineSnapshot(
`"a.b.apply(a, arguments)"`,
);
expect(transformInput("a[b](...arguments)")).toMatchInlineSnapshot(
`"a[b].apply(a, arguments)"`,
);
expect(transformInput("a.b?.(...arguments)")).toMatchInlineSnapshot(
`"a.b?.apply(a, arguments)"`,
);
expect(transformInput("a[b]?.(...arguments)")).toMatchInlineSnapshot(
`"a[b]?.apply(a, arguments)"`,
);

expect(transformInput("a.b(...args)")).toMatchInlineSnapshot(
`"a.b.call(a, ...args)"`,
);
expect(transformInput("a[b](...args)")).toMatchInlineSnapshot(
`"a[b].call(a, ...args)"`,
);
expect(transformInput("a.b?.(...args)")).toMatchInlineSnapshot(
`"a.b?.call(a, ...args)"`,
);
expect(transformInput("a[b]?.(...args)")).toMatchInlineSnapshot(
`"a[b]?.call(a, ...args)"`,
);

expect(transformInput("a.b(arg1, arg2)")).toMatchInlineSnapshot(
`"a.b.call(a, arg1, arg2)"`,
);
expect(transformInput("a[b](arg1, arg2)")).toMatchInlineSnapshot(
`"a[b].call(a, arg1, arg2)"`,
);
expect(transformInput("a.b?.(arg1, arg2)")).toMatchInlineSnapshot(
`"a.b?.call(a, arg1, arg2)"`,
);
expect(transformInput("a[b]?.(arg1, arg2)")).toMatchInlineSnapshot(
`"a[b]?.call(a, arg1, arg2)"`,
);
});

test("optimizeCallExpression should work when thisNode is provided", () => {
expect(transformInput("a.b(...arguments)", "c")).toMatchInlineSnapshot(
`"a.b.apply(c, arguments)"`,
);
expect(transformInput("a[b](...arguments)", "c")).toMatchInlineSnapshot(
`"a[b].apply(c, arguments)"`,
);
expect(transformInput("a.b?.(...arguments)", "c")).toMatchInlineSnapshot(
`"a.b?.apply(c, arguments)"`,
);
expect(transformInput("a[b]?.(...arguments)", "c")).toMatchInlineSnapshot(
`"a[b]?.apply(c, arguments)"`,
);

expect(transformInput("a.b(...args)", "c")).toMatchInlineSnapshot(
`"a.b.call(c, ...args)"`,
);
expect(transformInput("a[b](...args)", "c")).toMatchInlineSnapshot(
`"a[b].call(c, ...args)"`,
);
expect(transformInput("a.b?.(...args)", "c")).toMatchInlineSnapshot(
`"a.b?.call(c, ...args)"`,
);
expect(transformInput("a[b]?.(...args)", "c")).toMatchInlineSnapshot(
`"a[b]?.call(c, ...args)"`,
);

expect(transformInput("a.b(arg1, arg2)", "c")).toMatchInlineSnapshot(
`"a.b.call(c, arg1, arg2)"`,
);
expect(transformInput("a[b](arg1, arg2)", "c")).toMatchInlineSnapshot(
`"a[b].call(c, arg1, arg2)"`,
);
expect(transformInput("a.b?.(arg1, arg2)", "c")).toMatchInlineSnapshot(
`"a.b?.call(c, arg1, arg2)"`,
);
expect(transformInput("a[b]?.(arg1, arg2)", "c")).toMatchInlineSnapshot(
`"a[b]?.call(c, arg1, arg2)"`,
);
});
});
@@ -0,0 +1,19 @@
class Foo {
#m;
init() {
this.#m = (...args) => args;
}
static test() {
const f = new Foo();
f.init();
return f.#m?.(...arguments);
}

static testNull() {
const f = new Foo();
return f.#m?.(...arguments);
}
}

expect(Foo.test(1, 2)).toEqual([1, 2]);
expect(Foo.testNull(1, 2)).toBe(undefined);
@@ -0,0 +1,16 @@
class Foo {
#m;
init() {
this.#m = (...args) => args;
}
static test() {
const f = new Foo();
f.init();
return f.#m?.(...arguments);
}

static testNull() {
const f = new Foo();
return f.#m?.(...arguments);
}
}
@@ -0,0 +1,4 @@
{
"plugins": [["proposal-class-properties", { "loose": true }]],
"minNodeVersion": "14.0.0"
}
@@ -0,0 +1,32 @@
function _classPrivateFieldLooseBase(receiver, privateKey) { if (!Object.prototype.hasOwnProperty.call(receiver, privateKey)) { throw new TypeError("attempted to use private field on non-instance"); } return receiver; }

var id = 0;

function _classPrivateFieldLooseKey(name) { return "__private_" + id++ + "_" + name; }

var _m = _classPrivateFieldLooseKey("m");

class Foo {
constructor() {
Object.defineProperty(this, _m, {
writable: true,
value: void 0
});
}

init() {
_classPrivateFieldLooseBase(this, _m)[_m] = (...args) => args;
}

static test() {
const f = new Foo();
f.init();
return _classPrivateFieldLooseBase(f, _m)[_m]?.(...arguments);
}

static testNull() {
const f = new Foo();
return _classPrivateFieldLooseBase(f, _m)[_m]?.(...arguments);
}

}
@@ -0,0 +1,19 @@
class Foo {
#m;
init() {
this.#m = (...args) => args;
}
static test() {
const f = new Foo();
f.init();
return f.#m?.(...arguments);
}

static testNull() {
const f = new Foo();
return f.#m?.(...arguments);
}
}

expect(Foo.test(1, 2)).toEqual([1, 2]);
expect(Foo.testNull(1, 2)).toBe(undefined);
@@ -0,0 +1,16 @@
class Foo {
#m;
init() {
this.#m = (...args) => args;
}
static test() {
const f = new Foo();
f.init();
return f.#m?.(...arguments);
}

static testNull() {
const f = new Foo();
return f.#m?.(...arguments);
}
}
@@ -0,0 +1,4 @@
{
"plugins": ["proposal-class-properties"],
"minNodeVersion": "14.0.0"
}
@@ -0,0 +1,30 @@
function _classPrivateFieldGet(receiver, privateMap) { var descriptor = privateMap.get(receiver); if (!descriptor) { throw new TypeError("attempted to get private field on non-instance"); } if (descriptor.get) { return descriptor.get.call(receiver); } return descriptor.value; }

function _classPrivateFieldSet(receiver, privateMap, value) { var descriptor = privateMap.get(receiver); if (!descriptor) { throw new TypeError("attempted to set private field on non-instance"); } if (descriptor.set) { descriptor.set.call(receiver, value); } else { if (!descriptor.writable) { throw new TypeError("attempted to set read only private field"); } descriptor.value = value; } return value; }

var _m = new WeakMap();

class Foo {
constructor() {
_m.set(this, {
writable: true,
value: void 0
});
}

init() {
_classPrivateFieldSet(this, _m, (...args) => args);
}

static test() {
const f = new Foo();
f.init();
return _classPrivateFieldGet(f, _m)?.apply(f, arguments);
}

static testNull() {
const f = new Foo();
return _classPrivateFieldGet(f, _m)?.apply(f, arguments);
}

}
2 changes: 2 additions & 0 deletions yarn.lock
Expand Up @@ -610,6 +610,8 @@ __metadata:
version: 0.0.0-use.local
resolution: "@babel/helper-optimise-call-expression@workspace:packages/babel-helper-optimise-call-expression"
dependencies:
"@babel/generator": "workspace:*"
"@babel/parser": "workspace:*"
"@babel/types": "workspace:^7.10.4"
languageName: unknown
linkType: soft
Expand Down

0 comments on commit f54f1ee

Please sign in to comment.