Implement MemberExpressionToFunctions helper (#7763)

* Implement MemberExpressionToFunctions helper

Fixes #7733.

This will also be used to simplify the Private Fields transform, which had [almost the same code](ccd941057a/packages/babel-plugin-proposal-class-properties/src/index.js (L114-L217)) hand written.

* Cleanup

* Little more comment cleanup

* Use unary plus

This can't be redefined, unlike the `Number` identifier.

* Review comments

* Remove unused deps
This commit is contained in:
Justin Ridgewell 2018-04-21 13:13:42 -04:00 committed by GitHub
parent dbdce0e4e4
commit 8f24f91166
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 252 additions and 213 deletions

View File

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

View File

@ -0,0 +1,66 @@
# @babel/helper-member-expression-to-functions
Helper function to replace certain member expressions with function calls
## Usage
> Designed for internal Babel use.
Traverses the `path` using the supplied `visitor` and an augmented `state`.
```js
const visitor = {
MemberExpression(memberPath, state) {
if (someCondition(memberPath)) {
// The handle method is supplied by memberExpressionToFunctions.
// It should be called whenever a MemberExpression should be
// converted into the proper function calls.
state.handle(memberPath);
}
},
};
// The helper requires three special methods on state: `get`, `set`, and
// `call`.
// Everything else will be passed through as normal.
const state = {
get(memberPath) {
// Return some AST that will get the member
return t.callExpression(
this.file.addHelper('superGet'),
[t.thisExpression(), memberPath.node.property]
);
},
get(memberPath, value) {
// Return some AST that will set the member
return t.callExpression(
this.file.addHelper('superSet'),
[t.thisExpression(), memberPath.node.property, value]
);
},
call(memberPath, args) {
// Return some AST that will call the member with the proper context
// and args
return t.callExpression(
t.memberExpression(this.get(memberPath), t.identifier("apply")),
[t.thisExpression(), t.arrayExpression(args)]
);
},
// The handle method is provided by memberExpressionToFunctions.
// handle(memberPath) { ... }
// Other state stuff is left untouched.
someState: new Set(),
};
// Replace all the special MemberExpressions in rootPath, as determined
// by our visitor, using the state methods.
memberExpressionToFunctions(rootPath, visitor, state);
```

View File

@ -0,0 +1,12 @@
{
"name": "@babel/helper-member-expression-to-functions",
"version": "7.0.0-beta.44",
"description": "Helper function to replace certain member expressions with function calls",
"repository": "https://github.com/babel/babel/tree/master/packages/babel-helper-member-expression-to-functions",
"license": "MIT",
"main": "lib/index.js",
"author": "Justin Ridgewell <justin@ridgewell.name>",
"dependencies": {
"@babel/types": "7.0.0-beta.44"
}
}

View File

@ -0,0 +1,72 @@
import * as t from "@babel/types";
const handle = {
handle(member) {
const { node, parent, parentPath } = member;
// MEMBER++ -> _set(MEMBER, (_ref = (+_get(MEMBER))) + 1), _ref
// ++MEMBER -> _set(MEMBER, (+_get(MEMBER)) + 1)
if (parentPath.isUpdateExpression({ argument: node })) {
const { operator, prefix } = parent;
const value = t.binaryExpression(
operator[0],
t.unaryExpression("+", this.get(member)),
t.numericLiteral(1),
);
if (prefix) {
parentPath.replaceWith(this.set(member, value));
} else {
const { scope } = member;
const ref = scope.generateUidIdentifierBasedOnNode(node);
scope.push({ id: ref });
value.left = t.assignmentExpression("=", t.cloneNode(ref), value.left);
parentPath.replaceWith(
t.sequenceExpression([this.set(member, value), t.cloneNode(ref)]),
);
}
return;
}
// MEMBER = VALUE -> _set(MEMBER, VALUE)
// MEMBER += VALUE -> _set(MEMBER, _get(MEMBER) + VALUE)
if (parentPath.isAssignmentExpression({ left: node })) {
const { operator, right } = parent;
let value = right;
if (operator !== "=") {
value = t.binaryExpression(
operator.slice(0, -1),
this.get(member),
value,
);
}
parentPath.replaceWith(this.set(member, value));
return;
}
// MEMBER(ARGS) -> _call(MEMBER, ARGS)
if (parentPath.isCallExpression({ callee: node })) {
const { arguments: args } = parent;
parentPath.replaceWith(this.call(member, args));
return;
}
// MEMBER -> _get(MEMBER)
member.replaceWith(this.get(member));
},
};
// We do not provide a default traversal visitor
// Instead, caller passes one, and must call `state.handle` on the members
// it wishes to be transformed.
// Additionally, the caller must pass in a state object with at least
// get, set, and call methods.
export default function memberExpressionToFunctions(path, visitor, state) {
path.traverse(visitor, Object.assign({}, state, handle));
}

View File

@ -6,8 +6,8 @@
"license": "MIT",
"main": "lib/index.js",
"dependencies": {
"@babel/helper-member-expression-to-functions": "7.0.0-beta.44",
"@babel/helper-optimise-call-expression": "7.0.0-beta.44",
"@babel/template": "7.0.0-beta.44",
"@babel/traverse": "7.0.0-beta.44",
"@babel/types": "7.0.0-beta.44"
}

View File

@ -1,5 +1,6 @@
import type { NodePath, Scope } from "@babel/traverse";
import traverse from "@babel/traverse";
import memberExpressionToFunctions from "@babel/helper-member-expression-to-functions";
import optimiseCall from "@babel/helper-optimise-call-expression";
import * as t from "@babel/types";
@ -74,11 +75,83 @@ const visitor = traverse.visitors.merge([
state.bareSupers.add(parentPath);
return;
}
state[state.isLoose ? "looseHandle" : "specHandle"](path);
state.handle(parentPath);
},
},
]);
const specHandlers = {
get(superMember) {
const { computed, property } = superMember.node;
let thisExpr = t.thisExpression();
// TODO Remove
if (this.inConstructor) {
thisExpr = t.callExpression(
this.file.addHelper("assertThisInitialized"),
[thisExpr],
);
}
return t.callExpression(this.file.addHelper("get"), [
getPrototypeOfExpression(this.getObjectRef(), this.isStatic, this.file),
computed ? property : t.stringLiteral(property.name),
thisExpr,
]);
},
set(superMember, value) {
const { computed, property } = superMember.node;
return t.callExpression(this.file.addHelper("set"), [
getPrototypeOfExpression(this.getObjectRef(), this.isStatic, this.file),
computed ? property : t.stringLiteral(property.name),
value,
t.thisExpression(),
t.booleanLiteral(superMember.isInStrictMode()),
]);
},
call(superMember, args) {
return optimiseCall(this.get(superMember), t.thisExpression(), args);
},
};
const looseHandlers = {
get(superMember) {
const { isStatic, superRef } = this;
const { property, computed } = superMember.node;
let object;
if (isStatic) {
object = superRef
? t.cloneNode(superRef)
: t.memberExpression(
t.identifier("Function"),
t.identifier("prototype"),
);
} else {
object = superRef
? t.memberExpression(t.cloneNode(superRef), t.identifier("prototype"))
: t.memberExpression(t.identifier("Object"), t.identifier("prototype"));
}
return t.memberExpression(object, property, computed);
},
set(superMember, value) {
// TODO https://github.com/babel/babel/pull/7553#issuecomment-381434519
return t.assignmentExpression("=", this.get(superMember), value);
},
call(superMember, args) {
return t.callExpression(
t.memberExpression(this.get(superMember), t.identifier("call")),
[t.thisExpression(), ...args],
);
},
};
export default class ReplaceSupers {
constructor(opts: Object) {
const path = opts.methodPath;
@ -119,205 +192,24 @@ export default class ReplaceSupers {
return t.cloneNode(this.opts.objectRef || this.opts.getObjectRef());
}
/**
* Sets a super class value of the named property.
*
* @example
*
* _set(Object.getPrototypeOf(CLASS.prototype), "METHOD", "VALUE", this, isStrict)
*
*/
setSuperProperty(
property: Object,
value: Object,
isComputed: boolean,
isStrict: boolean,
): Object {
return t.callExpression(this.file.addHelper("set"), [
getPrototypeOfExpression(this.getObjectRef(), this.isStatic, this.file),
isComputed ? property : t.stringLiteral(property.name),
value,
t.thisExpression(),
t.booleanLiteral(isStrict),
]);
}
/**
* Gets a node representing the super class value of the named property.
*
* @example
*
* _get(Object.getPrototypeOf(CLASS.prototype), "METHOD", this)
*
*/
getSuperProperty(property: Object, isComputed: boolean): Object {
let thisExpr = t.thisExpression();
if (this.inConstructor) {
thisExpr = t.callExpression(
this.file.addHelper("assertThisInitialized"),
[thisExpr],
);
}
return t.callExpression(this.file.addHelper("get"), [
getPrototypeOfExpression(this.getObjectRef(), this.isStatic, this.file),
isComputed ? property : t.stringLiteral(property.name),
thisExpr,
]);
}
replace() {
this.methodPath.traverse(visitor, this);
}
const { get, set, call } = this.isLoose ? looseHandlers : specHandlers;
getLooseSuperProperty(path) {
const { isStatic, superRef } = this;
memberExpressionToFunctions(this.methodPath, visitor, {
get,
set,
call,
let object;
if (isStatic) {
object = superRef
? t.cloneNode(superRef)
: t.memberExpression(
t.identifier("Function"),
t.identifier("prototype"),
);
} else {
object = superRef
? t.memberExpression(t.cloneNode(superRef), t.identifier("prototype"))
: t.memberExpression(t.identifier("Object"), t.identifier("prototype"));
}
path.get("object").replaceWith(object);
}
// Necessary state
file: this.file,
isStatic: this.isStatic,
getObjectRef: this.getObjectRef.bind(this),
superRef: this.superRef,
looseHandle(path: NodePath) {
const { node, parentPath } = path;
// super.test
if (parentPath.isMemberExpression({ object: node })) {
this.getLooseSuperProperty(parentPath);
}
// super.test()
// though, it's SUPER.prototype.test() after the above.
const grandParentPath = parentPath.parentPath;
const callee = parentPath.node;
if (grandParentPath.isCallExpression({ callee })) {
grandParentPath
.get("callee")
.replaceWith(t.memberExpression(callee, t.identifier("call")));
grandParentPath.node.arguments.unshift(t.thisExpression());
}
}
specHandleAssignmentExpression(path) {
const { node } = path;
const { operator } = node;
const { computed, property } = node.left;
if (operator === "=") {
// super.name = "val"
// to
// _set(Object.getPrototypeOf(CLASS.prototype), "name", this);
const setter = this.setSuperProperty(
property,
node.right,
computed,
path.isInStrictMode(),
);
return [setter];
}
// super.age += 2;
// to
// _set(
// Object.getPrototypeOf(CLASS.prototype),
// "name",
// _get(Object.getPrototypeOf(CLASS.prototype), "METHOD", this) + 2,
// this,
// true,
// );
// TODO this needs cleanup. Should be a single proto lookup
const { scope } = path;
const ref = scope.generateUidIdentifierBasedOnNode(node);
scope.push({ id: ref });
const setter = this.setSuperProperty(
property,
t.binaryExpression(operator.slice(0, -1), t.cloneNode(ref), node.right),
computed,
path.isInStrictMode(),
);
return [
t.assignmentExpression(
"=",
t.cloneNode(ref),
this.getSuperProperty(property, computed),
),
setter,
];
}
specHandle(path: NodePath) {
const { node, parentPath } = path;
const grandParentPath = parentPath.parentPath;
let parent = parentPath.node;
if (grandParentPath.isUpdateExpression({ argument: parent })) {
const { operator, prefix } = grandParentPath.node;
const assignment = t.assignmentExpression(
operator[0] + "=",
parent,
t.numericLiteral(1),
);
grandParentPath.replaceWith(assignment);
// ++super.foo;
// to
// _ref = Number(super.foo), super.foo = _ref + 1
// super.foo++;
// to
// _ref = Number(super.foo), super.foo = _ref + 1, _ref
const nodes = this.specHandleAssignmentExpression(grandParentPath);
const [first] = nodes;
first.right = t.callExpression(t.identifier("Number"), [first.right]);
// Postfix returns the old value, not the new.
if (!prefix) {
nodes.push(t.cloneNode(first.left));
}
grandParentPath.replaceWith(t.sequenceExpression(nodes));
return;
}
if (grandParentPath.isAssignmentExpression({ left: parent })) {
grandParentPath.replaceWithMultiple(
this.specHandleAssignmentExpression(grandParentPath),
);
return;
}
if (parentPath.isMemberExpression({ object: node })) {
// super.name;
// to
// _get(Object.getPrototypeOf(CLASS.prototype), "name", this);
const { node } = parentPath;
const { computed, property } = node;
parent = this.getSuperProperty(property, computed);
parentPath.replaceWith(parent);
}
if (grandParentPath.isCallExpression({ callee: parent })) {
// _get(Object.getPrototypeOf(CLASS.prototype), "test", this)();
// to
// _get(Object.getPrototypeOf(CLASS.prototype), "test", this).call(this);
const call = this.optimiseCall(parent, grandParentPath.node.arguments);
grandParentPath.replaceWith(call);
return;
}
}
optimiseCall(callee, args) {
return optimiseCall(callee, t.thisExpression(), args);
// TODO Remove this shit.
inConstructor: this.inConstructor,
returns: this.returns,
bareSupers: this.bareSupers,
});
}
}

View File

@ -29,7 +29,7 @@ function (_Point) {
babelHelpers.classCallCheck(this, ColorPoint);
_this = babelHelpers.possibleConstructorReturn(this, babelHelpers.getPrototypeOf(ColorPoint).call(this));
_this.x = 2;
babelHelpers.set(babelHelpers.getPrototypeOf(ColorPoint.prototype), "x", 3, _this, true)
babelHelpers.set(babelHelpers.getPrototypeOf(ColorPoint.prototype), "x", 3, _this, true);
expect(_this.x).toBe(3); // A
expect(babelHelpers.get(babelHelpers.getPrototypeOf(ColorPoint.prototype), "x", babelHelpers.assertThisInitialized(_this))).toBeUndefined(); // B

View File

@ -1,22 +1,20 @@
var _obj;
function _get(target, property, receiver) { if (typeof Reflect !== "undefined" && Reflect.get) { _get = Reflect.get; } else { _get = function _get(target, property, receiver) { var base = _superPropBase(target, property); if (!base) return; var desc = Object.getOwnPropertyDescriptor(base, property); if (desc.get) { return desc.get.call(receiver); } return desc.value; }; } return _get(target, property, receiver || target); }
function set(target, property, value, receiver) { if (typeof Reflect !== "undefined" && Reflect.set) { set = Reflect.set; } else { set = function set(target, property, value, receiver) { var base = _superPropBase(target, property); var desc; if (base) { desc = Object.getOwnPropertyDescriptor(base, property); if (desc.set) { desc.set.call(receiver, value); return true; } else if (!desc.writable) { return false; } } desc = Object.getOwnPropertyDescriptor(receiver, property); if (desc) { if (!desc.writable) { return false; } desc.value = value; Object.defineProperty(receiver, property, desc); } else { _defineProperty(receiver, property, value); } return true; }; } return set(target, property, value, receiver); }
function _set(target, property, value, receiver, isStrict) { const s = set(target, property, value, receiver || target); if (!s && isStrict) { throw new Error('failed to set property'); } return value; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function _get(target, property, receiver) { if (typeof Reflect !== "undefined" && Reflect.get) { _get = Reflect.get; } else { _get = function _get(target, property, receiver) { var base = _superPropBase(target, property); if (!base) return; var desc = Object.getOwnPropertyDescriptor(base, property); if (desc.get) { return desc.get.call(receiver); } return desc.value; }; } return _get(target, property, receiver || target); }
function _superPropBase(object, property) { while (!Object.prototype.hasOwnProperty.call(object, property)) { object = _getPrototypeOf(object); if (object === null) break; } return object; }
function _getPrototypeOf(o) { _getPrototypeOf = Object.getPrototypeOf || function _getPrototypeOf(o) { return o.__proto__; }; return _getPrototypeOf(o); }
foo = _obj = {
bar() {
var _super$baz;
return _super$baz = _get(_getPrototypeOf(_obj), "baz", this), _set(_getPrototypeOf(_obj), "baz", Math.pow(_super$baz, 12), this, false);
return _set(_getPrototypeOf(_obj), "baz", Math.pow(_get(_getPrototypeOf(_obj), "baz", this), 12), this, false);
}
};

View File

@ -1,21 +1,19 @@
var _obj;
function _get(target, property, receiver) { if (typeof Reflect !== "undefined" && Reflect.get) { _get = Reflect.get; } else { _get = function _get(target, property, receiver) { var base = _superPropBase(target, property); if (!base) return; var desc = Object.getOwnPropertyDescriptor(base, property); if (desc.get) { return desc.get.call(receiver); } return desc.value; }; } return _get(target, property, receiver || target); }
function set(target, property, value, receiver) { if (typeof Reflect !== "undefined" && Reflect.set) { set = Reflect.set; } else { set = function set(target, property, value, receiver) { var base = _superPropBase(target, property); var desc; if (base) { desc = Object.getOwnPropertyDescriptor(base, property); if (desc.set) { desc.set.call(receiver, value); return true; } else if (!desc.writable) { return false; } } desc = Object.getOwnPropertyDescriptor(receiver, property); if (desc) { if (!desc.writable) { return false; } desc.value = value; Object.defineProperty(receiver, property, desc); } else { _defineProperty(receiver, property, value); } return true; }; } return set(target, property, value, receiver); }
function _set(target, property, value, receiver, isStrict) { const s = set(target, property, value, receiver || target); if (!s && isStrict) { throw new Error('failed to set property'); } return value; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function _get(target, property, receiver) { if (typeof Reflect !== "undefined" && Reflect.get) { _get = Reflect.get; } else { _get = function _get(target, property, receiver) { var base = _superPropBase(target, property); if (!base) return; var desc = Object.getOwnPropertyDescriptor(base, property); if (desc.get) { return desc.get.call(receiver); } return desc.value; }; } return _get(target, property, receiver || target); }
function _superPropBase(object, property) { while (!Object.prototype.hasOwnProperty.call(object, property)) { object = _getPrototypeOf(object); if (object === null) break; } return object; }
function _getPrototypeOf(o) { _getPrototypeOf = Object.getPrototypeOf || function _getPrototypeOf(o) { return o.__proto__; }; return _getPrototypeOf(o); }
foo = _obj = {
bar: function () {
var _super$baz;
return _super$baz = _get(_getPrototypeOf(_obj), "baz", this), _set(_getPrototypeOf(_obj), "baz", _super$baz ** 12, this, false);
return _set(_getPrototypeOf(_obj), "baz", _get(_getPrototypeOf(_obj), "baz", this) ** 12, this, false);
}
};

View File

@ -7,7 +7,7 @@ var obj = _obj = {
bar: function () {
var _super$test;
return _super$test = Number(babelHelpers.get(babelHelpers.getPrototypeOf(_obj), "test", this)), babelHelpers.set(babelHelpers.getPrototypeOf(_obj), "test", _super$test + 1, this, false), _super$test;
return babelHelpers.set(babelHelpers.getPrototypeOf(_obj), "test", (_super$test = +babelHelpers.get(babelHelpers.getPrototypeOf(_obj), "test", this)) + 1, this, false), _super$test;
}
};
Object.setPrototypeOf(obj, Base);

View File

@ -5,9 +5,7 @@ var Base = {
};
var obj = _obj = {
bar: function () {
var _super$test;
return _super$test = Number(babelHelpers.get(babelHelpers.getPrototypeOf(_obj), "test", this)), babelHelpers.set(babelHelpers.getPrototypeOf(_obj), "test", _super$test + 1, this, false);
return babelHelpers.set(babelHelpers.getPrototypeOf(_obj), "test", +babelHelpers.get(babelHelpers.getPrototypeOf(_obj), "test", this) + 1, this, false);
}
};
Object.setPrototypeOf(obj, Base);