Skip to content

Commit de7d06f

Browse files
fix: do not alias an import a class heritage clause shadows
The parser walks a class' heritage clause before its own name enters scope, so a reference there is still tagged as the import. A namespace read survives that; a bare local binding is captured by the class' own name, which is in its temporal dead zone during `extends`. Record those names on `buildInfo` and leave such references reading through the namespace.
1 parent 9fed212 commit de7d06f

8 files changed

Lines changed: 65 additions & 1 deletion

File tree

‎lib/NormalModule.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1650,6 +1650,7 @@ class NormalModule extends Module {
16501650
pureFunctions: undefined,
16511651
inlineExports: undefined,
16521652
stableExports: undefined,
1653+
classHeritageNames: undefined,
16531654
moduleConcatenationBailout: undefined,
16541655
moduleConcatenationRootBailout: undefined,
16551656
needCreateRequire: undefined,

‎lib/dependencies/HarmonyImportDependencyParserPlugin.js‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,16 +234,27 @@ class HarmonyImportDependencyParserPlugin {
234234
this.options.sourceImport
235235
);
236236

237-
// Parsers are reused across modules, so both are reset per program.
237+
// Parsers are reused across modules, so all three are reset per program.
238238
/** @type {HarmonySettings[]} */
239239
const declaredSpecifiers = [];
240+
/** @type {Set<string>} */
241+
const classHeritageNames = new Set();
240242
/** @type {HarmonyImportSideEffectDependency | undefined} */
241243
let lastSideEffectDependency;
242244

243245
parser.hooks.program.tap(PLUGIN_NAME, () => {
244246
declaredSpecifiers.length = 0;
247+
classHeritageNames.clear();
245248
lastSideEffectDependency = undefined;
246249
});
250+
// The heritage clause is walked before the class name enters scope, so a
251+
// reference to that name resolves here to whatever the enclosing scope has.
252+
parser.hooks.classExtendsExpression.tap(
253+
PLUGIN_NAME,
254+
(_expression, classy) => {
255+
if (classy.id) classHeritageNames.add(classy.id.name);
256+
}
257+
);
247258
parser.hooks.finish.tap(PLUGIN_NAME, () => {
248259
for (const settings of declaredSpecifiers) {
249260
const dependency = settings.dependency;
@@ -268,7 +279,14 @@ class HarmonyImportDependencyParserPlugin {
268279
settings.name
269280
]);
270281
}
282+
if (classHeritageNames.size > 0) {
283+
/** @type {JavascriptModuleBuildInfo} */
284+
(parser.state.module.buildInfo).classHeritageNames = new Set(
285+
classHeritageNames
286+
);
287+
}
271288
declaredSpecifiers.length = 0;
289+
classHeritageNames.clear();
272290
lastSideEffectDependency = undefined;
273291
});
274292

‎lib/dependencies/HarmonyImportSpecifierDependency.js‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ const { ImportPhaseUtils } = require("./ImportPhase");
3636
/** @import { GetConditionFn, ReferencedExports } from "../Dependency" */
3737
/** @import { DependencyTemplateContext } from "../DependencyTemplate" */
3838
/** @import Module, { BuildMeta } from "../Module" */
39+
/** @import { JavascriptModuleBuildInfo } from "../javascript/JavascriptModule" */
3940
/** @import ModuleGraph from "../ModuleGraph" */
4041
/**
4142
* @import ModuleGraphConnection, {
@@ -772,6 +773,18 @@ HarmonyImportSpecifierDependency.Template = class HarmonyImportSpecifierDependen
772773
return;
773774
}
774775
if (!isDeclarableBindingName(dep.name)) return;
776+
// A class heritage clause reads the class' own binding, which is in its
777+
// temporal dead zone there, so only a name user code cannot bind survives.
778+
const originBuildInfo =
779+
/** @type {JavascriptModuleBuildInfo | undefined} */
780+
(/** @type {Module} */ (module).buildInfo);
781+
if (
782+
originBuildInfo !== undefined &&
783+
originBuildInfo.classHeritageNames !== undefined &&
784+
originBuildInfo.classHeritageNames.has(dep.name)
785+
) {
786+
return;
787+
}
775788
const importedModule = connection && connection.module;
776789
if (!importedModule) return;
777790
if (

‎lib/javascript/JavascriptModule.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const makeSerializable = require("../util/makeSerializable");
2525
* @property {Set<string>=} pureFunctions names of locally declared functions known to be free of side effects
2626
* @property {boolean=} inlineExports whether this module was parsed with `optimization.inlineExports` enabled (gates inlining of its exports)
2727
* @property {Set<string>=} stableExports names of own exports the language pins to one value, so an importer may read them into a local binding
28+
* @property {Set<string>=} classHeritageNames names a class binds while its own heritage clause is parsed in the enclosing scope, so a reference there resolves to the class at runtime but to the outer name here
2829
* @property {boolean=} usesTopLevelUsingDeclaration module scope holds a `using`/`await using` declaration, so its resources must be disposed when the module finished evaluating
2930
*/
3031

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const Base = class Base {};
2+
export const Untouched = "untouched";
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Base, Untouched } from "./exports";
2+
3+
const extend = (fn) => class Base extends fn(Base) {};
4+
const selfExtend = () => class Untouched extends Object {};
5+
6+
it("should not capture a heritage reference with the class' own name", () => {
7+
expect(new (extend((x) => x))()).toBeInstanceOf(Base);
8+
});
9+
10+
it("should read the import where no class shadows the name", () => {
11+
expect(Untouched).toBe("untouched");
12+
expect(typeof selfExtend()).toBe("function");
13+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
"use strict";
2+
3+
/** @type {import("../../../../").Configuration} */
4+
module.exports = {
5+
mode: "development",
6+
devtool: false,
7+
optimization: {
8+
localImportBindings: true,
9+
concatenateModules: false
10+
}
11+
};

‎types.d.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15251,6 +15251,11 @@ declare interface KnownJavascriptModuleBuildInfo {
1525115251
*/
1525215252
stableExports?: Set<string>;
1525315253

15254+
/**
15255+
* names a class binds while its own heritage clause is parsed in the enclosing scope, so a reference there resolves to the class at runtime but to the outer name here
15256+
*/
15257+
classHeritageNames?: Set<string>;
15258+
1525415259
/**
1525515260
* module scope holds a `using`/`await using` declaration, so its resources must be disposed when the module finished evaluating
1525615261
*/

0 commit comments

Comments
 (0)