fix: distinguish free variable and tagged variable (#19795)

This commit is contained in:
Gengkun 2025-08-14 02:36:56 +08:00 committed by GitHub
parent 61a15a672e
commit cbfba9a150
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 204 additions and 30 deletions

View File

@ -54,8 +54,7 @@ class JavascriptMetaInfoPlugin {
topLevelDeclarations = buildInfo.topLevelDeclarations = new Set(); topLevelDeclarations = buildInfo.topLevelDeclarations = new Set();
} }
for (const name of parser.scope.definitions.asSet()) { for (const name of parser.scope.definitions.asSet()) {
const freeInfo = parser.getFreeInfoFromVariable(name); if (parser.isVariableDefined(name)) {
if (freeInfo === undefined) {
topLevelDeclarations.add(name); topLevelDeclarations.add(name);
} }
} }

View File

@ -258,17 +258,42 @@ const getImportAttributes = (node) => {
return result; return result;
}; };
/** @typedef {typeof VariableInfoFlags.Evaluated | typeof VariableInfoFlags.Free | typeof VariableInfoFlags.Normal | typeof VariableInfoFlags.Tagged} VariableInfoFlagsType */
const VariableInfoFlags = Object.freeze({
Evaluated: 0b000,
Free: 0b001,
Normal: 0b010,
Tagged: 0b100
});
class VariableInfo { class VariableInfo {
/** /**
* @param {ScopeInfo} declaredScope scope in which the variable is declared * @param {ScopeInfo} declaredScope scope in which the variable is declared
* @param {string | true | undefined} freeName which free name the variable aliases, or true when none * @param {string | undefined} name which name the variable use, defined name or free name or tagged name
* @param {VariableInfoFlagsType} flags how the variable is created
* @param {TagInfo | undefined} tagInfo info about tags * @param {TagInfo | undefined} tagInfo info about tags
*/ */
constructor(declaredScope, freeName, tagInfo) { constructor(declaredScope, name, flags, tagInfo) {
this.declaredScope = declaredScope; this.declaredScope = declaredScope;
this.freeName = freeName; this.name = name;
this.flags = flags;
this.tagInfo = tagInfo; this.tagInfo = tagInfo;
} }
/**
* @returns {boolean} the variable is free or not
*/
isFree() {
return (this.flags & VariableInfoFlags.Free) > 0;
}
/**
* @returns {boolean} the variable is tagged by tagVariable or not
*/
isTagged() {
return (this.flags & VariableInfoFlags.Tagged) > 0;
}
} }
/** @typedef {string | ScopeInfo | VariableInfo} ExportedVariableInfo */ /** @typedef {string | ScopeInfo | VariableInfo} ExportedVariableInfo */
@ -1426,7 +1451,7 @@ class JavascriptParser extends Parser {
const info = this.getVariableInfo(/** @type {Identifier} */ (expr).name); const info = this.getVariableInfo(/** @type {Identifier} */ (expr).name);
if ( if (
typeof info === "string" || typeof info === "string" ||
(info instanceof VariableInfo && typeof info.freeName === "string") (info instanceof VariableInfo && (info.isFree() || info.isTagged()))
) { ) {
return { return {
name: info, name: info,
@ -1441,7 +1466,7 @@ class JavascriptParser extends Parser {
const info = this.getVariableInfo("this"); const info = this.getVariableInfo("this");
if ( if (
typeof info === "string" || typeof info === "string" ||
(info instanceof VariableInfo && typeof info.freeName === "string") (info instanceof VariableInfo && (info.isFree() || info.isTagged()))
) { ) {
return { return {
name: info, name: info,
@ -4053,13 +4078,13 @@ class JavascriptParser extends Parser {
} }
tagInfo = tagInfo.next; tagInfo = tagInfo.next;
} }
if (info.freeName === true) { if (!info.isFree() && !info.isTagged()) {
if (defined !== undefined) { if (defined !== undefined) {
return defined(); return defined();
} }
return; return;
} }
name = info.freeName; name = info.name;
} }
const hook = hookMap.get(name); const hook = hookMap.get(name);
if (hook !== undefined) { if (hook !== undefined) {
@ -4818,25 +4843,31 @@ class JavascriptParser extends Parser {
* @param {string} name name * @param {string} name name
* @param {Tag} tag tag info * @param {Tag} tag tag info
* @param {TagData=} data data * @param {TagData=} data data
* @param {VariableInfoFlagsType=} flags flags
*/ */
tagVariable(name, tag, data) { tagVariable(name, tag, data, flags = VariableInfoFlags.Tagged) {
const oldInfo = this.scope.definitions.get(name); const oldInfo = this.scope.definitions.get(name);
/** @type {VariableInfo} */ /** @type {VariableInfo} */
let newInfo; let newInfo;
if (oldInfo === undefined) { if (oldInfo === undefined) {
newInfo = new VariableInfo(this.scope, name, { newInfo = new VariableInfo(this.scope, name, flags, {
tag, tag,
data, data,
next: undefined next: undefined
}); });
} else if (oldInfo instanceof VariableInfo) { } else if (oldInfo instanceof VariableInfo) {
newInfo = new VariableInfo(oldInfo.declaredScope, oldInfo.freeName, { newInfo = new VariableInfo(
tag, oldInfo.declaredScope,
data, oldInfo.name,
next: oldInfo.tagInfo /** @type {VariableInfoFlagsType} */ (oldInfo.flags | flags),
}); {
tag,
data,
next: oldInfo.tagInfo
}
);
} else { } else {
newInfo = new VariableInfo(oldInfo, true, { newInfo = new VariableInfo(oldInfo, name, flags, {
tag, tag,
data, data,
next: undefined next: undefined
@ -4875,7 +4906,7 @@ class JavascriptParser extends Parser {
const info = this.scope.definitions.get(name); const info = this.scope.definitions.get(name);
if (info === undefined) return false; if (info === undefined) return false;
if (info instanceof VariableInfo) { if (info instanceof VariableInfo) {
return info.freeName === true; return !info.isFree();
} }
return true; return true;
} }
@ -4904,7 +4935,12 @@ class JavascriptParser extends Parser {
} else { } else {
this.scope.definitions.set( this.scope.definitions.set(
name, name,
new VariableInfo(this.scope, variableInfo, undefined) new VariableInfo(
this.scope,
variableInfo,
VariableInfoFlags.Free,
undefined
)
); );
} }
} else { } else {
@ -4917,7 +4953,12 @@ class JavascriptParser extends Parser {
* @returns {VariableInfo} variable info * @returns {VariableInfo} variable info
*/ */
evaluatedVariable(tagInfo) { evaluatedVariable(tagInfo) {
return new VariableInfo(this.scope, undefined, tagInfo); return new VariableInfo(
this.scope,
undefined,
VariableInfoFlags.Evaluated,
tagInfo
);
} }
/** /**
@ -5002,9 +5043,27 @@ class JavascriptParser extends Parser {
getFreeInfoFromVariable(varName) { getFreeInfoFromVariable(varName) {
const info = this.getVariableInfo(varName); const info = this.getVariableInfo(varName);
let name; let name;
if (info instanceof VariableInfo) { if (info instanceof VariableInfo && info.name) {
name = info.freeName; if (!info.isFree()) return;
if (typeof name !== "string") return; name = info.name;
} else if (typeof info !== "string") {
return;
} else {
name = info;
}
return { info, name };
}
/**
* @param {string} varName variable name
* @returns {{name: string, info: VariableInfo | string} | undefined} name of the free variable and variable info for that
*/
getNameInfoFromVariable(varName) {
const info = this.getVariableInfo(varName);
let name;
if (info instanceof VariableInfo && info.name) {
if (!info.isFree() && !info.isTagged()) return;
name = info.name;
} else if (typeof info !== "string") { } else if (typeof info !== "string") {
return; return;
} else { } else {
@ -5035,7 +5094,7 @@ class JavascriptParser extends Parser {
} }
const rootName = getRootName(callee); const rootName = getRootName(callee);
if (!rootName) return; if (!rootName) return;
const result = this.getFreeInfoFromVariable(rootName); const result = this.getNameInfoFromVariable(rootName);
if (!result) return; if (!result) return;
const { info: rootInfo, name: resolvedRoot } = result; const { info: rootInfo, name: resolvedRoot } = result;
const calleeName = objectAndMembersToName(resolvedRoot, rootMembers); const calleeName = objectAndMembersToName(resolvedRoot, rootMembers);
@ -5058,7 +5117,7 @@ class JavascriptParser extends Parser {
const rootName = getRootName(object); const rootName = getRootName(object);
if (!rootName) return; if (!rootName) return;
const result = this.getFreeInfoFromVariable(rootName); const result = this.getNameInfoFromVariable(rootName);
if (!result) return; if (!result) return;
const { info: rootInfo, name: resolvedRoot } = result; const { info: rootInfo, name: resolvedRoot } = result;
return { return {
@ -5151,4 +5210,5 @@ module.exports.ALLOWED_MEMBER_TYPES_CALL_EXPRESSION =
module.exports.ALLOWED_MEMBER_TYPES_EXPRESSION = module.exports.ALLOWED_MEMBER_TYPES_EXPRESSION =
ALLOWED_MEMBER_TYPES_EXPRESSION; ALLOWED_MEMBER_TYPES_EXPRESSION;
module.exports.VariableInfo = VariableInfo; module.exports.VariableInfo = VariableInfo;
module.exports.VariableInfoFlags = VariableInfoFlags;
module.exports.getImportAttributes = getImportAttributes; module.exports.getImportAttributes = getImportAttributes;

View File

@ -6,6 +6,7 @@
"use strict"; "use strict";
const { UsageState } = require("../ExportsInfo"); const { UsageState } = require("../ExportsInfo");
const JavascriptParser = require("../javascript/JavascriptParser");
/** @typedef {import("estree").Node} AnyNode */ /** @typedef {import("estree").Node} AnyNode */
/** @typedef {import("../Dependency")} Dependency */ /** @typedef {import("../Dependency")} Dependency */
@ -15,7 +16,6 @@ const { UsageState } = require("../ExportsInfo");
/** @typedef {import("../ModuleGraphConnection")} ModuleGraphConnection */ /** @typedef {import("../ModuleGraphConnection")} ModuleGraphConnection */
/** @typedef {import("../ModuleGraphConnection").ConnectionState} ConnectionState */ /** @typedef {import("../ModuleGraphConnection").ConnectionState} ConnectionState */
/** @typedef {import("../Parser").ParserState} ParserState */ /** @typedef {import("../Parser").ParserState} ParserState */
/** @typedef {import("../javascript/JavascriptParser")} JavascriptParser */
/** @typedef {import("../util/runtime").RuntimeSpec} RuntimeSpec */ /** @typedef {import("../util/runtime").RuntimeSpec} RuntimeSpec */
/** @typedef {Map<TopLevelSymbol | null, Set<string | TopLevelSymbol> | true | undefined>} InnerGraph */ /** @typedef {Map<TopLevelSymbol | null, Set<string | TopLevelSymbol> | true | undefined>} InnerGraph */
@ -348,7 +348,12 @@ module.exports.tagTopLevelSymbol = (parser, name) => {
} }
const fn = new TopLevelSymbol(name); const fn = new TopLevelSymbol(name);
parser.tagVariable(name, topLevelSymbolTag, fn); parser.tagVariable(
name,
topLevelSymbolTag,
fn,
JavascriptParser.VariableInfoFlags.Normal
);
return fn; return fn;
}; };

View File

@ -0,0 +1 @@
export const a = 1;

View File

@ -0,0 +1 @@
exports.b = 2;

View File

@ -0,0 +1 @@
export const c = 3;

View File

@ -0,0 +1 @@
export const d = 4;

View File

@ -0,0 +1,15 @@
import { a } from "./a";
import { createRequire } from "module";
const myRequire = createRequire(import.meta.url);
const { b } = myRequire("./b");
const c = new URL("./c.js", import.meta.url);
const audioContext = new AudioContext();
const d = audioContext.audioWorklet.addModule(new URL("./d.js", import.meta.url));
it("should have correct top level declarations", async () => {
await d;
expect(a).toBe(1);
expect(b).toBe(2);
expect(c.pathname.endsWith(".js")).toBe(true);
})

View File

@ -0,0 +1,23 @@
"use strict";
let outputDirectory;
module.exports = {
moduleScope(scope) {
const FakeWorker = require("../../../helpers/createFakeWorker")({
outputDirectory
});
scope.AudioContext = class AudioContext {
constructor() {
this.audioWorklet = {
addModule: (url) => Promise.resolve(FakeWorker.bind(null, url))
};
}
};
},
findBundle(i, options) {
outputDirectory = options.output.path;
return ["main.js"];
}
};

View File

@ -0,0 +1,5 @@
"use strict";
const supportsWorker = require("../../../helpers/supportsWorker");
module.exports = () => supportsWorker();

View File

@ -0,0 +1,44 @@
"use strict";
/** @type {import("../../../../").Configuration} */
module.exports = {
target: "web",
output: {
filename: "[name].js"
},
module: {
parser: {
javascript: {
createRequire: true,
worker: ["*audioContext.audioWorklet.addModule()"]
}
}
},
plugins: [
function testPlugin(compiler) {
compiler.hooks.finishMake.tap("test", (compilation) => {
for (const module of compilation.modules) {
const name = module.nameForCondition();
const topLevelDeclarations =
module.buildInfo && module.buildInfo.topLevelDeclarations;
if (
name &&
name.includes("top-level-declarations/index.js") &&
topLevelDeclarations
) {
const expectedTopLevelDeclarations = new Set([
"a",
"createRequire",
"myRequire",
"b",
"c",
"audioContext",
"d"
]);
expect(topLevelDeclarations).toEqual(expectedTopLevelDeclarations);
}
}
});
}
]
};

25
types.d.ts vendored
View File

@ -7716,7 +7716,12 @@ declare class JavascriptParser extends ParserClass {
unsetAsiPosition(pos: number): void; unsetAsiPosition(pos: number): void;
isStatementLevelExpression(expr: Expression): boolean; isStatementLevelExpression(expr: Expression): boolean;
getTagData(name: string, tag: symbol): undefined | TagData; getTagData(name: string, tag: symbol): undefined | TagData;
tagVariable(name: string, tag: symbol, data?: TagData): void; tagVariable(
name: string,
tag: symbol,
data?: TagData,
flags?: 0 | 1 | 2 | 4
): void;
defineVariable(name: string): void; defineVariable(name: string): void;
undefineVariable(name: string): void; undefineVariable(name: string): void;
isVariableDefined(name: string): boolean; isVariableDefined(name: string): boolean;
@ -7794,6 +7799,9 @@ declare class JavascriptParser extends ParserClass {
getFreeInfoFromVariable( getFreeInfoFromVariable(
varName: string varName: string
): undefined | { name: string; info: string | VariableInfo }; ): undefined | { name: string; info: string | VariableInfo };
getNameInfoFromVariable(
varName: string
): undefined | { name: string; info: string | VariableInfo };
getMemberExpressionInfo( getMemberExpressionInfo(
expression: expression:
| ImportExpressionImport | ImportExpressionImport
@ -7842,6 +7850,12 @@ declare class JavascriptParser extends ParserClass {
static ALLOWED_MEMBER_TYPES_CALL_EXPRESSION: 1; static ALLOWED_MEMBER_TYPES_CALL_EXPRESSION: 1;
static ALLOWED_MEMBER_TYPES_EXPRESSION: 2; static ALLOWED_MEMBER_TYPES_EXPRESSION: 2;
static VariableInfo: typeof VariableInfo; static VariableInfo: typeof VariableInfo;
static VariableInfoFlags: Readonly<{
Evaluated: 0;
Free: 1;
Normal: 2;
Tagged: 4;
}>;
static getImportAttributes: ( static getImportAttributes: (
node: node:
| ImportDeclarationJavascriptParser | ImportDeclarationJavascriptParser
@ -16949,13 +16963,18 @@ declare interface Values {
declare class VariableInfo { declare class VariableInfo {
constructor( constructor(
declaredScope: ScopeInfo, declaredScope: ScopeInfo,
freeName?: string | true, name: undefined | string,
flags: VariableInfoFlagsType,
tagInfo?: TagInfo tagInfo?: TagInfo
); );
declaredScope: ScopeInfo; declaredScope: ScopeInfo;
freeName?: string | true; name?: string;
flags: VariableInfoFlagsType;
tagInfo?: TagInfo; tagInfo?: TagInfo;
isFree(): boolean;
isTagged(): boolean;
} }
type VariableInfoFlagsType = 0 | 1 | 2 | 4;
declare interface VirtualModuleConfig { declare interface VirtualModuleConfig {
/** /**
* - The module type * - The module type