Files
rea/src/domain/javascript/javascriptSemanticAnalysis.getter_expansion.test.ts
T
morluto 880b3a510b fix(javascript): bound static getter reference expansion
Replace recursive getter histories with a query-owned worklist and memo. Bound
precise expansion at 512 states and widen growing receiver cycles; retain
escaped roots and constructed children rather than silently dropping suffixes.
Carry the resource limit through semantic coverage, worker transport and graph
Evidence while preserving direct writes, imports and unrelated literal facts.

The small source probe reduced 65/326 paths to one widened reference for five
and six getters. The original large Claude artifact was not rerun. Validate
receiver/alias/mutation controls and compiled CLI/MCP consumers.

Refs #3839, #3848

Co-authored-by: Yi Han <yhan606@aucklanduni.ac.nz>
2026-10-16 06:06:00 +08:00

95 lines
2.9 KiB
TypeScript

import { expect, it } from "vitest";
import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js";
import { onlyBinding } from "./javascriptSemanticAnalysis.fixture.js";
const permutationSource = (count: number, isStatic: boolean) => {
const getterPrefix = isStatic ? "static " : "";
const access = isStatic ? "Request.field0" : "new Request().field0";
const getters = Array.from(
{ length: count },
(_, index) =>
`${getterPrefix}get field${index}() { return this[key].field${index}; }`,
).join("\n");
return `
import imported from "dep";
const shared = { value: 1 };
class Request {
${getters}
${getterPrefix}get state() { return shared; }
}
consume(${access});
const result = shared.value;
const unrelated = 42;
`;
};
const expectRetainedFacts = (
ir: ReturnType<typeof analyzeJavaScriptSemantics>,
) => {
expect(onlyBinding(ir, "unrelated").value).toEqual({
status: "literal",
value: 42,
});
expect(onlyBinding(ir, "imported").provenance.status).toBe("module");
expect(ir.moduleLinks).toContainEqual(
expect.objectContaining({ kind: "import", specifier: "dep" }),
);
};
it.each([false, true])(
"bounds unknown-key %s getter permutations and preserves unrelated facts",
(isStatic) => {
const ir = analyzeJavaScriptSemantics(permutationSource(10, isStatic));
expect(ir.coverage).toMatchObject({
status: "partial",
omittedCount: null,
resourceLimits: ["getter-expansion"],
});
expect(onlyBinding(ir, "result").value.status).toBe("unknown");
expect(onlyBinding(ir, "result").provenance).toMatchObject({
status: "local",
origins: [],
});
expect(ir.limitations.join(" ")).toMatch(
/Getter expansion was conservatively bounded/,
);
expectRetainedFacts(ir);
},
);
it("keeps direct field writes through an escaped growing getter result", () => {
const ir = analyzeJavaScriptSemantics(`
import imported from "dep";
const shared = { x: 1 };
class Request {
slot = shared;
get grow() { return this.grow.more; }
}
new Request()[key].x = 2;
const result = shared.x;
const unrelated = 42;
`);
expect(onlyBinding(ir, "result").value.status).toBe("unknown");
expect(ir.coverage.resourceLimits).toEqual(["getter-expansion"]);
expectRetainedFacts(ir);
});
it("escapes constructed fields when a getter query widens its suffix", () => {
const ir = analyzeJavaScriptSemantics(`
import imported from "dep";
const shared = { value: 1 };
class Inner { alias = shared; }
class Outer {
get result() { return new Inner(); }
get grow() { return this.grow.more; }
}
new Outer()[key].alias.value = 2;
const result = shared.value;
const unrelated = 42;
`);
expect(onlyBinding(ir, "result").value.status).toBe("unknown");
expect(ir.coverage.resourceLimits).toEqual(["getter-expansion"]);
expectRetainedFacts(ir);
});