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();
}
for (const name of parser.scope.definitions.asSet()) {
const freeInfo = parser.getFreeInfoFromVariable(name);
if (freeInfo === undefined) {
if (parser.isVariableDefined(name)) {
topLevelDeclarations.add(name);
}
}

View File

@ -258,17 +258,42 @@ const getImportAttributes = (node) => {
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 {
/**
* @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
*/
constructor(declaredScope, freeName, tagInfo) {
constructor(declaredScope, name, flags, tagInfo) {
this.declaredScope = declaredScope;
this.freeName = freeName;
this.name = name;
this.flags = flags;
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 */
@ -1426,7 +1451,7 @@ class JavascriptParser extends Parser {
const info = this.getVariableInfo(/** @type {Identifier} */ (expr).name);
if (
typeof info === "string" ||
(info instanceof VariableInfo && typeof info.freeName === "string")
(info instanceof VariableInfo && (info.isFree() || info.isTagged()))
) {
return {
name: info,
@ -1441,7 +1466,7 @@ class JavascriptParser extends Parser {
const info = this.getVariableInfo("this");
if (
typeof info === "string" ||
(info instanceof VariableInfo && typeof info.freeName === "string")
(info instanceof VariableInfo && (info.isFree() || info.isTagged()))
) {
return {
name: info,
@ -4053,13 +4078,13 @@ class JavascriptParser extends Parser {
}
tagInfo = tagInfo.next;
}
if (info.freeName === true) {
if (!info.isFree() && !info.isTagged()) {
if (defined !== undefined) {
return defined();
}
return;
}
name = info.freeName;
name = info.name;
}
const hook = hookMap.get(name);
if (hook !== undefined) {
@ -4818,25 +4843,31 @@ class JavascriptParser extends Parser {
* @param {string} name name
* @param {Tag} tag tag info
* @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);
/** @type {VariableInfo} */
let newInfo;
if (oldInfo === undefined) {
newInfo = new VariableInfo(this.scope, name, {
newInfo = new VariableInfo(this.scope, name, flags, {
tag,
data,
next: undefined
});
} else if (oldInfo instanceof VariableInfo) {
newInfo = new VariableInfo(oldInfo.declaredScope, oldInfo.freeName, {
newInfo = new VariableInfo(
oldInfo.declaredScope,
oldInfo.name,
/** @type {VariableInfoFlagsType} */ (oldInfo.flags | flags),
{
tag,
data,
next: oldInfo.tagInfo
});
}
);
} else {
newInfo = new VariableInfo(oldInfo, true, {
newInfo = new VariableInfo(oldInfo, name, flags, {
tag,
data,
next: undefined
@ -4875,7 +4906,7 @@ class JavascriptParser extends Parser {
const info = this.scope.definitions.get(name);
if (info === undefined) return false;
if (info instanceof VariableInfo) {
return info.freeName === true;
return !info.isFree();
}
return true;
}
@ -4904,7 +4935,12 @@ class JavascriptParser extends Parser {
} else {
this.scope.definitions.set(
name,
new VariableInfo(this.scope, variableInfo, undefined)
new VariableInfo(
this.scope,
variableInfo,
VariableInfoFlags.Free,
undefined
)
);
}
} else {
@ -4917,7 +4953,12 @@ class JavascriptParser extends Parser {
* @returns {VariableInfo} variable info
*/
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) {
const info = this.getVariableInfo(varName);
let name;
if (info instanceof VariableInfo) {
name = info.freeName;
if (typeof name !== "string") return;
if (info instanceof VariableInfo && info.name) {
if (!info.isFree()) 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") {
return;
} else {
@ -5035,7 +5094,7 @@ class JavascriptParser extends Parser {
}
const rootName = getRootName(callee);
if (!rootName) return;
const result = this.getFreeInfoFromVariable(rootName);
const result = this.getNameInfoFromVariable(rootName);
if (!result) return;
const { info: rootInfo, name: resolvedRoot } = result;
const calleeName = objectAndMembersToName(resolvedRoot, rootMembers);
@ -5058,7 +5117,7 @@ class JavascriptParser extends Parser {
const rootName = getRootName(object);
if (!rootName) return;
const result = this.getFreeInfoFromVariable(rootName);
const result = this.getNameInfoFromVariable(rootName);
if (!result) return;
const { info: rootInfo, name: resolvedRoot } = result;
return {
@ -5151,4 +5210,5 @@ module.exports.ALLOWED_MEMBER_TYPES_CALL_EXPRESSION =
module.exports.ALLOWED_MEMBER_TYPES_EXPRESSION =
ALLOWED_MEMBER_TYPES_EXPRESSION;
module.exports.VariableInfo = VariableInfo;
module.exports.VariableInfoFlags = VariableInfoFlags;
module.exports.getImportAttributes = getImportAttributes;

View File

@ -6,6 +6,7 @@
"use strict";
const { UsageState } = require("../ExportsInfo");
const JavascriptParser = require("../javascript/JavascriptParser");
/** @typedef {import("estree").Node} AnyNode */
/** @typedef {import("../Dependency")} Dependency */
@ -15,7 +16,6 @@ const { UsageState } = require("../ExportsInfo");
/** @typedef {import("../ModuleGraphConnection")} ModuleGraphConnection */
/** @typedef {import("../ModuleGraphConnection").ConnectionState} ConnectionState */
/** @typedef {import("../Parser").ParserState} ParserState */
/** @typedef {import("../javascript/JavascriptParser")} JavascriptParser */
/** @typedef {import("../util/runtime").RuntimeSpec} RuntimeSpec */
/** @typedef {Map<TopLevelSymbol | null, Set<string | TopLevelSymbol> | true | undefined>} InnerGraph */
@ -348,7 +348,12 @@ module.exports.tagTopLevelSymbol = (parser, name) => {
}
const fn = new TopLevelSymbol(name);
parser.tagVariable(name, topLevelSymbolTag, fn);
parser.tagVariable(
name,
topLevelSymbolTag,
fn,
JavascriptParser.VariableInfoFlags.Normal
);
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;
isStatementLevelExpression(expr: Expression): boolean;
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;
undefineVariable(name: string): void;
isVariableDefined(name: string): boolean;
@ -7794,6 +7799,9 @@ declare class JavascriptParser extends ParserClass {
getFreeInfoFromVariable(
varName: string
): undefined | { name: string; info: string | VariableInfo };
getNameInfoFromVariable(
varName: string
): undefined | { name: string; info: string | VariableInfo };
getMemberExpressionInfo(
expression:
| ImportExpressionImport
@ -7842,6 +7850,12 @@ declare class JavascriptParser extends ParserClass {
static ALLOWED_MEMBER_TYPES_CALL_EXPRESSION: 1;
static ALLOWED_MEMBER_TYPES_EXPRESSION: 2;
static VariableInfo: typeof VariableInfo;
static VariableInfoFlags: Readonly<{
Evaluated: 0;
Free: 1;
Normal: 2;
Tagged: 4;
}>;
static getImportAttributes: (
node:
| ImportDeclarationJavascriptParser
@ -16949,13 +16963,18 @@ declare interface Values {
declare class VariableInfo {
constructor(
declaredScope: ScopeInfo,
freeName?: string | true,
name: undefined | string,
flags: VariableInfoFlagsType,
tagInfo?: TagInfo
);
declaredScope: ScopeInfo;
freeName?: string | true;
name?: string;
flags: VariableInfoFlagsType;
tagInfo?: TagInfo;
isFree(): boolean;
isTagged(): boolean;
}
type VariableInfoFlagsType = 0 | 1 | 2 | 4;
declare interface VirtualModuleConfig {
/**
* - The module type