rea/tests/composition/javascript-applications/javascriptSemanticAnalysis....

265 lines
8.4 KiB
TypeScript

import { it } from "@fast-check/vitest";
import { describe, expect } from "vitest";
import { analyzeJavaScriptSemantics } from "../../../src/domain/javascriptSemanticAnalysis.js";
import {
onlyCallable,
topLevelBinding,
} from "./javascriptSemanticAnalysis.support.js";
describe("JavaScript semantic analysis: dataflow 1", () => {
it("recovers static object reads, writes, spreads, and destructuring", () => {
const ir = analyzeJavaScriptSemantics(`
const source = { token: "TOKEN", count: 1 };
const { token } = source;
const copy = { ...source };
source.count = 2;
const read = source.token;
`);
expect(
ir.objectOperations.map(({ kind, propertyName, resolution }) => ({
kind,
propertyName,
resolution,
})),
).toEqual(
expect.arrayContaining([
{
kind: "destructure",
propertyName: "token",
resolution: "complete",
},
{ kind: "spread", propertyName: null, resolution: "complete" },
{ kind: "write", propertyName: "count", resolution: "complete" },
{ kind: "read", propertyName: "token", resolution: "complete" },
]),
);
});
it("does not project positional argument flow after a spread", () => {
const ir = analyzeJavaScriptSemantics(`
function target(first, second, third) { return third; }
target(one, ...rest, three);
`);
expect(ir.argumentFlows.map(({ argumentIndex }) => argumentIndex)).toEqual([
0,
]);
});
it("applies independent hard bounds to every new semantic fact family", () => {
const ir = analyzeJavaScriptSemantics(
`
const captured = 1;
function target(value, other) { return value + other + captured; }
target(1, 2);
target(3, 4);
object[first];
object[second];
`,
{
maxCallSites: 1,
maxCallArguments: 1,
maxArgumentFlows: 0,
maxCallReturnFlows: 0,
maxClosureCaptures: 0,
maxFrontiers: 1,
},
);
expect(ir.callSites).toHaveLength(1);
expect(ir.callSites[0]?.arguments).toEqual([
expect.objectContaining({ index: 0, spread: false }),
]);
expect(ir.argumentFlows).toEqual([]);
expect(ir.callReturnFlows).toEqual([]);
expect(ir.closureCaptures).toEqual([]);
expect(ir.frontiers).toHaveLength(1);
expect(ir.coverage).toMatchObject({
status: "truncated",
limitsReached: expect.arrayContaining([
"maxCallSites",
"maxCallArguments",
"maxArgumentFlows",
"maxCallReturnFlows",
"maxClosureCaptures",
"maxFrontiers",
]),
});
});
it("recovers only direct return sites with literal and unknown object fields", () => {
const ir = analyzeJavaScriptSemantics(`
export default function parseMarkdown(value) {
if (value.startsWith("# "))
return { type: "heading", depth: 1, text: value.slice(2) };
function nested() { return { type: "nested" }; }
const arrow = () => ({ type: "arrow" });
void nested; void arrow;
return { type: "paragraph", text: value.replaceAll("x", "y") };
}
`);
const parse = onlyCallable(ir, "parseMarkdown");
expect(parse.returnCoverage).toEqual({
status: "complete",
retainedCount: 2,
omittedCount: 0,
limitsReached: [],
});
expect(parse.returnSites).toHaveLength(2);
expect(parse.returnSites[0]?.value).toMatchObject({
status: "object",
unknownProperties: false,
omittedProperties: 0,
properties: expect.arrayContaining([
{ name: "depth", value: { status: "literal", value: 1 } },
{ name: "type", value: { status: "literal", value: "heading" } },
{
name: "text",
value: {
status: "unknown",
reason: "Unsupported CallExpression value.",
},
},
]),
});
expect(onlyCallable(ir, "nested").returnSites).toHaveLength(1);
expect(onlyCallable(ir, "arrow").returnSites).toHaveLength(1);
expect(ir.moduleLinks).toContainEqual(
expect.objectContaining({
exportedName: "default",
callableId: parse.callableId,
}),
);
});
});
describe("JavaScript semantic analysis: dataflow 2", () => {
it("retains partial property coverage, empty returns, and return limits", () => {
const partial = analyzeJavaScriptSemantics(`
const spread = () => ({ type: "spread", ...dynamic });
const computed = () => ({ type: "computed", [key]: 1 });
function noReturn() { throw new Error("stop"); }
function emptyReturn() { return; }
`);
expect(onlyCallable(partial, "spread").returnSites[0]?.value).toMatchObject(
{
status: "object",
unknownProperties: true,
omittedProperties: null,
},
);
expect(
onlyCallable(partial, "computed").returnSites[0]?.value,
).toMatchObject({
status: "object",
unknownProperties: true,
omittedProperties: 1,
});
expect(onlyCallable(partial, "noReturn").returnSites).toEqual([]);
expect(onlyCallable(partial, "emptyReturn").returnSites[0]?.value).toEqual({
status: "unknown",
reason: "Return has no value.",
});
const limited = analyzeJavaScriptSemantics(
"function many(value) { if (value) return 1; return 2; }",
{ maxReturnSites: 1 },
);
expect(onlyCallable(limited, "many")).toMatchObject({
returnSites: [{ value: { status: "literal", value: 1 } }],
returnCoverage: {
status: "truncated",
retainedCount: 1,
omittedCount: 1,
limitsReached: ["maxReturnSites"],
},
});
expect(limited.coverage.limitsReached).toContain("maxReturnSites");
});
it("fails closed on assignment ambiguity, alias cycles, and lattice limits", () => {
const ambiguous = analyzeJavaScriptSemantics(`
let current = "first";
current = "second";
const left = right;
const right = left;
`);
expect(topLevelBinding(ambiguous, "current").value).toMatchObject({
status: "ambiguous",
});
expect(topLevelBinding(ambiguous, "left").value).toMatchObject({
status: "cycle",
});
expect(topLevelBinding(ambiguous, "right").provenance).toMatchObject({
status: "cycle",
});
const limited = analyzeJavaScriptSemantics(
'const channel = enabled ? "one" : "two";',
{ maxUnionValues: 1 },
);
expect(topLevelBinding(limited, "channel").value).toEqual({
status: "limit-reached",
reason: "maxUnionValues reached.",
});
expect(limited.coverage).toMatchObject({
status: "truncated",
limitsReached: ["maxUnionValues"],
});
expect(limited.coverage.omittedCount).toBeGreaterThan(0);
});
it("does not invent exact module paths for dynamic property access", () => {
const ir = analyzeJavaScriptSemantics(`
const key = getKey();
const dynamicMember = require("electron")[key];
const { [key]: dynamicBinding } = require("electron");
`);
expect(topLevelBinding(ir, "dynamicMember").provenance).toMatchObject({
status: "unknown",
origins: [],
});
expect(topLevelBinding(ir, "dynamicBinding").provenance).toMatchObject({
status: "unknown",
origins: [],
});
expect(
ir.moduleLinks.filter(({ localName }) =>
["dynamicMember", "dynamicBinding"].includes(localName ?? ""),
),
).toEqual([]);
});
it("bounds retained scopes and bindings without corrupting outer resolution", () => {
const ir = analyzeJavaScriptSemantics(
`
const retained = "yes";
const omitted = "no";
retained;
function nested(value) { return retained + value; }
`,
{ maxBindings: 1, maxCallables: 0, maxScopes: 1 },
);
expect(ir.bindings.map(({ name }) => name)).toEqual(["retained"]);
expect(ir.callables).toEqual([]);
expect(ir.coverage.status).toBe("truncated");
expect(ir.coverage.limitsReached).toEqual(
expect.arrayContaining(["maxBindings", "maxCallables", "maxScopes"]),
);
expect(ir.references.filter(({ name }) => name === "retained")).toEqual(
expect.arrayContaining([
expect.objectContaining({
resolution: "resolved",
bindingId: topLevelBinding(ir, "retained").bindingId,
}),
expect.objectContaining({ resolution: "unknown", bindingId: null }),
]),
);
});
});