Skip to content

Commit cd4793d

Browse files
authored
feat: support import specifier guard (#20320)
1 parent fe48655 commit cd4793d

File tree

8 files changed

+631
-18
lines changed

8 files changed

+631
-18
lines changed

.changeset/orange-suns-care.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"webpack": minor
3+
---
4+
5+
Detect conditional imports to avoid compile-time linking errors for non-existent exports.

lib/dependencies/HarmonyImportDependencyParserPlugin.js

Lines changed: 266 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const {
1212
getImportAttributes
1313
} = require("../javascript/JavascriptParser");
1414
const InnerGraph = require("../optimize/InnerGraph");
15+
const AppendOnlyStackedSet = require("../util/AppendOnlyStackedSet");
1516
const ConstDependency = require("./ConstDependency");
1617
const HarmonyAcceptDependency = require("./HarmonyAcceptDependency");
1718
const 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 */
4042

43+
/**
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+
4150
const harmonySpecifierTag = Symbol("harmony import");
51+
const harmonySpecifierGuardTag = Symbol("harmony import guard");
4252

4353
/**
4454
* @typedef {object} HarmonySettings
@@ -53,6 +63,18 @@ const harmonySpecifierTag = Symbol("harmony import");
5363

5464
const PLUGIN_NAME = "HarmonyImportDependencyParserPlugin";
5565

66+
/** @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+
5678
module.exports = class HarmonyImportDependencyParserPlugin {
5779
/**
5880
* @param {JavascriptParserOptions} options options
@@ -70,13 +92,30 @@ module.exports = class HarmonyImportDependencyParserPlugin {
7092
this.strictThisContextOnImports = options.strictThisContextOnImports;
7193
}
7294

95+
/**
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
*/
77118
apply(parser) {
78-
const { exportPresenceMode } = this;
79-
80119
const getImportPhase = createGetImportPhase(this.options.deferImport);
81120

82121
/**
@@ -228,14 +267,18 @@ module.exports = class HarmonyImportDependencyParserPlugin {
228267
.for(harmonySpecifierTag)
229268
.tap(PLUGIN_NAME, (expr) => {
230269
const settings = /** @type {HarmonySettings} */ (parser.currentTagData);
270+
231271
const dep = new HarmonyImportSpecifierDependency(
232272
settings.source,
233273
settings.sourceOrder,
234274
settings.ids,
235275
settings.name,
236276
/** @type {Range} */
237277
(expr.range),
238-
exportPresenceMode,
278+
this.getExportPresenceMode(
279+
parser,
280+
getIdsForPresence(settings, settings.ids)
281+
),
239282
settings.phase,
240283
settings.attributes,
241284
[]
@@ -285,7 +328,10 @@ module.exports = class HarmonyImportDependencyParserPlugin {
285328
settings.name,
286329
/** @type {Range} */
287330
(expr.range),
288-
exportPresenceMode,
331+
this.getExportPresenceMode(
332+
parser,
333+
getIdsForPresence(settings, ids)
334+
),
289335
settings.phase,
290336
settings.attributes,
291337
ranges
@@ -335,7 +381,10 @@ module.exports = class HarmonyImportDependencyParserPlugin {
335381
ids,
336382
settings.name,
337383
/** @type {Range} */ (expr.range),
338-
exportPresenceMode,
384+
this.getExportPresenceMode(
385+
parser,
386+
getIdsForPresence(settings, ids)
387+
),
339388
settings.phase,
340389
settings.attributes,
341390
ranges
@@ -402,7 +451,219 @@ module.exports = class HarmonyImportDependencyParserPlugin {
402451
parser.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
};
407667

668+
module.exports.harmonySpecifierGuardTag = harmonySpecifierGuardTag;
408669
module.exports.harmonySpecifierTag = harmonySpecifierTag;

0 commit comments

Comments
 (0)