Skip to content

Commit

Permalink
Merge pull request #11221 from webpack/feature/optional-chaining
Browse files Browse the repository at this point in the history
Feature/optional chaining
  • Loading branch information
sokra committed Aug 7, 2020
2 parents 94beab1 + 0bb9c34 commit 792ce1d
Show file tree
Hide file tree
Showing 15 changed files with 235 additions and 2 deletions.
55 changes: 55 additions & 0 deletions lib/ConstPlugin.js
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,59 @@ class ConstPlugin {
}
}
);
parser.hooks.optionalChaining.tap("ConstPlugin", expr => {
/** @type {ExpressionNode[]} */
const optionalExpressionsStack = [];
/** @type {ExpressionNode|SuperNode} */
let next = expr.expression;

while (
next.type === "MemberExpression" ||
next.type === "CallExpression"
) {
if (next.type === "MemberExpression") {
if (next.optional) {
// SuperNode can not be optional
optionalExpressionsStack.push(
/** @type {ExpressionNode} */ (next.object)
);
}
next = next.object;
} else {
if (next.optional) {
// SuperNode can not be optional
optionalExpressionsStack.push(
/** @type {ExpressionNode} */ (next.callee)
);
}
next = next.callee;
}
}

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:
//
// undefined;
//
// ------------------------------------------
//
const dep = new ConstDependency(" undefined", expr.range);
dep.loc = expr.loc;
parser.state.module.addPresentationalDependency(dep);
return true;
}
}
});
parser.hooks.evaluateIdentifier
.for("__resourceQuery")
.tap("ConstPlugin", expr => {
Expand Down
1 change: 1 addition & 0 deletions lib/javascript/BasicEvaluatedExpression.js
Expand Up @@ -405,6 +405,7 @@ class BasicEvaluatedExpression {
setTruthy() {
this.falsy = false;
this.truthy = true;
this.nullish = false;
return this;
}

Expand Down
80 changes: 80 additions & 0 deletions lib/javascript/JavascriptParser.js
Expand Up @@ -29,6 +29,7 @@ const BasicEvaluatedExpression = require("./BasicEvaluatedExpression");
/** @typedef {import("estree").LabeledStatement} LabeledStatementNode */
/** @typedef {import("estree").Literal} LiteralNode */
/** @typedef {import("estree").LogicalExpression} LogicalExpressionNode */
/** @typedef {import("estree").ChainExpression} ChainExpressionNode */
/** @typedef {import("estree").MemberExpression} MemberExpressionNode */
/** @typedef {import("estree").MetaProperty} MetaPropertyNode */
/** @typedef {import("estree").MethodDefinition} MethodDefinitionNode */
Expand Down Expand Up @@ -260,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 @@ -755,6 +758,15 @@ class JavascriptParser extends Parser {
if (res !== undefined) return res;
break;
}
case "ChainExpression": {
const res = this.callHooksForExpression(
this.hooks.evaluateTypeof,
expr.argument.expression,
expr
);
if (res !== undefined) return res;
break;
}
case "FunctionExpression": {
return new BasicEvaluatedExpression()
.setString("function")
Expand Down Expand Up @@ -1223,6 +1235,48 @@ class JavascriptParser extends Parser {
.setItems(items)
.setRange(expr.range);
});
this.hooks.evaluate
.for("ChainExpression")
.tap("JavascriptParser", _expr => {
const expr = /** @type {ChainExpressionNode} */ (_expr);
/** @type {ExpressionNode[]} */
const optionalExpressionsStack = [];
/** @type {ExpressionNode|SuperNode} */
let next = expr.expression;

while (
next.type === "MemberExpression" ||
next.type === "CallExpression"
) {
if (next.type === "MemberExpression") {
if (next.optional) {
// SuperNode can not be optional
optionalExpressionsStack.push(
/** @type {ExpressionNode} */ (next.object)
);
}
next = next.object;
} else {
if (next.optional) {
// SuperNode can not be optional
optionalExpressionsStack.push(
/** @type {ExpressionNode} */ (next.callee)
);
}
next = next.callee;
}
}

while (optionalExpressionsStack.length > 0) {
const expression = optionalExpressionsStack.pop();
const evaluated = this.evaluateExpression(expression);

if (evaluated && evaluated.asNullish()) {
return evaluated.setRange(_expr.range);
}
}
return this.evaluateExpression(expr.expression);
});
}

getRenameIdentifier(expr) {
Expand Down Expand Up @@ -2024,6 +2078,9 @@ class JavascriptParser extends Parser {
case "CallExpression":
this.walkCallExpression(expression);
break;
case "ChainExpression":
this.walkChainExpression(expression);
break;
case "ClassExpression":
this.walkClassExpression(expression);
break;
Expand Down Expand Up @@ -2182,6 +2239,14 @@ class JavascriptParser extends Parser {
expression
);
if (result === true) return;
if (expression.argument.type === "ChainExpression") {
const result = this.callHooksForExpression(
this.hooks.typeof,
expression.argument.expression,
expression
);
if (result === true) return;
}
}
this.walkExpression(expression.argument);
}
Expand Down Expand Up @@ -2326,6 +2391,21 @@ class JavascriptParser extends Parser {
this.walkClass(expression);
}

/**
* @param {ChainExpressionNode} expression expression
*/
walkChainExpression(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);
}
}
}

