147 lines
4.4 KiB
TypeScript
147 lines
4.4 KiB
TypeScript
import { writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { expect } from "vitest";
|
|
import { z } from "zod";
|
|
|
|
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
|
|
import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import { cliTest } from "../../support/cli/cliFixture.js";
|
|
|
|
cliTest.for(["CLI", "stdio MCP"] as const)(
|
|
"keeps getter receiver mutations uncertain in exported Evidence through %s",
|
|
async (surface, { cli }) => {
|
|
const root = await createTestTempDirectory("rea-getter-receivers-");
|
|
await writeFile(
|
|
join(root, "app.mjs"),
|
|
`
|
|
export function self() {
|
|
const box = { x: 1, get self() { return this; } };
|
|
box.self.x = 2;
|
|
return box.x;
|
|
}
|
|
export function property() {
|
|
const shared = { x: 1 };
|
|
const box = { s: shared, get v() { return this.s; } };
|
|
box.v.x = 2;
|
|
return shared.x;
|
|
}
|
|
export function privateField() {
|
|
const shared = { x: 1 };
|
|
class B { #s = shared; get v() { return this.#s; } }
|
|
new B().v.x = 2;
|
|
return shared.x;
|
|
}
|
|
export function superclass() {
|
|
const shared = { x: 1 };
|
|
class A { get v() { return shared; } }
|
|
class B extends A { get v() { return super.v; } }
|
|
new B().v.x = 2;
|
|
return shared.x;
|
|
}
|
|
export function arrayReceiver() {
|
|
const arr = [{ x: 1, get self() { return this; } }];
|
|
arr[0].self.x = 2;
|
|
return arr[0].x;
|
|
}
|
|
export function nestedArrayReceiver() {
|
|
const outer = { arr: [{ x: 1, get self() { return this; } }] };
|
|
outer.arr[0].self.x = 2;
|
|
return outer.arr[0].x;
|
|
}
|
|
export function assignedReceiver() {
|
|
let box;
|
|
box = { x: 1, get self() { return this; } };
|
|
box.self.x = 2;
|
|
return box.x;
|
|
}
|
|
export function savedArrayReceiver() {
|
|
const arr = [{ x: 1, get self() { return this; } }];
|
|
const saved = arr[0].self;
|
|
saved.x = 2;
|
|
return arr[0].x;
|
|
}
|
|
export function arrayReadOnly() {
|
|
const arr = [{ x: 1, get self() { return this; } }];
|
|
const saved = arr[0].self;
|
|
return arr[0].x;
|
|
}
|
|
export function replacementKeepsOldObject() {
|
|
const old = { x: 1 };
|
|
let box = old;
|
|
box = { x: 1, old: box, get self() { return this; } };
|
|
box.self.x = 2;
|
|
return old.x;
|
|
}
|
|
export function replacementMutatesCapturedObject() {
|
|
const old = { x: 1 };
|
|
let box = old;
|
|
box = { old: box, get self() { return this; } };
|
|
box.self.old.x = 2;
|
|
return old.x;
|
|
}
|
|
export function untouched() {
|
|
const box = { x: 1, y: 1, get self() { return this; } };
|
|
box.self.y = 2;
|
|
return box.x;
|
|
}
|
|
`,
|
|
);
|
|
let document: unknown;
|
|
if (surface === "CLI") {
|
|
const response = await cli.run({
|
|
arguments: ["analyze-javascript-application", root, "--json"],
|
|
environment: { REA_LOG_LEVEL: "silent" },
|
|
});
|
|
expect(response.exitCode, response.stderr).toBe(0);
|
|
document = response.json;
|
|
} else {
|
|
const { call } = await connectLocalToolsMcp();
|
|
const response = await call("analyze_javascript_application", {
|
|
input_path: root,
|
|
format: "directory",
|
|
});
|
|
expect(response.isError, JSON.stringify(response)).not.toBe(true);
|
|
document = response.structuredContent;
|
|
}
|
|
const { graph } = z
|
|
.object({ normalized_result: javascriptApplicationAnalysisResultSchema })
|
|
.parse(document).normalized_result;
|
|
for (const [name, status] of [
|
|
["self", "unknown"],
|
|
["property", "unknown"],
|
|
["privateField", "unknown"],
|
|
["superclass", "unknown"],
|
|
["arrayReceiver", "unknown"],
|
|
["nestedArrayReceiver", "unknown"],
|
|
["assignedReceiver", "unknown"],
|
|
["savedArrayReceiver", "unknown"],
|
|
["arrayReadOnly", "literal"],
|
|
["replacementKeepsOldObject", "literal"],
|
|
["replacementMutatesCapturedObject", "unknown"],
|
|
["untouched", "literal"],
|
|
] as const) {
|
|
const projection = graph.nodes
|
|
.flatMap(({ observations }) => observations)
|
|
.find(
|
|
({ properties }) =>
|
|
properties.semantic_role === "export-return-shapes" &&
|
|
properties.exported_name === name,
|
|
);
|
|
expect(projection, name).toBeDefined();
|
|
expect(projection?.properties.static_return_shapes).toMatchObject([
|
|
{
|
|
value_status: status,
|
|
fields: [
|
|
{
|
|
path: "",
|
|
state: status,
|
|
...(status === "literal" ? { value: 1 } : {}),
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
}
|
|
},
|
|
);
|