mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
fix(javascript): preserve inherited aliases past erased class fields (#5040)
This commit is contained in:
@@ -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({
|
||||
|
||||
+34
@@ -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");');
|
||||
|
||||
Reference in New Issue
Block a user