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

This commit is contained in:
NIKHIL
2026-10-22 23:56:24 +05:30
committed by GitHub
parent be4498d394
commit 9a21cd78ce
4 changed files with 110 additions and 3 deletions
@@ -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({
@@ -10,6 +10,40 @@ import { createJavaScriptArtifactReader } from "../../../../src/artifacts/javasc
import { analyzeJavaScriptArtifactFiles } from "../../../../src/application/javascript/JavaScriptArtifactAnalysis.js";
import { buildJavaScriptArtifactGraph } from "../../../../src/application/javascript/JavaScriptArtifactGraphBuilder.js";
it("keeps an endpoint uncertain after mutation through a declared inherited field", async () => {
const root = await createTestTempDirectory("rea-js-erased-field-");
await writeFile(
join(root, "app.ts"),
`
const options = { url: "/before" };
class Base { field = options; }
class Derived extends Base { declare field: { url: string }; }
new Derived().field.url = "/after";
const target = options.url;
fetch(target);
`,
);
const snapshot = await scanCanonicalArtifactInventory(root, {});
const reader = createJavaScriptArtifactReader(root, "directory");
try {
const files = await readJavaScriptArtifactFiles(reader, snapshot);
const analysis = analyzeJavaScriptArtifactFiles(files);
const graph = buildJavaScriptArtifactGraph(snapshot, files, analysis);
const endpoints = graph.nodes.filter(({ kind }) => kind === "endpoint");
expect(endpoints).toHaveLength(1);
expect(endpoints[0]?.observations[0]).toMatchObject({
properties: { value: "target", resolution: "partial" },
evidence: {
authority: "ast-static-analysis",
state: "observed",
limitations: [expect.stringContaining("not an exact literal")],
},
});
} finally {
await reader.close();
}
});
it("keeps ambiguous endpoint candidates inferred through artifact graph projection", async () => {
const root = await createTestTempDirectory("rea-js-endpoint-certainty-");
await writeFile(join(root, "app.js"), 'form.get("shared"); fetch("shared");');