Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add guards support in JavascriptParser #15497

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
36 changes: 36 additions & 0 deletions lib/dependencies/HarmonyExportPresenceImportSpecifierDependency.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Ivan Kopeykin @vankop
*/

"use strict";

const makeSerializable = require("../util/makeSerializable");
const HarmonyImportSpecifierDependency = require("./HarmonyImportSpecifierDependency");
const NullDependency = require("./NullDependency");

/** @typedef {import("webpack-sources").ReplaceSource} ReplaceSource */
/** @typedef {import("../ChunkGraph")} ChunkGraph */
/** @typedef {import("../Dependency")} Dependency */
/** @typedef {import("../DependencyTemplate").DependencyTemplateContext} DependencyTemplateContext */

/**
* Dependency just for export presence import specifier.
*/
class HarmonyExportPresenceImportSpecifierDependency extends HarmonyImportSpecifierDependency {
vankop marked this conversation as resolved.
Show resolved Hide resolved
get type() {
return "export presence harmony import specifier";
}
}

makeSerializable(
HarmonyExportPresenceImportSpecifierDependency,
"webpack/lib/dependencies/HarmonyExportPresenceImportSpecifierDependency"
);

HarmonyExportPresenceImportSpecifierDependency.Template =
/** @type {any} can't cast to HarmonyImportSpecifierDependency.Template */ (
NullDependency.Template
);

