diff --git a/lib/dependencies/HarmonyExportPresenceImportSpecifierDependency.js b/lib/dependencies/HarmonyExportPresenceImportSpecifierDependency.js new file mode 100644 index 00000000000..d98185f8b14 --- /dev/null +++ b/lib/dependencies/HarmonyExportPresenceImportSpecifierDependency.js @@ -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 { + 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; diff --git a/lib/dependencies/HarmonyImportDependencyParserPlugin.js b/lib/dependencies/HarmonyImportDependencyParserPlugin.js index 4e2bc216e8e..aa9c90aa708 100644 --- a/lib/dependencies/HarmonyImportDependencyParserPlugin.js +++ b/lib/dependencies/HarmonyImportDependencyParserPlugin.js @@ -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"); @@ -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 */ @@ -46,6 +48,15 @@ const harmonySpecifierTag = Symbol("harmony import"); * @property {Record | 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 | undefined} assertions @@ -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 ( + 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 => { @@ -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, [] ); @@ -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, @@ -321,7 +433,7 @@ module.exports = class HarmonyImportDependencyParserPlugin { ids, settings.name, /** @type {Range} */ (expr.range), - exportPresenceMode, + detectExportPresenceMode(createGuard(ids), ids.length), settings.assertions, ranges ); diff --git a/lib/dependencies/HarmonyModulesPlugin.js b/lib/dependencies/HarmonyModulesPlugin.js index a3bbd98de82..1e31c01a9a6 100644 --- a/lib/dependencies/HarmonyModulesPlugin.js +++ b/lib/dependencies/HarmonyModulesPlugin.js @@ -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"); @@ -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() diff --git a/lib/javascript/BasicEvaluatedExpression.js b/lib/javascript/BasicEvaluatedExpression.js index 9306b030bc4..98fb8258d99 100644 --- a/lib/javascript/BasicEvaluatedExpression.js +++ b/lib/javascript/BasicEvaluatedExpression.js @@ -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; } @@ -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; } @@ -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; } @@ -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; } @@ -385,6 +395,7 @@ class BasicEvaluatedExpression { this.type = TypeRegExp; this.regExp = regExp; this.sideEffects = false; + this.truthy = true; return this; } diff --git a/lib/javascript/JavascriptParser.js b/lib/javascript/JavascriptParser.js index 2a940a73d5a..70d2af500d0 100644 --- a/lib/javascript/JavascriptParser.js +++ b/lib/javascript/JavascriptParser.js @@ -10,6 +10,7 @@ const { importAssertions } = require("acorn-import-assertions"); const { SyncBailHook, HookMap } = require("tapable"); const vm = require("vm"); const Parser = require("../Parser"); +const AppendOnlyStackedSet = require("../util/AppendOnlyStackedSet"); const StackedMap = require("../util/StackedMap"); const binarySearchBounds = require("../util/binarySearchBounds"); const memoize = require("../util/memoize"); @@ -137,12 +138,14 @@ class VariableInfo { /** * @typedef {Object} ScopeInfo * @property {StackedMap} definitions + * @property {AppendOnlyStackedSet} guards * @property {boolean | "arrow"} topLevelScope * @property {boolean | string} inShorthand * @property {boolean} inTaggedTemplateTag * @property {boolean} inTry * @property {boolean} isStrict * @property {boolean} isAsmJs + * @property {boolean} inGuardPosition */ /** @typedef {[number, number]} Range */ @@ -2018,8 +2021,11 @@ class JavascriptParser extends Parser { walkIfStatement(statement) { const result = this.hooks.statementIf.call(statement); if (result === undefined) { - this.walkExpression(statement.test); + const oldGuard = this.scope.guards; + this.scope.guards = oldGuard.createChild(); + this.inGuardPosition(() => this.walkExpression(statement.test), true); this.walkNestedStatement(statement.consequent); + this.scope.guards = oldGuard; if (statement.alternate) { this.walkNestedStatement(statement.alternate); } @@ -2968,7 +2974,7 @@ class JavascriptParser extends Parser { walkAwaitExpression(expression) { if (this.scope.topLevelScope === true) this.hooks.topLevelAwait.call(expression); - this.walkExpression(expression.argument); + this.inGuardPosition(() => this.walkExpression(expression.argument), false); } /** @@ -2976,7 +2982,10 @@ class JavascriptParser extends Parser { */ walkArrayExpression(expression) { if (expression.elements) { - this.walkExpressions(expression.elements); + this.inGuardPosition( + () => this.walkExpressions(expression.elements), + false + ); } } @@ -2999,7 +3008,7 @@ class JavascriptParser extends Parser { propIndex++ ) { const prop = expression.properties[propIndex]; - this.walkProperty(prop); + this.inGuardPosition(() => this.walkProperty(prop), false); } } @@ -3092,12 +3101,15 @@ class JavascriptParser extends Parser { const old = /** @type {StatementPathItem} */ (this.statementPath.pop()); for (const expr of expression.expressions) { this.statementPath.push(expr); - this.walkExpression(expr); + this.inGuardPosition(() => this.walkExpression(expr), false); this.statementPath.pop(); } this.statementPath.push(old); } else { - this.walkExpressions(expression.expressions); + this.inGuardPosition( + () => this.walkExpressions(expression.expressions), + false + ); } } @@ -3105,7 +3117,7 @@ class JavascriptParser extends Parser { * @param {UpdateExpression} expression the update expression */ walkUpdateExpression(expression) { - this.walkExpression(expression.argument); + this.inGuardPosition(() => this.walkExpression(expression.argument), false); } /** @@ -3128,7 +3140,7 @@ class JavascriptParser extends Parser { if (result === true) return; } } - this.walkExpression(expression.argument); + this.inGuardPosition(() => this.walkExpression(expression.argument), false); } /** @@ -3144,7 +3156,10 @@ class JavascriptParser extends Parser { */ walkBinaryExpression(expression) { if (this.hooks.binaryExpression.call(expression) === undefined) { - this.walkLeftRightExpression(expression); + this.inGuardPosition( + () => this.walkLeftRightExpression(expression), + false + ); } } @@ -3154,7 +3169,14 @@ class JavascriptParser extends Parser { walkLogicalExpression(expression) { const result = this.hooks.expressionLogicalOperator.call(expression); if (result === undefined) { - this.walkLeftRightExpression(expression); + if (expression.operator === "&&") { + this.walkLeftRightExpression(expression); + } else { + this.inGuardPosition( + () => this.walkLeftRightExpression(expression), + false + ); + } } else { if (result) { this.walkExpression(expression.right); @@ -3241,8 +3263,11 @@ class JavascriptParser extends Parser { walkConditionalExpression(expression) { const result = this.hooks.expressionConditionalOperator.call(expression); if (result === undefined) { - this.walkExpression(expression.test); + const oldGuard = this.scope.guards; + this.scope.guards = oldGuard.createChild(); + this.inGuardPosition(() => this.walkExpression(expression.test), true); this.walkExpression(expression.consequent); + this.scope.guards = oldGuard; if (expression.alternate) { this.walkExpression(expression.alternate); } @@ -3265,10 +3290,12 @@ class JavascriptParser extends Parser { expression ); if (result === true) return; - this.walkExpression(expression.callee); - if (expression.arguments) { - this.walkExpressions(expression.arguments); - } + this.inGuardPosition(() => { + this.walkExpression(expression.callee); + if (expression.arguments) { + this.walkExpressions(expression.arguments); + } + }, false); } /** @@ -3276,7 +3303,10 @@ class JavascriptParser extends Parser { */ walkYieldExpression(expression) { if (expression.argument) { - this.walkExpression(expression.argument); + this.inGuardPosition( + () => this.walkExpression(expression.argument), + false + ); } } @@ -3285,7 +3315,10 @@ class JavascriptParser extends Parser { */ walkTemplateLiteral(expression) { if (expression.expressions) { - this.walkExpressions(expression.expressions); + this.inGuardPosition( + () => this.walkExpressions(expression.expressions), + false + ); } } @@ -3293,14 +3326,16 @@ class JavascriptParser extends Parser { * @param {TaggedTemplateExpression} expression tagged template expression */ walkTaggedTemplateExpression(expression) { - if (expression.tag) { - this.scope.inTaggedTemplateTag = true; - this.walkExpression(expression.tag); - this.scope.inTaggedTemplateTag = false; - } - if (expression.quasi && expression.quasi.expressions) { - this.walkExpressions(expression.quasi.expressions); - } + this.inGuardPosition(() => { + if (expression.tag) { + this.scope.inTaggedTemplateTag = true; + this.walkExpression(expression.tag); + this.scope.inTaggedTemplateTag = false; + } + if (expression.quasi && expression.quasi.expressions) { + this.walkExpressions(expression.quasi.expressions); + } + }, false); } /** @@ -3411,7 +3446,7 @@ class JavascriptParser extends Parser { let result = this.hooks.importCall.call(expression); if (result === true) return; - this.walkExpression(expression.source); + this.inGuardPosition(() => this.walkExpression(expression.source), false); } /** @@ -3495,17 +3530,19 @@ class JavascriptParser extends Parser { if (result2 === true) return; } - if (expression.callee) { - if (expression.callee.type === "MemberExpression") { - // because of call context we need to walk the call context as expression - this.walkExpression(expression.callee.object); - if (expression.callee.computed === true) - this.walkExpression(expression.callee.property); - } else { - this.walkExpression(expression.callee); + this.inGuardPosition(() => { + if (expression.callee) { + if (expression.callee.type === "MemberExpression") { + // because of call context we need to walk the call context as expression + this.walkExpression(expression.callee.object); + if (expression.callee.computed === true) + this.walkExpression(expression.callee.property); + } else { + this.walkExpression(expression.callee); + } } - } - if (expression.arguments) this.walkExpressions(expression.arguments); + if (expression.arguments) this.walkExpressions(expression.arguments); + }, false); } } @@ -3793,6 +3830,17 @@ class JavascriptParser extends Parser { ); } + /** + * @param {Function} fn function + * @param {boolean} state guard position state + */ + inGuardPosition(fn, state) { + const old = this.scope.inGuardPosition; + this.scope.inGuardPosition = state; + fn(); + this.scope.inGuardPosition = old; + } + /** * @deprecated * @param {any} params scope params @@ -3804,11 +3852,13 @@ class JavascriptParser extends Parser { this.scope = { topLevelScope: oldScope.topLevelScope, inTry: false, + inGuardPosition: false, inShorthand: false, inTaggedTemplateTag: false, isStrict: oldScope.isStrict, isAsmJs: oldScope.isAsmJs, - definitions: oldScope.definitions.createChild() + definitions: oldScope.definitions.createChild(), + guards: oldScope.guards.createChild() }; this.undefineVariable("this"); @@ -3833,11 +3883,13 @@ class JavascriptParser extends Parser { this.scope = { topLevelScope: oldScope.topLevelScope, inTry: false, + inGuardPosition: false, inShorthand: false, inTaggedTemplateTag: false, isStrict: oldScope.isStrict, isAsmJs: oldScope.isAsmJs, - definitions: oldScope.definitions.createChild() + definitions: oldScope.definitions.createChild(), + guards: oldScope.guards.createChild() }; if (hasThis) { @@ -3864,11 +3916,13 @@ class JavascriptParser extends Parser { this.scope = { topLevelScope: oldScope.topLevelScope, inTry: false, + inGuardPosition: false, inShorthand: false, inTaggedTemplateTag: false, isStrict: oldScope.isStrict, isAsmJs: oldScope.isAsmJs, - definitions: oldScope.definitions.createChild() + definitions: oldScope.definitions.createChild(), + guards: oldScope.guards.createChild() }; if (hasThis) { @@ -3893,11 +3947,13 @@ class JavascriptParser extends Parser { this.scope = { topLevelScope: oldScope.topLevelScope, inTry: oldScope.inTry, + inGuardPosition: false, inShorthand: false, inTaggedTemplateTag: false, isStrict: oldScope.isStrict, isAsmJs: oldScope.isAsmJs, - definitions: oldScope.definitions.createChild() + definitions: oldScope.definitions.createChild(), + guards: oldScope.guards.createChild() }; fn(); @@ -4197,11 +4253,13 @@ class JavascriptParser extends Parser { this.scope = { topLevelScope: true, inTry: false, + inGuardPosition: false, inShorthand: false, inTaggedTemplateTag: false, isStrict: false, isAsmJs: false, - definitions: new StackedMap() + definitions: new StackedMap(), + guards: new AppendOnlyStackedSet() }; /** @type {ParserState} */ this.state = state; diff --git a/lib/util/AppendOnlyStackedSet.js b/lib/util/AppendOnlyStackedSet.js new file mode 100644 index 00000000000..b4bfa8c070c --- /dev/null +++ b/lib/util/AppendOnlyStackedSet.js @@ -0,0 +1,59 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Ivan Kopeykin @vankop +*/ + +"use strict"; + +/** + * @template T + */ +class AppendOnlyStackedSet { + /** + * @param {Array>} sets an optional array of sets + */ + constructor(sets = []) { + /** @type {Array>} */ + this._sets = sets; + /** @type {Set|undefined} */ + this._current = undefined; + } + + /** + * @param {T} el element + */ + add(el) { + if (!this._current) { + this._current = new Set(); + this._sets.push(this._current); + } + this._current.add(el); + } + + /** + * @param {T} el element + * @returns {boolean} result + */ + has(el) { + for (const set of this._sets) { + if (set.has(el)) return true; + } + return false; + } + + clear() { + this._sets = []; + if (this._current) this._current.clear(); + } + + /** + * @returns {AppendOnlyStackedSet} child + */ + createChild() { + return new AppendOnlyStackedSet( + this._sets.length ? this._sets.slice() : [] + ); + } +} + +module.exports = AppendOnlyStackedSet; diff --git a/lib/util/internalSerializables.js b/lib/util/internalSerializables.js index 1cd63dbd5d5..381dbc2a3e4 100644 --- a/lib/util/internalSerializables.js +++ b/lib/util/internalSerializables.js @@ -109,6 +109,8 @@ module.exports = { require("../dependencies/HarmonyImportSpecifierDependency"), "dependencies/HarmonyEvaluatedImportSpecifierDependency": () => require("../dependencies/HarmonyEvaluatedImportSpecifierDependency"), + "dependencies/HarmonyExportPresenceImportSpecifierDependency": () => + require("../dependencies/HarmonyExportPresenceImportSpecifierDependency"), "dependencies/ImportContextDependency": () => require("../dependencies/ImportContextDependency"), "dependencies/ImportDependency": () => diff --git a/test/configCases/compiletime/exports-presence/index.js b/test/configCases/compiletime/exports-presence/index.js index 3b8d2e8b66d..74aa264cb15 100644 --- a/test/configCases/compiletime/exports-presence/index.js +++ b/test/configCases/compiletime/exports-presence/index.js @@ -1,7 +1,102 @@ -import { NotHere as aaa } from "./aaa/index.js"; -import { NotHere as bbb } from "./bbb/index.js"; -import { NotHere as ccc } from "./ccc/index.js"; -import { NotHere as ddd } from "./ddd/index.js"; +import { NotHere as aaa, /* not here */ a } from "./aaa/index.js"; +import { NotHere as bbb, /* not here */ b } from "./bbb/index.js"; +import { NotHere as ccc, /* not here */ c } from "./ccc/index.js"; +import { NotHere as ddd, /* not here */ d } from "./ddd/index.js"; +import * as m from "./module"; + +const val1 = Math.random(); + +function throw_() { + throw new Error(); +} +function justFunction() {} + +describe("should not add additional warnings/errors", () => { + it("simple cases", () => { + if (b) { + if (d) d(); + b(); + if (c) { + b(); + } + } + (false && d); + (d ? d() : throw_()); + // should add 2 warnings + if (a && val1 || true) { + a(); + } + if (a && a.b && a.b.c) { + a(); + } + // only one warning + if (a.b.c) { + a.b.c(); + } + }); + + it("different expressions", () => { + if (a && a.b.c) {} + // should add warning (function scope) + if ((() => a())()) {} + // should add warning (unary expression) + if (!a && b) {} + // should add warning (binary expression) + if (a & true) {} + + function *foo() { + // should add warning (yield expression) + if (yield a && true) {} + } + async function foo1() { + // should add warning (yield expression) + if (await a && true) {} + } + let var1; + if (var1 = b) {} + if ((var1 = b) && c && c.a) {} + // should add warning + if (justFunction`${a}`) {} + if (`${a}`) {} + }); + + it("ternary operator", () => { + (c && c.a ? c.a() : 0); + const b1 = c ? c() : 0; + (c && c.a && d && d.a ? c.a(d.a) : 0); + ("a" in c ? c.a() : 0); + ("a" in c && "a" in b ? b.a(c.a) : 0); + (c ? d() : (() => {})()); + }); + + it("in operator", () => { + if ("a" in m) { justFunction(m.a); } + if ("b" in m && "c" in m.b) { justFunction(m.b.c); } + if ("c" in m) { justFunction(m.c); } + // should add one warning + if ("a"in d.c) { justFunction(d.c.a()); } + }); + + it("identifier != falsy", () => { + if (c != false) {} + // should add warning since value could be undefined !== false + if (c !== false) {} + if (c != null && c.a != undefined && c.a.b != false && 0 != c.a.b.c && "" != c.a.b.c.d) { + c(); + c.a(); + c().a; + { + c.a.b(); + const a = () => c.a.b.c.d(); + const b = function () { + c.a.b.c.d(); + } + } + } + // should add 2 warnings + if (c != undefined ?? undefined != c) {} + }); +}); it("should do nothing", () => { expect(aaa).toBe(undefined); diff --git a/test/configCases/compiletime/exports-presence/module.js b/test/configCases/compiletime/exports-presence/module.js new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/configCases/compiletime/exports-presence/warnings.js b/test/configCases/compiletime/exports-presence/warnings.js index ca07ad2aacf..cd4a0b14095 100644 --- a/test/configCases/compiletime/exports-presence/warnings.js +++ b/test/configCases/compiletime/exports-presence/warnings.js @@ -10,5 +10,69 @@ module.exports = [ { moduleName: /ddd/, message: /NoNo.+not found/ + }, + { + moduleName: /index/, + message: /a.+not found/ + }, + { + moduleName: /index/, + message: /a.+not found/ + }, + { + moduleName: /index/, + message: /a.+not found/ + }, + { + moduleName: /index/, + message: /a.+not found/ + }, + { + moduleName: /index/, + message: /a.+not found/ + }, + { + moduleName: /index/, + message: /a.+not found/ + }, + { + moduleName: /index/, + message: /a.+not found/ + }, + { + moduleName: /index/, + message: /a.+not found/ + }, + { + moduleName: /index/, + message: /a.+not found/ + }, + { + moduleName: /index/, + message: /a.+not found/ + }, + { + moduleName: /index/, + message: /a.+not found/ + }, + { + moduleName: /index/, + message: /d.+not found/ + }, + { + moduleName: /index/, + message: /d.+not found/ + }, + { + moduleName: /index/, + message: /c.+not found/ + }, + { + moduleName: /index/, + message: /c.+not found/ + }, + { + moduleName: /index/, + message: /c.+not found/ } ]; diff --git a/types.d.ts b/types.d.ts index 14e3bb79da6..83b74e7121f 100644 --- a/types.d.ts +++ b/types.d.ts @@ -229,6 +229,12 @@ type AliasOptionNewRequest = string | false | string[]; declare interface AliasOptions { [index: string]: AliasOptionNewRequest; } +declare abstract class AppendOnlyStackedSet { + add(el: T): void; + has(el: T): boolean; + clear(): void; + createChild(): AppendOnlyStackedSet; +} declare interface Argument { description: string; simpleType: "string" | "number" | "boolean"; @@ -6320,6 +6326,7 @@ declare class JavascriptParser extends Parser { defined: undefined | (() => any), ...args: AsArray ): undefined | R; + inGuardPosition(fn: Function, state: boolean): void; inScope(params: any, fn: () => void): void; inClassScope(hasThis: boolean, params: any, fn: () => void): void; inFunctionScope(hasThis: boolean, params: any, fn: () => void): void; @@ -12922,12 +12929,14 @@ declare interface RuntimeValueOptions { */ declare interface ScopeInfo { definitions: StackedMap; + guards: AppendOnlyStackedSet; topLevelScope: boolean | "arrow"; inShorthand: string | boolean; inTaggedTemplateTag: boolean; inTry: boolean; isStrict: boolean; isAsmJs: boolean; + inGuardPosition: boolean; } declare interface Selector { (input: A): undefined | null | B;