diff --git a/lib/dependencies/HarmonyImportDependencyParserPlugin.js b/lib/dependencies/HarmonyImportDependencyParserPlugin.js index c0c1f6896ce..851d97a49cf 100644 --- a/lib/dependencies/HarmonyImportDependencyParserPlugin.js +++ b/lib/dependencies/HarmonyImportDependencyParserPlugin.js @@ -83,6 +83,29 @@ module.exports = class HarmonyImportDependencyParserPlugin { */ apply(parser) { const { exportPresenceMode } = this; + /** + * @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) + return exportPresenceMode; + if (parser.scope.guards.has(guard)) return ExportPresenceModes.NONE; + if ( + 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 => { @@ -131,10 +154,84 @@ module.exports = class HarmonyImportDependencyParserPlugin { return true; } ); + parser.hooks.binaryExpression.tap( + "HarmonyImportDependencyParserPlugin", + expression => { + if (!parser.scope.inGuardPosition) return; + if (expression.operator === "in") { + const leftPartEvaluated = parser.evaluateExpression(expression.left); + if (!leftPartEvaluated || leftPartEvaluated.couldHaveSideEffects()) + return; + const leftPart = leftPartEvaluated.asString(); + if (!leftPart) return; + + const rightPart = parser.evaluateExpression(expression.right); + if (!rightPart || !rightPart.isIdentifier()) return; + + const rootInfo = rightPart.rootInfo; + if ( + !rootInfo || + !rootInfo.tagInfo || + rootInfo.tagInfo.tag !== harmonySpecifierTag + ) + return; + parser.walkExpression(rightPart.expression); + return true; + } else if (expression.operator === "!=") { + let identifierEvaluated; + const leftPartEvaluated = parser.evaluateExpression(expression.left); + if (!leftPartEvaluated) { + return; + } else if (leftPartEvaluated.isIdentifier()) { + const rootInfo = leftPartEvaluated.rootInfo; + if ( + !rootInfo || + !rootInfo.tagInfo || + rootInfo.tagInfo.tag !== harmonySpecifierTag + ) + return; + identifierEvaluated = leftPartEvaluated; + } else if (leftPartEvaluated.isFalsy() !== true) { + return; + } + + const rightPartEvaluated = parser.evaluateExpression( + expression.right + ); + if (!rightPartEvaluated) { + return; + } else if ( + !identifierEvaluated && + rightPartEvaluated.isIdentifier() + ) { + const rootInfo = rightPartEvaluated.rootInfo; + if ( + !rootInfo || + !rootInfo.tagInfo || + rootInfo.tagInfo.tag !== harmonySpecifierTag + ) + 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); + const guard = createGuard(settings.ids); + const exportPresenceMode = detectExportPresenceMode( + guard, + settings.ids.length + ); + if (parser.scope.inGuardPosition) parser.scope.guards.add(guard); const dep = new HarmonyImportSpecifierDependency( settings.source, settings.sourceOrder, @@ -157,6 +254,9 @@ module.exports = class HarmonyImportDependencyParserPlugin { .tap("HarmonyImportDependencyParserPlugin", (expr, members) => { const settings = /** @type {HarmonySettings} */ (parser.currentTagData); const ids = settings.ids.concat(members); + 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, @@ -184,7 +284,7 @@ module.exports = class HarmonyImportDependencyParserPlugin { ids, settings.name, callee.range, - exportPresenceMode, + detectExportPresenceMode(createGuard(ids), ids.length), settings.assertions ); dep.directImport = members.length === 0; diff --git a/lib/javascript/BasicEvaluatedExpression.js b/lib/javascript/BasicEvaluatedExpression.js index 0e5a21183c9..d0705d39890 100644 --- a/lib/javascript/BasicEvaluatedExpression.js +++ b/lib/javascript/BasicEvaluatedExpression.js @@ -299,18 +299,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; } @@ -318,6 +322,8 @@ class BasicEvaluatedExpression { this.type = TypeNumber; this.number = number; this.sideEffects = false; + if (number === 0) this.falsy = true; + else this.truthy = true; return this; } @@ -325,6 +331,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; } @@ -332,6 +340,8 @@ class BasicEvaluatedExpression { this.type = TypeBoolean; this.bool = bool; this.sideEffects = false; + if (bool === false) this.falsy = true; + else this.truthy = true; return this; } @@ -339,6 +349,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 93050a6352b..820ddd4a7b5 100644 --- a/lib/javascript/JavascriptParser.js +++ b/lib/javascript/JavascriptParser.js @@ -11,6 +11,7 @@ const { SyncBailHook, HookMap } = require("tapable"); const vm = require("vm"); const Parser = require("../Parser"); const StackedMap = require("../util/StackedMap"); +const WriteOnlyStackedSet = require("../util/WriteOnlyStackedSet"); const binarySearchBounds = require("../util/binarySearchBounds"); const memoize = require("../util/memoize"); const BasicEvaluatedExpression = require("./BasicEvaluatedExpression"); @@ -95,11 +96,13 @@ class VariableInfo { /** * @typedef {Object} ScopeInfo * @property {StackedMap} definitions + * @property {WriteOnlyStackedSet} guards * @property {boolean | "arrow"} topLevelScope * @property {boolean} inShorthand * @property {boolean} isStrict * @property {boolean} isAsmJs * @property {boolean} inTry + * @property {boolean} inGuardPosition */ const joinRanges = (startRange, endRange) => { @@ -292,6 +295,8 @@ class JavascriptParser extends Parser { optionalChaining: new SyncBailHook(["optionalChaining"]), /** @type {HookMap>} */ new: new HookMap(() => new SyncBailHook(["expression"])), + /** @type {SyncBailHook<[BinaryExpressionNode], boolean | void>} */ + binaryExpression: new SyncBailHook(["binaryExpression"]), /** @type {HookMap>} */ expression: new HookMap(() => new SyncBailHook(["expression"])), /** @type {HookMap>} */ @@ -1642,8 +1647,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); } @@ -2312,12 +2320,15 @@ 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); } walkArrayExpression(expression) { if (expression.elements) { - this.walkExpressions(expression.elements); + this.inGuardPosition( + () => this.walkExpressions(expression.elements), + false + ); } } @@ -2334,7 +2345,7 @@ class JavascriptParser extends Parser { propIndex++ ) { const prop = expression.properties[propIndex]; - this.walkProperty(prop); + this.inGuardPosition(() => this.walkProperty(prop), false); } } @@ -2418,17 +2429,20 @@ class JavascriptParser extends Parser { const old = 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 + ); } } walkUpdateExpression(expression) { - this.walkExpression(expression.argument); + this.inGuardPosition(() => this.walkExpression(expression.argument), false); } walkUnaryExpression(expression) { @@ -2448,7 +2462,7 @@ class JavascriptParser extends Parser { if (result === true) return; } } - this.walkExpression(expression.argument); + this.inGuardPosition(() => this.walkExpression(expression.argument), false); } walkLeftRightExpression(expression) { @@ -2457,13 +2471,28 @@ class JavascriptParser extends Parser { } walkBinaryExpression(expression) { - this.walkLeftRightExpression(expression); + if (this.hooks.binaryExpression.call(expression) === undefined) { + this.inGuardPosition( + () => this.walkLeftRightExpression(expression), + false + ); + } } + /** + * @param {LogicalExpressionNode} expression expression + */ 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); @@ -2542,8 +2571,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); } @@ -2563,31 +2595,41 @@ 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); } walkYieldExpression(expression) { if (expression.argument) { - this.walkExpression(expression.argument); + this.inGuardPosition( + () => this.walkExpression(expression.argument), + false + ); } } walkTemplateLiteral(expression) { if (expression.expressions) { - this.walkExpressions(expression.expressions); + this.inGuardPosition( + () => this.walkExpressions(expression.expressions), + false + ); } } walkTaggedTemplateExpression(expression) { - if (expression.tag) { - this.walkExpression(expression.tag); - } - if (expression.quasi && expression.quasi.expressions) { - this.walkExpressions(expression.quasi.expressions); - } + this.inGuardPosition(() => { + if (expression.tag) { + this.walkExpression(expression.tag); + } + if (expression.quasi && expression.quasi.expressions) { + this.walkExpressions(expression.quasi.expressions); + } + }, false); } walkClassExpression(expression) { @@ -2675,7 +2717,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); } walkCallExpression(expression) { @@ -2738,17 +2780,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); } } @@ -3008,6 +3052,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 @@ -3019,10 +3074,12 @@ class JavascriptParser extends Parser { this.scope = { topLevelScope: oldScope.topLevelScope, inTry: false, + inGuardPosition: false, inShorthand: false, isStrict: oldScope.isStrict, isAsmJs: oldScope.isAsmJs, - definitions: oldScope.definitions.createChild() + definitions: oldScope.definitions.createChild(), + guards: oldScope.guards.createChild() }; this.undefineVariable("this"); @@ -3041,10 +3098,12 @@ class JavascriptParser extends Parser { this.scope = { topLevelScope: oldScope.topLevelScope, inTry: false, + inGuardPosition: false, inShorthand: false, isStrict: oldScope.isStrict, isAsmJs: oldScope.isAsmJs, - definitions: oldScope.definitions.createChild() + definitions: oldScope.definitions.createChild(), + guards: oldScope.guards.createChild() }; if (hasThis) { @@ -3065,10 +3124,12 @@ class JavascriptParser extends Parser { this.scope = { topLevelScope: oldScope.topLevelScope, inTry: oldScope.inTry, + inGuardPosition: false, inShorthand: false, isStrict: oldScope.isStrict, isAsmJs: oldScope.isAsmJs, - definitions: oldScope.definitions.createChild() + definitions: oldScope.definitions.createChild(), + guards: oldScope.guards.createChild() }; fn(); @@ -3318,10 +3379,12 @@ class JavascriptParser extends Parser { this.scope = { topLevelScope: true, inTry: false, + inGuardPosition: false, inShorthand: false, isStrict: false, isAsmJs: false, - definitions: new StackedMap() + definitions: new StackedMap(), + guards: new WriteOnlyStackedSet() }; /** @type {ParserState} */ this.state = state; diff --git a/lib/util/WriteOnlyStackedSet.js b/lib/util/WriteOnlyStackedSet.js new file mode 100644 index 00000000000..807c0193777 --- /dev/null +++ b/lib/util/WriteOnlyStackedSet.js @@ -0,0 +1,49 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Ivan Kopeykin @vankop +*/ + +"use strict"; + +/** + * @template T + */ +class WriteOnlyStackedSet { + constructor(sets = []) { + this._sets = sets; + 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(); + } + + createChild() { + return new WriteOnlyStackedSet(this._sets.length ? this._sets.slice() : []); + } +} + +module.exports = WriteOnlyStackedSet; diff --git a/test/configCases/compiletime/exports-presence/index.js b/test/configCases/compiletime/exports-presence/index.js index 3b8d2e8b66d..96b4e241cbd 100644 --- a/test/configCases/compiletime/exports-presence/index.js +++ b/test/configCases/compiletime/exports-presence/index.js @@ -1,7 +1,91 @@ -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("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); } + }); + + 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..8330b54da03 100644 --- a/test/configCases/compiletime/exports-presence/warnings.js +++ b/test/configCases/compiletime/exports-presence/warnings.js @@ -10,5 +10,61 @@ 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: /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 7ab6cc78bd1..1c15e684a98 100644 --- a/types.d.ts +++ b/types.d.ts @@ -5118,6 +5118,7 @@ declare class JavascriptParser extends Parser { >; optionalChaining: SyncBailHook<[ChainExpression], boolean | void>; new: HookMap>; + binaryExpression: SyncBailHook<[BinaryExpression], boolean | void>; expression: HookMap>; expressionMemberChain: HookMap< SyncBailHook<[Expression, string[]], boolean | void> @@ -5266,7 +5267,7 @@ declare class JavascriptParser extends Parser { walkUnaryExpression(expression?: any): void; walkLeftRightExpression(expression?: any): void; walkBinaryExpression(expression?: any): void; - walkLogicalExpression(expression?: any): void; + walkLogicalExpression(expression: LogicalExpression): void; walkAssignmentExpression(expression?: any): void; walkConditionalExpression(expression?: any): void; walkNewExpression(expression?: any): void; @@ -5324,6 +5325,7 @@ declare class JavascriptParser extends Parser { defined: () => any, ...args: AsArray ): R; + inGuardPosition(fn: Function, state: boolean): void; inScope(params: any, fn: () => void): void; inFunctionScope(hasThis?: any, params?: any, fn?: any): void; inBlockScope(fn?: any): void; @@ -10725,11 +10727,13 @@ declare interface RuntimeValueOptions { } declare interface ScopeInfo { definitions: StackedMap; + guards: WriteOnlyStackedSet; topLevelScope: boolean | "arrow"; inShorthand: boolean; isStrict: boolean; isAsmJs: boolean; inTry: boolean; + inGuardPosition: boolean; } declare interface Selector { (input: A): B; @@ -12474,6 +12478,12 @@ declare interface WithOptions { declare interface WriteOnlySet { add: (T?: any) => void; } +declare abstract class WriteOnlyStackedSet { + add(el: T): void; + has(el: T): boolean; + clear(): void; + createChild(): WriteOnlyStackedSet; +} type __TypeWebpackOptions = (data: object) => | string | {