_walkIIFE(functionExpression, options, currentThis) {
const getVarInfo = argOrThis => {
const renameIdentifier = this.getRenameIdentifier(argOrThis);
Expand Down
3 changes: 2 additions & 1 deletion test/TestCasesProduction.test.js
Expand Up @@ -3,6 +3,7 @@ const { describeCases } = require("./TestCases.template");
describe("TestCases", () => {
describeCases({
name: "production",
mode: "production"
mode: "production",
minimize: true
});
});
Empty file.
15 changes: 15 additions & 0 deletions test/cases/parsing/optional-chaining/index.js
@@ -0,0 +1,15 @@
it("should evaluate optional members", () => {
if (!module.hot) {
expect(
module.hot?.accept((() => {throw new Error("fail")})())
).toBe(undefined);
}
});

it("should evaluate optional chaining as a part of statement", () => {
if (module.hot?.accept) {
module.hot?.accept("./a.js");
} else {
expect(module.hot).toBe(undefined);
}
});
5 changes: 5 additions & 0 deletions test/cases/parsing/optional-chaining/test.filter.js
@@ -0,0 +1,5 @@
const supportsOptionalChaining = require("../../../helpers/supportsOptionalChaining");

module.exports = function (config) {
return !config.minimize && supportsOptionalChaining();
};
28 changes: 28 additions & 0 deletions test/configCases/parsing/optional-chaining/index.js
@@ -0,0 +1,28 @@
it("should correctly render defined data #1", () => {
expect(_VALUE_?._DEFINED_).toBe(1);
});

it("should correctly render defined data #2", () => {
const val1 = _VALUE_?._PROP_?._DEFINED_;
const val2 = _VALUE_?._PROP_?._UNDEFINED_;
const val3 = typeof _VALUE_?._PROP_?._DEFINED_;
const val4 = typeof _VALUE_?._PROP_?._UNDEFINED_;
const val5 = _VALUE_?._PROP_;
const val6 = typeof _VALUE_?._PROP_;
expect(val1).toBe(2);
expect(val2).toBeUndefined();
expect(val3).toBe("number");
expect(val4).toBe("undefined");
expect(val5).toEqual({ _DEFINED_: 2 });
expect(val6).toBe("object");
expect((() => typeof _VALUE_?._PROP_?._DEFINED_).toString()).toContain(
"number"
);
expect((() => typeof _VALUE_?._PROP_).toString()).toContain("object");
if (_VALUE_._PROP_._DEFINED_ !== 2) require("fail");
if (_VALUE_?._PROP_?._DEFINED_ !== 2) require("fail");
if (typeof _VALUE_._PROP_._DEFINED_ !== "number") require("fail");
if (typeof _VALUE_?._PROP_?._DEFINED_ !== "number") require("fail");
if (typeof _VALUE_._PROP_ !== "object") require("fail");
if (typeof _VALUE_?._PROP_ !== "object") require("fail");
});
5 changes: 5 additions & 0 deletions test/configCases/parsing/optional-chaining/test.filter.js
@@ -0,0 +1,5 @@
var supportsOptionalChaining = require("../../../helpers/supportsOptionalChaining");

module.exports = function (config) {
return supportsOptionalChaining();
};
18 changes: 18 additions & 0 deletions test/configCases/parsing/optional-chaining/webpack.config.js
@@ -0,0 +1,18 @@
const { DefinePlugin } = require("../../../../");

/** @type {import("../../../../").Configuration} */
module.exports = {
mode: "development",
devtool: false,
target: "web",
plugins: [
new DefinePlugin({
_VALUE_: {
_DEFINED_: 1,
_PROP_: {
_DEFINED_: 2
}
}
})
]
};
2 changes: 1 addition & 1 deletion test/helpers/supportsNullishCoalescing.js
@@ -1,4 +1,4 @@
module.exports = function supportsObjectDestructuring() {
module.exports = function supportsNullishCoalescing() {
try {
var f = eval("(function f() { return null ?? true; })");
return f();
Expand Down
8 changes: 8 additions & 0 deletions test/helpers/supportsOptionalChaining.js
@@ -0,0 +1,8 @@
module.exports = function supportsOptionalChaining() {
try {
var f = eval("(function f() { return ({a: true}) ?.a })");
return f();
} catch (e) {
return false;
}
};
3 changes: 3 additions & 0 deletions test/hotCases/parsing/hot-api-optional-chaining/a.js
@@ -0,0 +1,3 @@
export default 1;
---
export default 2;
12 changes: 12 additions & 0 deletions test/hotCases/parsing/hot-api-optional-chaining/index.js
@@ -0,0 +1,12 @@
import value from "./a";

it("should run module.hot.accept(…)", function (done) {
expect(value).toBe(1);
module?.hot?.accept("./a", function () {});
NEXT(
require("../../update")(done, true, () => {
expect(value).toBe(2);
done();
})
);
});
2 changes: 2 additions & 0 deletions types.d.ts
Expand Up @@ -3459,6 +3459,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 Expand Up @@ -3563,6 +3564,7 @@ declare abstract class JavascriptParser extends Parser {
walkTemplateLiteral(expression?: any): void;
walkTaggedTemplateExpression(expression?: any): void;
walkClassExpression(expression?: any): void;
walkChainExpression(expression: ChainExpression): void;
walkImportExpression(expression?: any): void;
walkCallExpression(expression?: any, args?: any): void;
walkMemberExpression(expression?: any): void;
Expand Down

0 comments on commit 792ce1d

Please sign in to comment.