242 lines
7.1 KiB
TypeScript
242 lines
7.1 KiB
TypeScript
import { it } from "@fast-check/vitest";
|
|
import { describe, expect } from "vitest";
|
|
|
|
import { analyzeJavaScriptSemantics } from "../../../src/domain/javascriptSemanticAnalysis.js";
|
|
import { semanticBinding } from "../../../src/domain/javascriptSemanticIr.js";
|
|
import {
|
|
onlyCallable,
|
|
topLevelBinding,
|
|
} from "./javascriptSemanticAnalysis.support.js";
|
|
|
|
describe("JavaScript semantic analysis: calls 1", () => {
|
|
it("links exact local calls to parameters, returns, and closure captures", () => {
|
|
const ir = analyzeJavaScriptSemantics(`
|
|
const prefix = "rea";
|
|
function render(value, { mode }) {
|
|
return prefix + value + mode;
|
|
}
|
|
const alias = render;
|
|
const result = alias(input, { mode: "fast" });
|
|
`);
|
|
|
|
expect(ir.schemaVersion).toBe(4);
|
|
const render = onlyCallable(ir, "render");
|
|
const call = ir.callSites.find(
|
|
({ calleeCallableIds }) => calleeCallableIds[0] === render.callableId,
|
|
);
|
|
expect(call).toMatchObject({
|
|
kind: "call",
|
|
callerCallableId: null,
|
|
resolution: "exact",
|
|
calleeCallableIds: [render.callableId],
|
|
arguments: [
|
|
{ index: 0, spread: false },
|
|
{ index: 1, spread: false },
|
|
],
|
|
});
|
|
if (call === undefined) throw new Error("Missing render call");
|
|
expect(
|
|
ir.argumentFlows
|
|
.filter(({ callSiteId }) => callSiteId === call.callSiteId)
|
|
.map(({ argumentIndex, parameterBindingId }) => ({
|
|
argumentIndex,
|
|
parameter: semanticBinding(ir, parameterBindingId)?.name,
|
|
})),
|
|
).toEqual([
|
|
{ argumentIndex: 0, parameter: "value" },
|
|
{ argumentIndex: 1, parameter: "mode" },
|
|
]);
|
|
expect(ir.callReturnFlows).toEqual([
|
|
expect.objectContaining({
|
|
callSiteId: call.callSiteId,
|
|
callableId: render.callableId,
|
|
returnSiteId: render.returnSites[0]?.returnSiteId,
|
|
}),
|
|
]);
|
|
expect(ir.closureCaptures).toEqual([
|
|
expect.objectContaining({
|
|
callableId: render.callableId,
|
|
bindingId: topLevelBinding(ir, "prefix").bindingId,
|
|
}),
|
|
]);
|
|
expect(ir.frontiers).toEqual([]);
|
|
});
|
|
|
|
it("recovers explicit Promise ownership, chains, aggregation, and awaits", () => {
|
|
const ir = analyzeJavaScriptSemantics(`
|
|
async function run(value) {
|
|
const base = Promise.resolve(value);
|
|
const chained = base.then(work).finally(cleanup);
|
|
await chained;
|
|
Promise.resolve(value).then(work);
|
|
return Promise.all([base, Promise.resolve(value)]);
|
|
}
|
|
const settled = Promise.allSettled([Promise.resolve(1)]);
|
|
`);
|
|
|
|
expect(
|
|
ir.promiseOperations.map(
|
|
({ kind, method, ownership, sourceResolution }) => ({
|
|
kind,
|
|
method,
|
|
ownership,
|
|
sourceResolution,
|
|
}),
|
|
),
|
|
).toEqual(
|
|
expect.arrayContaining([
|
|
{
|
|
kind: "static",
|
|
method: "resolve",
|
|
ownership: "assigned",
|
|
sourceResolution: "complete",
|
|
},
|
|
{
|
|
kind: "chain",
|
|
method: "then",
|
|
ownership: "chained",
|
|
sourceResolution: "complete",
|
|
},
|
|
{
|
|
kind: "chain",
|
|
method: "finally",
|
|
ownership: "assigned",
|
|
sourceResolution: "complete",
|
|
},
|
|
{
|
|
kind: "awaited-expression",
|
|
method: "await",
|
|
ownership: "awaited",
|
|
sourceResolution: "complete",
|
|
},
|
|
{
|
|
kind: "chain",
|
|
method: "then",
|
|
ownership: "detached",
|
|
sourceResolution: "complete",
|
|
},
|
|
{
|
|
kind: "aggregate",
|
|
method: "all",
|
|
ownership: "returned",
|
|
sourceResolution: "complete",
|
|
},
|
|
{
|
|
kind: "aggregate",
|
|
method: "allSettled",
|
|
ownership: "assigned",
|
|
sourceResolution: "complete",
|
|
},
|
|
]),
|
|
);
|
|
const aggregate = ir.promiseOperations.find(
|
|
({ method }) => method === "all",
|
|
);
|
|
expect(aggregate?.sourcePromiseIds).toHaveLength(2);
|
|
expect(aggregate?.returnSiteId).not.toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("JavaScript semantic analysis: calls 2", () => {
|
|
it("does not treat a shadowed Promise binding as the intrinsic", () => {
|
|
const ir = analyzeJavaScriptSemantics(`
|
|
function run(Promise) {
|
|
return Promise.resolve(1).then(work);
|
|
}
|
|
`);
|
|
|
|
expect(ir.promiseOperations).toEqual([
|
|
expect.objectContaining({
|
|
kind: "chain",
|
|
method: "then",
|
|
ownership: "returned",
|
|
sourcePromiseIds: [],
|
|
sourceResolution: "unresolved",
|
|
}),
|
|
]);
|
|
});
|
|
|
|
it("bounds Promise recovery with an explicit frontier", () => {
|
|
const ir = analyzeJavaScriptSemantics(
|
|
`
|
|
Promise.resolve(1);
|
|
Promise.resolve(2);
|
|
Promise.resolve(3);
|
|
`,
|
|
{ maxPromiseOperations: 1 },
|
|
);
|
|
|
|
expect(ir.promiseOperations).toHaveLength(1);
|
|
expect(ir.coverage).toMatchObject({
|
|
status: "truncated",
|
|
limitsReached: ["maxPromiseOperations"],
|
|
});
|
|
});
|
|
|
|
it("fingerprints formatting and local-name changes identically", () => {
|
|
const left = analyzeJavaScriptSemantics(`
|
|
function calculate(value) {
|
|
const result = value + 1;
|
|
return result;
|
|
}
|
|
`);
|
|
const right = analyzeJavaScriptSemantics(
|
|
"function renamed(input){const output=input+1;return output}",
|
|
);
|
|
const changed = analyzeJavaScriptSemantics(
|
|
"function renamed(input){const output=input+2;return output}",
|
|
);
|
|
|
|
expect(left.functionFingerprints).toHaveLength(1);
|
|
expect(right.functionFingerprints).toHaveLength(1);
|
|
expect(left.functionFingerprints[0]?.components).toEqual(
|
|
right.functionFingerprints[0]?.components,
|
|
);
|
|
expect(
|
|
changed.functionFingerprints[0]?.components.literalSetSha256,
|
|
).not.toBe(left.functionFingerprints[0]?.components.literalSetSha256);
|
|
});
|
|
|
|
it("retains ambiguous local callees and explicit dynamic frontiers", () => {
|
|
const ir = analyzeJavaScriptSemantics(`
|
|
function left(value) { return value; }
|
|
function right(value) { return value; }
|
|
const selected = chooseLeft ? left : right;
|
|
selected(input);
|
|
receiver[key](input);
|
|
external(input);
|
|
`);
|
|
|
|
const selected = ir.callSites.find(
|
|
({ resolution }) => resolution === "ambiguous",
|
|
);
|
|
expect(selected?.calleeCallableIds).toEqual(
|
|
[
|
|
onlyCallable(ir, "left").callableId,
|
|
onlyCallable(ir, "right").callableId,
|
|
].sort(),
|
|
);
|
|
expect(
|
|
ir.argumentFlows.filter(
|
|
({ callSiteId }) => callSiteId === selected?.callSiteId,
|
|
),
|
|
).toHaveLength(2);
|
|
expect(ir.frontiers).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
kind: "dynamic-call",
|
|
reason: expect.stringMatching(/ambiguous/iu),
|
|
}),
|
|
expect.objectContaining({
|
|
kind: "dynamic-property",
|
|
reason: expect.stringMatching(/computed member/iu),
|
|
}),
|
|
expect.objectContaining({
|
|
kind: "dynamic-call",
|
|
reason: "Unresolved call target external.",
|
|
}),
|
|
]),
|
|
);
|
|
});
|
|
});
|