Skip to content

Commit

Permalink
add evaluation to optional chaining
Browse files Browse the repository at this point in the history
  • Loading branch information
vankop committed Jul 23, 2020
1 parent be6e61a commit 236e763
Show file tree
Hide file tree
Showing 5 changed files with 78 additions and 13 deletions.
47 changes: 47 additions & 0 deletions lib/ConstPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ const ConstDependency = require("./dependencies/ConstDependency");
const { evaluateToString } = require("./javascript/JavascriptParserHelpers");
const { parseResource } = require("./util/identifier");

/** @typedef {import("estree").Expression} ExpressionNode */
/** @typedef {import("estree").Super} SuperNode */
/** @typedef {import("./Compiler")} Compiler */

const collectDeclaration = (declarations, pattern) => {
Expand Down Expand Up @@ -372,6 +374,51 @@ class ConstPlugin {
}
}
);
parser.hooks.optionalChaining.tap("ConstPlugin", expr => {
const optionalExpressionsStack = [];
/** @type {ExpressionNode|SuperNode} */
let next;

if (expr.expression.type === "CallExpression") {
next = expr.expression.callee;
} else {
next = expr.expression;
}

while (next.type === "MemberExpression") {
if (next.optional) {
optionalExpressionsStack.push(next.object);
}
next = next.object;
}

while (optionalExpressionsStack.length) {
const expression = optionalExpressionsStack.pop();
const evaluated = parser.evaluateExpression(expression);

if (evaluated && evaluated.asNullish()) {
// ------------------------------------------
//
// Given the following code:
//
// nullishMemberChain?.a.b();
//
// the generated code is:
//
// null; // or undefined; if evaluated to undefined
//
// ------------------------------------------
//
const dep = new ConstDependency(
evaluated.isUndefined() ? "undefined" : "null",
expr.range
);
dep.loc = expr.loc;
parser.state.module.addPresentationalDependency(dep);
return true;
}
}
});
parser.hooks.evaluateIdentifier
.for("__resourceQuery")
.tap("ConstPlugin", expr => {
Expand Down
37 changes: 24 additions & 13 deletions lib/javascript/JavascriptParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,8 @@ class JavascriptParser extends Parser {
"members"
])
),
/** @type {SyncBailHook<[ChainExpressionNode], boolean | void>} */
optionalChaining: new SyncBailHook(["optionalChaining"]),
/** @type {HookMap<SyncBailHook<[ExpressionNode], boolean | void>>} */
new: new HookMap(() => new SyncBailHook(["expression"])),
/** @type {SyncBailHook<[MetaPropertyNode], boolean | void>} */
Expand Down Expand Up @@ -1228,9 +1230,11 @@ class JavascriptParser extends Parser {
.for("ChainExpression")
.tap("JavascriptParser", _expr => {
const expr = /** @type {ChainExpressionNode} */ (_expr);
const result = this.evaluateExpression(expr.expression);
let result = this.evaluateExpression(expr.expression);
if (result) return result;

/** @type {ExpressionNode[]} */
const stack = [];
/** @type {ExpressionNode|SuperNode} */
let next;

Expand All @@ -1241,22 +1245,25 @@ class JavascriptParser extends Parser {
}

while (next.type === "MemberExpression") {
while (next.type === "MemberExpression" && !next.optional) {
next = next.object;
if (next.optional) {
stack.push(/** @type {ExpressionNode} */ (next.object));
}

const result = this.evaluateExpression(expr.expression);
if (result) {
result.setRange(_expr.range);
if (result.isNull()) result.setUndefined();
next = next.object;
}

return result;
while (stack.length) {
const expression = stack.pop();
const evaluated = this.evaluateExpression(expression);

if (evaluated && evaluated.asNullish()) {
return evaluated.setRange(_expr.range);
}
}

return new BasicEvaluatedExpression()
.setRange(_expr.range)
.setUndefined();
.setExpression(_expr);
});
}

Expand Down Expand Up @@ -2368,10 +2375,14 @@ class JavascriptParser extends Parser {
* @param {ChainExpressionNode} expression expression
*/
walkChainExpression(expression) {
if (expression.expression.type === "CallExpression") {
this.walkCallExpression(expression.expression);
} else {
this.walkMemberExpression(expression.expression);
const result = this.hooks.optionalChaining.call(expression);

if (result === undefined) {
if (expression.expression.type === "CallExpression") {
this.walkCallExpression(expression.expression);
} else {
this.walkMemberExpression(expression.expression);
}
}
}

Expand Down
1 change: 1 addition & 0 deletions test/cases/parsing/optional-chaining/a.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = 1;
5 changes: 5 additions & 0 deletions test/cases/parsing/optional-chaining/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
it("should handle optional members", () => {
expect(
module.hot?.accept((() => {throw new Error("fail")})())
).toBe(null);
});
1 change: 1 addition & 0 deletions types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3332,6 +3332,7 @@ declare abstract class JavascriptParser extends Parser {
boolean | void
>
>;
optionalChaining: SyncBailHook<[ChainExpression], boolean | void>;
new: HookMap<SyncBailHook<[Expression], boolean | void>>;
metaProperty: SyncBailHook<[MetaProperty], boolean | void>;
expression: HookMap<SyncBailHook<[Expression], boolean | void>>;
Expand Down

0 comments on commit 236e763

Please sign in to comment.