Implement Null Propagation Operator

This commit is contained in:
Justin Ridgewell
2017-06-02 03:51:04 -04:00
parent 750b03a22f
commit 3fae121460
8 changed files with 139 additions and 173 deletions

View File

@@ -1,161 +1,105 @@
export default function ({ types: t }) {
const optionalNodesTransformed = new WeakSet();
const nilIdentifier = t.unaryExpression("void", t.numericLiteral(0));
function setOptionalTransformed(node) {
t.assertMemberExpression(node); // Dev
optionalNodesTransformed.add(node);
}
function isOptionalTransformed(node) {
t.assertMemberExpression(node); // Dev
return optionalNodesTransformed.has(node);
}
function createCondition(ref, access, nextProperty, bailout) {
return t.conditionalExpression(
createCheckAgainstNull(
t.AssignmentExpression("=", ref, access)
),
t.memberExpression(ref, nextProperty),
bailout,
);
}
function createCheckAgainstNull(left) {
return t.BinaryExpression("!=", left, t.NullLiteral());
}
function isNodeOptional(node) {
return node.optional === true;
}
return {
visitor: {
AssignmentExpression(path, state) {
const { left } = path.node;
if (!isNodeOptional(left) || isOptionalTransformed(left)) {
return;
}
if (!state.optionalTemp) {
const id = path.scope.generateUidIdentifier();
state.optionalTemp = id;
path.scope.push({ id });
}
const { object, property } = left;
const isChainNil = t.BinaryExpression(
"!=",
createCondition(
state.optionalTemp,
object,
property,
nilIdentifier,
),
nilIdentifier,
);
// FIXME(sven): if will be a ConditionalExpression for childs, only top level will be ifStatement
const replacement = t.ifStatement(isChainNil, t.blockStatement([t.expressionStatement(path.node)]));
setOptionalTransformed(left);
path.traverse({
MemberExpression({ node }) {
setOptionalTransformed(node);
},
});
path.parentPath.replaceWith(replacement);
},
UnaryExpression(path, state) {
const { operator, argument } = path.node;
if (operator !== "delete") {
return;
}
if (!isNodeOptional(argument) || isOptionalTransformed(argument)) {
return;
}
if (!state.optionalTemp) {
const id = path.scope.generateUidIdentifier();
state.optionalTemp = id;
path.scope.push({ id });
}
const { object, property } = argument;
const isChainNil = t.BinaryExpression(
"!=",
createCondition(
state.optionalTemp,
object,
property,
nilIdentifier,
),
nilIdentifier,
);
const replacement = t.ifStatement(isChainNil, t.blockStatement([t.expressionStatement(path.node)]));
setOptionalTransformed(argument);
path.parentPath.replaceWith(replacement);
},
MemberExpression(path, state) {
if (!isNodeOptional(path.node) || isOptionalTransformed(path.node)) {
return;
}
const { object, property } = path.node;
if (!state.optionalTemp) {
const id = path.scope.generateUidIdentifier();
state.optionalTemp = id;
path.scope.push({ id });
}
if (t.isAssignmentExpression(path.parent)) {
return;
} else if (t.isUnaryExpression(path.parent)) {
return;
} else if (t.isCallExpression(path.parent)) {
const replacement = createCondition(
state.optionalTemp,
object,
property,
t.callExpression(t.identifier("Function"), []),
);
setOptionalTransformed(path.node);
path.replaceWith(replacement);
} else {
const replacement = createCondition(
state.optionalTemp,
object,
property,
nilIdentifier,
);
setOptionalTransformed(path.node);
// DO NOT SUBMIT. This is until the parser is complete
const fixer = {
NewExpression: {
exit(path) {
const { callee } = path.node;
if (t.isCallExpression(callee)) {
const replacement = t.newExpression(callee.callee, callee.arguments);
replacement.optional = true;
path.replaceWith(replacement);
}
},
},
CallExpression(path) {
const { node } = path;
if (!node.optional || node.callee) {
return;
}
const callee = t.clone(node.arguments[0]);
if (t.isMemberExpression(callee)) {
callee.optional = node.arguments[1].value;
}
node.callee = callee;
},
};
// END DO NOT SUBMIT
const visitor = {
Program(path) {
path.traverse(fixer);
},
MemberExpression(path) {
if (!path.node.optional) {
return;
}
const { scope, node } = path;
const { object } = node;
node.optional = false;
const ref = scope.generateUidIdentifierBasedOnNode(object);
scope.push({ id: ref });
node.object = ref;
let parent = path;
let expression;
do {
expression = parent;
parent = parent.parentPath;
} while (!parent.container);
const replace = parent.isExpression() ? parent : expression;
replace.replaceWith(t.conditionalExpression(
t.binaryExpression("==", t.assignmentExpression("=", ref, object), t.nullLiteral()),
scope.buildUndefinedNode(),
replace.node
));
},
"NewExpression|CallExpression": {
exit(path) {
if (!path.node.optional) {
return;
}
const { scope, node } = path;
const { callee } = node;
node.optional = false;
const ref = scope.generateUidIdentifierBasedOnNode(callee);
scope.push({ id: ref });
node.callee = ref;
if (t.isMemberExpression(callee) && !t.isNewExpression(node)) {
const context = scope.generateUidIdentifierBasedOnNode(callee.object);
scope.push({ id: context });
callee.object = t.assignmentExpression("=", context, callee.object);
node.arguments.unshift(context);
node.callee = t.memberExpression(node.callee, t.identifier("call"));
}
path.replaceWith(t.conditionalExpression(
t.binaryExpression("==", t.assignmentExpression("=", ref, callee), t.nullLiteral()),
scope.buildUndefinedNode(),
node
));
},
},
};
return {
visitor,
manipulateOptions(opts, parserOpts) {
parserOpts.plugins.push("optionalChaining");
},
};
}

View File

@@ -1,9 +1,5 @@
var _temp;
var _a, _a$b$c, _a$b, _a2;
if (((_temp = a) != null ? _temp.b : void 0) != void 0) {
a.b = 42;
}
(_a = a) == null ? void 0 : _a.b = 42;
if (((_temp = a.b.c) != null ? _temp.d : void 0) != void 0) {
a.b.c.d = 42;
}
(((_a2 = a) == null ? void 0 : _a$b = _a2.b) == null ? void 0 : _a$b$c = _a$b.c) == null ? void 0 : _a$b$c.d = 42;

View File

@@ -1,9 +1,5 @@
var _temp;
var _a, _a$b$c, _a$b, _a2;
if (((_temp = a) != null ? _temp.b : void 0) != void 0) {
delete a.b;
}
(_a = a) == null ? void 0 : delete _a.b;
if (((_temp = ((_temp = a) != null ? _temp.b : void 0).c) != null ? _temp.d : void 0) != void 0) {
delete ((_temp = a.b) != null ? _temp.c : void 0).d;
}
(((_a2 = a) == null ? void 0 : _a$b = _a2.b) == null ? void 0 : _a$b$c = _a$b.c) == null ? void 0 : delete _a$b$c.d;

View File

@@ -1 +1,7 @@
foo?.(foo);
foo?.bar()
foo.bar?.(foo.bar, false)
foo?.bar?.(foo.bar, true)

View File

@@ -1,3 +1,9 @@
var _temp;
var _foo, _foo2, _foo$bar, _foo3, _foo4, _foo4$bar, _foo5;
((_temp = foo) != null ? _temp.bar : Function())();
(_foo = foo) == null ? void 0 : _foo(foo);
(_foo2 = foo) == null ? void 0 : _foo2.bar();
(_foo$bar = (_foo3 = foo).bar) == null ? void 0 : _foo$bar.call(_foo3, foo.bar, false);
(_foo4 = foo) == null ? void 0 : (_foo4$bar = (_foo5 = _foo4).bar) == null ? void 0 : _foo4$bar.call(_foo5, foo.bar, true);

View File

@@ -1,5 +1,5 @@
var _temp;
var _foo, _a$b$c, _a;
(_temp = foo) != null ? _temp.bar : void 0;
(_foo = foo) == null ? void 0 : _foo.bar;
((_temp = ((_temp = a) != null ? _temp.b : void 0).c) != null ? _temp.d : void 0).e;
(_a$b$c = (_a = a) == null ? void 0 : _a.b.c) == null ? void 0 : _a$b$c.d.e;

View File

@@ -0,0 +1,8 @@
new a?.b
new a?.b?.c?.d
new a?.b()
new a?.b?.c?.d()
new b?.(b)
new a?.b?.(a.b, true)

View File

@@ -0,0 +1,10 @@
var _a, _a$b$c, _a$b, _a2, _a3, _a$b$c2, _a$b2, _a4, _b, _a5, _a5$b;
(_a = a) == null ? void 0 : new _a.b();
(((_a2 = a) == null ? void 0 : _a$b = _a2.b) == null ? void 0 : _a$b$c = _a$b.c) == null ? void 0 : new _a$b$c.d();
(_a3 = a) == null ? void 0 : new _a3.b();
(((_a4 = a) == null ? void 0 : _a$b2 = _a4.b) == null ? void 0 : _a$b$c2 = _a$b2.c) == null ? void 0 : new _a$b$c2.d();
(_b = b) == null ? void 0 : new _b(b);
(_a5 = a) == null ? void 0 : (_a5$b = _a5.b) == null ? void 0 : new _a5$b(a.b, true);