module.exports = HarmonyExportPresenceImportSpecifierDependency;
184 changes: 148 additions & 36 deletions lib/dependencies/HarmonyImportDependencyParserPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const ConstDependency = require("./ConstDependency");
const HarmonyAcceptDependency = require("./HarmonyAcceptDependency");
const HarmonyAcceptImportDependency = require("./HarmonyAcceptImportDependency");
const HarmonyEvaluatedImportSpecifierDependency = require("./HarmonyEvaluatedImportSpecifierDependency");
const HarmonyExportPresenceImportSpecifierDependency = require("./HarmonyExportPresenceImportSpecifierDependency");
const HarmonyExports = require("./HarmonyExports");
const { ExportPresenceModes } = require("./HarmonyImportDependency");
const HarmonyImportSideEffectDependency = require("./HarmonyImportSideEffectDependency");
Expand All @@ -30,6 +31,7 @@ const HarmonyImportSpecifierDependency = require("./HarmonyImportSpecifierDepend
/** @typedef {import("../javascript/BasicEvaluatedExpression")} BasicEvaluatedExpression */
/** @typedef {import("../javascript/JavascriptParser")} JavascriptParser */
/** @typedef {import("../javascript/JavascriptParser").Range} Range */
/** @typedef {import("../javascript/JavascriptParser").VariableInfoInterface} VariableInfoInterface */
/** @typedef {import("../optimize/InnerGraph").InnerGraph} InnerGraph */
/** @typedef {import("../optimize/InnerGraph").TopLevelSymbol} TopLevelSymbol */
/** @typedef {import("./HarmonyImportDependency")} HarmonyImportDependency */
Expand All @@ -46,6 +48,15 @@ const harmonySpecifierTag = Symbol("harmony import");
* @property {Record<string, any> | undefined} assertions
*/

/**
* @param {string|VariableInfoInterface|undefined} info rootInfo
* @returns {boolean} is harmonySpecifierTag
*/
function isHarmonySpecifierTag(info) {
if (!info || typeof info === "string") return false;
return info.tagInfo.tag === harmonySpecifierTag;
}

/**
* @param {ImportDeclaration | ExportNamedDeclaration | ExportAllDeclaration | (ImportExpression & { arguments?: ObjectExpression[] })} node node with assertions
* @returns {Record<string, any> | undefined} assertions
Expand Down Expand Up @@ -126,6 +137,34 @@ module.exports = class HarmonyImportDependencyParserPlugin {
return node;
}

/**
* @param {string[]} ids ids
* @returns {string} guard name
*/
const createGuard = ids => ids.join(".");
/**
* @param {string} guard guard
* @param {number} idsLength ids length
* @returns {number} mode
*/
const detectExportPresenceMode = (guard, idsLength) => {
if (
vankop marked this conversation as resolved.
Show resolved Hide resolved
exportPresenceMode === ExportPresenceModes.NONE ||
// namespace objects are safe to use
idsLength === 0 ||
parser.scope.guards.has(guard) ||
// if possible guard is in guard position,
// it is guarded by member chain minus one element. e.g.
// if (a && a.b) {} or if (a && "c" in a.b) {}
// a.b is guarded by a
(parser.scope.inGuardPosition &&
(idsLength === 1 ||
parser.scope.guards.has(guard.slice(0, guard.lastIndexOf(".")))))
)
return ExportPresenceModes.NONE;

return exportPresenceMode;
};
parser.hooks.isPure
.for("Identifier")
.tap("HarmonyImportDependencyParserPlugin", expression => {
Expand Down Expand Up @@ -179,56 +218,123 @@ module.exports = class HarmonyImportDependencyParserPlugin {
parser.hooks.binaryExpression.tap(
"HarmonyImportDependencyParserPlugin",
expression => {
if (expression.operator !== "in") return;
if (expression.operator === "in") {
const leftPartEvaluated = parser.evaluateExpression(expression.left);
if (!leftPartEvaluated || leftPartEvaluated.couldHaveSideEffects())
return;
const leftPart = leftPartEvaluated.asString();
if (!leftPart) return;

const leftPartEvaluated = parser.evaluateExpression(expression.left);
if (leftPartEvaluated.couldHaveSideEffects()) return;
const leftPart = leftPartEvaluated.asString();
if (!leftPart) return;
const rightPart = parser.evaluateExpression(expression.right);
if (!rightPart || !rightPart.isIdentifier()) return;

const rightPart = parser.evaluateExpression(expression.right);
if (!rightPart.isIdentifier()) return;
const rootInfo = rightPart.rootInfo;
if (!isHarmonySpecifierTag(rootInfo)) return;
const settings = /** @type {VariableInfoInterface} */ (rootInfo)
.tagInfo.data;
const members = rightPart.getMembers();
const baseIds = settings.ids.concat(members);
const ids = baseIds.concat([leftPart]);
const dep = new HarmonyEvaluatedImportSpecifierDependency(
settings.source,
settings.sourceOrder,
ids,
settings.name,
/** @type {Range} */ (expression.range),
settings.assertions,
"in"
);
dep.directImport = members.length === 0;
dep.asiSafe = !parser.isAsiPosition(
/** @type {Range} */ (expression.range)[0]
);
dep.loc = /** @type {DependencyLocation} */ (expression.loc);
parser.state.module.addDependency(dep);
InnerGraph.onUsage(parser.state, e => (dep.usedByExports = e));

const rootInfo = rightPart.rootInfo;
if (
typeof rootInfo === "string" ||
!rootInfo ||
!rootInfo.tagInfo ||
rootInfo.tagInfo.tag !== harmonySpecifierTag
)
return;
const settings = rootInfo.tagInfo.data;
const members = rightPart.getMembers();
const dep = new HarmonyEvaluatedImportSpecifierDependency(
settings.source,
settings.sourceOrder,
settings.ids.concat(members).concat([leftPart]),
settings.name,
/** @type {Range} */ (expression.range),
settings.assertions,
"in"
);
dep.directImport = members.length === 0;
dep.asiSafe = !parser.isAsiPosition(
/** @type {Range} */ (expression.range)[0]
);
dep.loc = /** @type {DependencyLocation} */ (expression.loc);
parser.state.module.addDependency(dep);
InnerGraph.onUsage(parser.state, e => (dep.usedByExports = e));
return true;
if (parser.scope.inGuardPosition) {
parser.scope.guards.add(createGuard(ids));
// namespace objects and identifiers are safe to use
if (baseIds.length > 1) {
// check for export presence for right side expression
const mode = detectExportPresenceMode(
createGuard(baseIds),
baseIds.length
);
// e.g if ("a" in b.c) {}
// here "b.c" is not guarded by "b"
if (mode !== ExportPresenceModes.NONE) {
const dep = new HarmonyExportPresenceImportSpecifierDependency(
settings.source,
settings.sourceOrder,
baseIds,
settings.name,
rightPart.expression.range,
mode,
settings.assertions
);
dep.loc = rightPart.expression.loc;
parser.state.module.addDependency(dep);
}
}
}

return true;
} else if (expression.operator === "!=") {
if (!parser.scope.inGuardPosition) return;
let identifierEvaluated;
const leftPartEvaluated = parser.evaluateExpression(expression.left);
if (!leftPartEvaluated) {
return;
} else if (leftPartEvaluated.isIdentifier()) {
if (!isHarmonySpecifierTag(leftPartEvaluated.rootInfo)) return;
identifierEvaluated = leftPartEvaluated;
} else if (leftPartEvaluated.isFalsy() !== true) {
return;
}

const rightPartEvaluated = parser.evaluateExpression(
expression.right
);
if (!rightPartEvaluated) {
return;
} else if (
!identifierEvaluated &&
rightPartEvaluated.isIdentifier()
) {
if (!isHarmonySpecifierTag(rightPartEvaluated.rootInfo)) return;
identifierEvaluated = rightPartEvaluated;
} else if (rightPartEvaluated.isFalsy() !== true) {
return;
}

// other hooks will add guards and dependencies
parser.walkExpression(identifierEvaluated.expression);
return true;
}
}
);
parser.hooks.expression
.for(harmonySpecifierTag)
.tap("HarmonyImportDependencyParserPlugin", expr => {
const settings = /** @type {HarmonySettings} */ (parser.currentTagData);
let exportPresenceModeComputed;
// namespace object is safe to use
if (settings.ids.length) {
const guard = createGuard(settings.ids);
exportPresenceModeComputed = detectExportPresenceMode(
guard,
settings.ids.length
);
if (parser.scope.inGuardPosition) parser.scope.guards.add(guard);
}
const dep = new HarmonyImportSpecifierDependency(
settings.source,
settings.sourceOrder,
settings.ids,
settings.name,
/** @type {Range} */ (expr.range),
exportPresenceMode,
exportPresenceModeComputed || ExportPresenceModes.NONE,
settings.assertions,
[]
);
Expand Down Expand Up @@ -269,6 +375,12 @@ module.exports = class HarmonyImportDependencyParserPlugin {
)
: expression;
const ids = settings.ids.concat(nonOptionalMembers);
const guard = createGuard(ids);
const exportPresenceMode = detectExportPresenceMode(
guard,
ids.length
);
if (parser.scope.inGuardPosition) parser.scope.guards.add(guard);
const dep = new HarmonyImportSpecifierDependency(
settings.source,
settings.sourceOrder,
Expand Down Expand Up @@ -321,7 +433,7 @@ module.exports = class HarmonyImportDependencyParserPlugin {
ids,
settings.name,
/** @type {Range} */ (expr.range),
exportPresenceMode,
detectExportPresenceMode(createGuard(ids), ids.length),
settings.assertions,
ranges
);
Expand Down
10 changes: 10 additions & 0 deletions lib/dependencies/HarmonyModulesPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const HarmonyEvaluatedImportSpecifierDependency = require("./HarmonyEvaluatedImp
const HarmonyExportExpressionDependency = require("./HarmonyExportExpressionDependency");
const HarmonyExportHeaderDependency = require("./HarmonyExportHeaderDependency");
const HarmonyExportImportedSpecifierDependency = require("./HarmonyExportImportedSpecifierDependency");
const HarmonyExportPresenceImportSpecifierDependency = require("./HarmonyExportPresenceImportSpecifierDependency");
const HarmonyExportSpecifierDependency = require("./HarmonyExportSpecifierDependency");
const HarmonyImportSideEffectDependency = require("./HarmonyImportSideEffectDependency");
const HarmonyImportSpecifierDependency = require("./HarmonyImportSpecifierDependency");
Expand Down Expand Up @@ -82,6 +83,15 @@ class HarmonyModulesPlugin {
new HarmonyEvaluatedImportSpecifierDependency.Template()
);

compilation.dependencyFactories.set(
HarmonyExportPresenceImportSpecifierDependency,
normalModuleFactory
);
compilation.dependencyTemplates.set(
HarmonyExportPresenceImportSpecifierDependency,
new HarmonyExportPresenceImportSpecifierDependency.Template()
);

compilation.dependencyTemplates.set(
HarmonyExportHeaderDependency,
new HarmonyExportHeaderDependency.Template()
Expand Down
11 changes: 11 additions & 0 deletions lib/javascript/BasicEvaluatedExpression.js
Original file line number Diff line number Diff line change
Expand Up @@ -325,18 +325,22 @@ class BasicEvaluatedExpression {
this.type = TypeString;
this.string = string;
this.sideEffects = false;
if (string === "") this.falsy = true;
else this.truthy = true;
return this;
}

setUndefined() {
this.type = TypeUndefined;
this.sideEffects = false;
this.falsy = true;
return this;
}

setNull() {
this.type = TypeNull;
this.sideEffects = false;
this.falsy = true;
return this;
}

Expand All @@ -349,6 +353,8 @@ class BasicEvaluatedExpression {
this.type = TypeNumber;
this.number = number;
this.sideEffects = false;
if (number === 0) this.falsy = true;
else this.truthy = true;
return this;
}

Expand All @@ -361,6 +367,8 @@ class BasicEvaluatedExpression {
this.type = TypeBigInt;
this.bigint = bigint;
this.sideEffects = false;
if (bigint === BigInt(0)) this.falsy = true;
else this.truthy = true;
return this;
}

Expand All @@ -373,6 +381,8 @@ class BasicEvaluatedExpression {
this.type = TypeBoolean;
this.bool = bool;
this.sideEffects = false;
if (bool === false) this.falsy = true;
else this.truthy = true;
return this;
}

Expand All @@ -385,6 +395,7 @@ class BasicEvaluatedExpression {
this.type = TypeRegExp;
this.regExp = regExp;
this.sideEffects = false;
this.truthy = true;
return this;
}

Expand Down