feat: support import specifier guard (#20320) · webpack/webpack@cd4793d

@@ -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 */

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+4150

const 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");

53635464

const 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+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

}

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

*/

77118

apply(parser) {

78-

const { exportPresenceMode } = this;

79-80119

const getImportPhase = createGetImportPhase(this.options.deferImport);

8112082121

/**

@@ -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

};

407667668+

module.exports.harmonySpecifierGuardTag = harmonySpecifierGuardTag;

408669

module.exports.harmonySpecifierTag = harmonySpecifierTag;