mirror of https://github.com/webpack/webpack.git
fix: distinguish free variable and tagged variable (#19795)
This commit is contained in:
parent
61a15a672e
commit
cbfba9a150
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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, {
|
||||
tag,
|
||||
data,
|
||||
next: oldInfo.tagInfo
|
||||
});
|
||||
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;
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export const a = 1;
|
|
@ -0,0 +1 @@
|
|||
exports.b = 2;
|
|
@ -0,0 +1 @@
|
|||
export const c = 3;
|
|
@ -0,0 +1 @@
|
|||
export const d = 4;
|
|
@ -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);
|
||||
})
|
|
@ -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"];
|
||||
}
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
"use strict";
|
||||
|
||||
const supportsWorker = require("../../../helpers/supportsWorker");
|
||||
|
||||
module.exports = () => supportsWorker();
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
]
|
||||
};
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue