fix(javascript): preserve inherited aliases past erased class fields (#5040)

This commit is contained in:
NIKHIL authored and GitHub committed 2026-10-23 02:26:24 +08:00
1 parent be4498d394
commit 9a21cd78ce
4 files changed
+110 -3

No files matched your search

@@ -2,6 +2,12 @@ import * as t from "@babel/types";
import type { JavaScriptSemanticPrimitive } from "./javascriptSemanticValueTypes.js";
import { SEMANTIC_EXPRESSION_DEPTH_LIMIT } from "./javascriptSemanticResourceLimits.js";
/** Type-only class fields neither initialize own slots nor shadow inherited members. */
export const isRuntimeJavaScriptClassProperty = (
node: t.Node,
): node is t.ClassProperty =>
t.isClassProperty(node) && node.declare !== true && node.abstract !== true;
/** TypeScript receiver annotations occupy no positional runtime parameter slot. */
export const runtimeFunctionParameters = (
node: t.Function,
@@ -9,6 +9,67 @@ const resultValue = (body: string) =>
"result",
).returnSites[0]?.value;
it.each([
["field = shared;", "declare field: { mode: string };", "new Derived()"],
[
"get field() { return shared; }",
"declare field: { mode: string };",
"new Derived()",
],
[
"static field = shared;",
"declare static field: { mode: string };",
"Derived",
],
[
"static get field() { return shared; }",
"declare static field: { mode: string };",
"Derived",
],
])(
"follows inherited references past erased class fields: %s",
(base, declaration, receiver) => {
expect(
resultValue(`
const shared = { mode: "initial" };
class Base { ${base} }
class Derived extends Base { ${declaration} }
${receiver}.field.mode = "updated";
return shared.mode;
`)?.status,
).toBe("unknown");
},
);
it.each(["field;", "field = { mode: 'other' };"])(
"preserves a real derived field's shadowing: %s",
(field) => {
expect(
resultValue(`
const shared = { mode: "initial" };
class Base { field = shared; }
class Derived extends Base { ${field} }
const instance = new Derived();
if (instance.field) instance.field.mode = "updated";
return shared.mode;
`),
).toEqual({ status: "literal", value: "initial" });
},
);
it("follows inherited references past an abstract field declaration", () => {
expect(
resultValue(`
const shared = { mode: "initial" };
class Base { field = shared; }
abstract class Derived extends Base { abstract field: { mode: string }; }
const Constructor = Derived as unknown as new () => Base;
new Constructor().field.mode = "updated";
return shared.mode;
`)?.status,
).toBe("unknown");
});
const mutations = [
'const shared = { mode: "initial" }; const options = { [key]: shared }; options.foo.mode = "updated"; return shared.mode;',
'const shared = { mode: "initial" }; const original = [shared]; const copy = [...original]; copy[0].mode = "updated"; return shared.mode;',
@@ -4,6 +4,7 @@ import {
type JavaScriptSemanticAnalysisState,
} from "./javascriptSemanticState.js";
import {
isRuntimeJavaScriptClassProperty,
semanticStaticPropertyKey,
unwrapJavaScriptExpression,
} from "./javascriptAstValues.js";
@@ -118,7 +119,7 @@ const instanceFieldReferences = (
? current.klass.body.body
: current.klass.body.body.slice(0, before);
for (const member of [...members].reverse()) {
if (!t.isClassProperty(member) || member.static) continue;
if (!isRuntimeJavaScriptClassProperty(member) || member.static) continue;
const key = semanticStaticPropertyKey(member.key, member.computed);
if (key !== null && shadowed.has(key)) continue;
if (key !== null) shadowed.add(key);
@@ -169,10 +170,15 @@ const classPropertyDescriptors = (
? -1
: klass.body.body.indexOf(initializedBefore);
klass.body.body.forEach((member, index) => {
if (before >= 0 && index >= before && t.isClassProperty(member)) return;
if (
before >= 0 &&
index >= before &&
isRuntimeJavaScriptClassProperty(member)
)
return;
if (
(t.isClassMethod(member) ||
(kind === "static" && t.isClassProperty(member))) &&
(kind === "static" && isRuntimeJavaScriptClassProperty(member))) &&
member.static === isStatic
)
indexedMembers.push({