feat: support import specifier guard (#20320) · webpack/webpack@cd4793d
@@ -12,6 +12,7 @@ const {
1212 getImportAttributes
1313} = require("../javascript/JavascriptParser");
1414const InnerGraph = require("../optimize/InnerGraph");
15+const AppendOnlyStackedSet = require("../util/AppendOnlyStackedSet");
1516const ConstDependency = require("./ConstDependency");
1617const HarmonyAcceptDependency = require("./HarmonyAcceptDependency");
1718const HarmonyAcceptImportDependency = require("./HarmonyAcceptImportDependency");
@@ -36,9 +37,18 @@ const { ImportPhaseUtils, createGetImportPhase } = require("./ImportPhase");
3637/** @typedef {import("../javascript/JavascriptParser").Members} Members */
3738/** @typedef {import("../javascript/JavascriptParser").MembersOptionals} MembersOptionals */
3839/** @typedef {import("./HarmonyImportDependency").Ids} Ids */
40+/** @typedef {import("./HarmonyImportDependency").ExportPresenceMode} ExportPresenceMode */
3941/** @typedef {import("./ImportPhase").ImportPhaseType} ImportPhaseType */
404243+/**
44+ * @typedef {object} HarmonySpecifierGuards
45+ * @property {AppendOnlyStackedSet<string> | undefined} guards
46+ */
47+48+/** @typedef {Map<string, Set<string>>} Guards Map of import root to guarded member keys */
49+4150const harmonySpecifierTag = Symbol("harmony import");
51+const harmonySpecifierGuardTag = Symbol("harmony import guard");
42524353/**
4454 * @typedef {object} HarmonySettings
@@ -53,6 +63,18 @@ const harmonySpecifierTag = Symbol("harmony import");
53635464const PLUGIN_NAME = "HarmonyImportDependencyParserPlugin";
556566+/** @type {(members: Members) => string} */
67+const getMembersKey = (members) => members.join(".");
68+69+/**
70+ * Strip the root binding name if needed
71+ * @param {HarmonySettings} settings settings
72+ * @param {Ids} ids ids
73+ * @returns {Ids} ids for presence check
74+ */
75+const getIdsForPresence = (settings, ids) =>
76+settings.ids.length ? ids.slice(1) : ids;
77+5678module.exports = class HarmonyImportDependencyParserPlugin {
5779/**
5880 * @param {JavascriptParserOptions} options options
@@ -70,13 +92,30 @@ module.exports = class HarmonyImportDependencyParserPlugin {
7092this.strictThisContextOnImports = options.strictThisContextOnImports;
7193}
729495+/**
96+ * @param {JavascriptParser} parser the parser
97+ * @param {Ids} ids ids
98+ * @returns {ExportPresenceMode} exportPresenceMode
99+ */
100+getExportPresenceMode(parser, ids) {
101+const harmonySettings = /** @type {HarmonySettings=} */ (
102+parser.currentTagData
103+);
104+if (!harmonySettings) return this.exportPresenceMode;
105+106+const data = /** @type {HarmonySpecifierGuards=} */ (
107+parser.getTagData(harmonySettings.name, harmonySpecifierGuardTag)
108+);
109+return data && data.guards && data.guards.has(getMembersKey(ids))
110+ ? false
111+ : this.exportPresenceMode;
112+}
113+73114/**
74115 * @param {JavascriptParser} parser the parser
75116 * @returns {void}
76117 */
77118apply(parser) {
78-const { exportPresenceMode } = this;
79-80119const getImportPhase = createGetImportPhase(this.options.deferImport);
8112082121/**
@@ -228,14 +267,18 @@ module.exports = class HarmonyImportDependencyParserPlugin {
228267.for(harmonySpecifierTag)
229268.tap(PLUGIN_NAME, (expr) => {
230269const settings = /** @type {HarmonySettings} */ (parser.currentTagData);
270+231271const dep = new HarmonyImportSpecifierDependency(
232272settings.source,
233273settings.sourceOrder,
234274settings.ids,
235275settings.name,
236276/** @type {Range} */
237277(expr.range),
238-exportPresenceMode,
278+this.getExportPresenceMode(
279+parser,
280+getIdsForPresence(settings, settings.ids)
281+),
239282settings.phase,
240283settings.attributes,
241284[]
@@ -285,7 +328,10 @@ module.exports = class HarmonyImportDependencyParserPlugin {
285328settings.name,
286329/** @type {Range} */
287330(expr.range),
288-exportPresenceMode,
331+this.getExportPresenceMode(
332+parser,
333+getIdsForPresence(settings, ids)
334+),
289335settings.phase,
290336settings.attributes,
291337ranges
@@ -335,7 +381,10 @@ module.exports = class HarmonyImportDependencyParserPlugin {
335381ids,
336382settings.name,
337383/** @type {Range} */ (expr.range),
338-exportPresenceMode,
384+this.getExportPresenceMode(
385+parser,
386+getIdsForPresence(settings, ids)
387+),
339388settings.phase,
340389settings.attributes,
341390ranges
@@ -402,7 +451,219 @@ module.exports = class HarmonyImportDependencyParserPlugin {
402451parser.state.module.addDependency(dep);
403452}
404453});
454+455+/**
456+ * @param {Expression} expression expression
457+ * @returns {{ root: string, members: Members } | undefined} info
458+ */
459+const getHarmonyImportInfo = (expression) => {
460+const nameInfo = parser.getNameForExpression(expression);
461+if (!nameInfo) return;
462+463+const rootInfo = nameInfo.rootInfo;
464+const root =
465+typeof rootInfo === "string"
466+ ? rootInfo
467+ : rootInfo instanceof VariableInfo
468+ ? rootInfo.name
469+ : undefined;
470+if (!root) return;
471+if (!parser.getTagData(root, harmonySpecifierTag)) return;
472+return { root, members: nameInfo.getMembers() };
473+};
474+475+/**
476+ * @param {Guards} guards guards
477+ * @param {string} root root name
478+ * @param {Members} members members
479+ */
480+const addToGuards = (guards, root, members) => {
481+const membersKey = getMembersKey(members);
482+const guardedMembers = guards.get(root);
483+if (guardedMembers) {
484+guardedMembers.add(membersKey);
485+return;
486+}
487+488+guards.set(
489+root,
490+// Adding `foo.bar` implies guarding `foo` as well
491+membersKey === "" ? new Set([""]) : new Set([membersKey, ""])
492+);
493+};
494+495+/**
496+ * @param {Expression} expression expression
497+ * @param {Guards} guards guards
498+ * @param {boolean} needTruthy need to be truthy
499+ */
500+const collect = (expression, guards, needTruthy) => {
501+// !foo
502+if (
503+expression.type === "UnaryExpression" &&
504+expression.operator === "!"
505+) {
506+collect(expression.argument, guards, !needTruthy);
507+return;
508+} else if (expression.type === "LogicalExpression" && needTruthy) {
509+// foo && bar
510+if (expression.operator === "&&") {
511+collect(expression.left, guards, true);
512+collect(expression.right, guards, true);
513+}
514+// falsy || foo
515+else if (expression.operator === "||") {
516+const leftEvaluation = parser.evaluateExpression(expression.left);
517+const leftBool = leftEvaluation.asBool();
518+if (leftBool === false) {
519+collect(expression.right, guards, true);
520+}
521+}
522+// nullish ?? foo
523+else if (expression.operator === "??") {
524+const leftEvaluation = parser.evaluateExpression(expression.left);
525+const leftNullish = leftEvaluation.asNullish();
526+if (leftNullish === true) {
527+collect(expression.right, guards, true);
528+}
529+}
530+return;
531+}
532+if (!needTruthy) return;
533+534+/**
535+ * @param {Expression} targetExpression expression
536+ * @returns {boolean} is added
537+ */
538+const addGuardForExpression = (targetExpression) => {
539+const info = getHarmonyImportInfo(targetExpression);
540+if (!info) return false;
541+addToGuards(guards, info.root, info.members);
542+return true;
543+};
544+545+/**
546+ * @param {Expression} left left expression
547+ * @param {Expression} right right expression
548+ * @param {(evaluation: ReturnType<JavascriptParser["evaluateExpression"]>) => boolean} matcher matcher
549+ * @returns {boolean} is added
550+ */
551+const addGuardForNullishCompare = (left, right, matcher) => {
552+const leftEval = parser.evaluateExpression(left);
553+if (leftEval && matcher(leftEval)) {
554+return addGuardForExpression(right);
555+}
556+const rightEval = parser.evaluateExpression(right);
557+if (rightEval && matcher(rightEval)) {
558+return addGuardForExpression(/** @type {Expression} */ (left));
559+}
560+return false;
561+};
562+563+if (expression.type === "BinaryExpression") {
564+// "bar" in foo
565+if (expression.operator === "in") {
566+const leftEvaluation = parser.evaluateExpression(expression.left);
567+if (leftEvaluation.couldHaveSideEffects()) return;
568+const propertyName = leftEvaluation.asString();
569+if (!propertyName) return;
570+parser.evaluateExpression(expression.right);
571+const info = getHarmonyImportInfo(expression.right);
572+if (!info) return;
573+574+if (info.members.length) {
575+for (const member of info.members) {
576+addToGuards(guards, info.root, [member]);
577+}
578+}
579+addToGuards(guards, info.root, [...info.members, propertyName]);
580+return;
581+}
582+// foo !== undefined
583+else if (
584+expression.operator === "!==" &&
585+addGuardForNullishCompare(
586+/** @type {Expression} */ (expression.left),
587+expression.right,
588+(evaluation) => evaluation.isUndefined()
589+)
590+) {
591+return;
592+}
593+// foo != undefined
594+// foo != null
595+else if (
596+expression.operator === "!=" &&
597+addGuardForNullishCompare(
598+/** @type {Expression} */ (expression.left),
599+expression.right,
600+(evaluation) => Boolean(evaluation.asNullish())
601+)
602+) {
603+return;
604+}
605+}
606+addGuardForExpression(expression);
607+};
608+609+/**
610+ * @param {Guards} guards guards
611+ * @param {() => void} walk walk callback
612+ * @returns {void}
613+ */
614+const withGuards = (guards, walk) => {
615+const applyGuards = () => {
616+/** @type {(() => void)[]} */
617+const restoreFns = [];
618+619+for (const [rootName, members] of guards) {
620+const previous = parser.getVariableInfo(rootName);
621+const exist = /** @type {HarmonySpecifierGuards=} */ (
622+parser.getTagData(rootName, harmonySpecifierGuardTag)
623+);
624+625+const mergedGuards =
626+exist && exist.guards
627+ ? exist.guards.createChild()
628+ : new AppendOnlyStackedSet();
629+630+for (const memberKey of members) mergedGuards.add(memberKey);
631+parser.tagVariable(rootName, harmonySpecifierGuardTag, {
632+guards: mergedGuards
633+});
634+restoreFns.push(() => {
635+parser.setVariable(rootName, previous);
636+});
637+}
638+639+return () => {
640+for (const restore of restoreFns) {
641+restore();
642+}
643+};
644+};
645+646+const restore = applyGuards();
647+try {
648+walk();
649+} finally {
650+restore();
651+}
652+};
653+654+parser.hooks.collectGuards.tap(PLUGIN_NAME, (expression) => {
655+if (parser.scope.isAsmJs) return;
656+/** @type {Guards} */
657+const guards = new Map();
658+collect(expression, guards, true);
659+660+if (guards.size === 0) return;
661+return (walk) => {
662+withGuards(guards, walk);
663+};
664+});
405665}
406666};
407667668+module.exports.harmonySpecifierGuardTag = harmonySpecifierGuardTag;
408669module.exports.harmonySpecifierTag = harmonySpecifierTag;