mirror of
https://github.com/morluto/rea.git
synced 2026-11-04 22:06:10 +00:00
refactor(javascript): group domain semantics and contracts (#907)
This commit is contained in:
1 parent
d5f28fb29c
commit
f6a58f1062
253 files changed
+432
-410
No files matched your search
@@ -0,0 +1,285 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js";
|
||||
import { semanticBinding } from "./javascriptSemanticIr.js";
|
||||
import {
|
||||
onlyCallable,
|
||||
topLevelBinding,
|
||||
} from "./javascriptSemanticAnalysis.fixture.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.schema).toBe("JavaScriptSemanticIR");
|
||||
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).toBeTypeOf("string");
|
||||
});
|
||||
});
|
||||
|
||||
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("retains every promise operation found in the finite source", () => {
|
||||
const ir = analyzeJavaScriptSemantics(
|
||||
`
|
||||
Promise.resolve(1);
|
||||
Promise.resolve(2);
|
||||
Promise.resolve(3);
|
||||
`,
|
||||
);
|
||||
|
||||
expect(ir.promiseOperations).toHaveLength(3);
|
||||
expect(ir.coverage.status).toBe("complete");
|
||||
});
|
||||
|
||||
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.",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("JavaScript semantic rest parameter flow", () => {
|
||||
it.each([
|
||||
{
|
||||
parameters: "first, ...rest",
|
||||
arguments: "1, 2, 3",
|
||||
expected: [
|
||||
[0, "first"],
|
||||
[1, "rest"],
|
||||
[2, "rest"],
|
||||
],
|
||||
},
|
||||
{
|
||||
parameters: "...rest",
|
||||
arguments: "1, 2, 3",
|
||||
expected: [
|
||||
[0, "rest"],
|
||||
[1, "rest"],
|
||||
[2, "rest"],
|
||||
],
|
||||
},
|
||||
{
|
||||
parameters: "first, second",
|
||||
arguments: "1, 2, 3",
|
||||
expected: [
|
||||
[0, "first"],
|
||||
[1, "second"],
|
||||
],
|
||||
},
|
||||
{
|
||||
parameters: "first, ...rest",
|
||||
arguments: "1, ...values, 3",
|
||||
expected: [[0, "first"]],
|
||||
},
|
||||
])(
|
||||
"links exact positions for ($parameters) called with ($arguments)",
|
||||
({ parameters, arguments: arguments_, expected }) => {
|
||||
const ir = analyzeJavaScriptSemantics(
|
||||
`function collect(${parameters}) { return 1; } collect(${arguments_});`,
|
||||
);
|
||||
expect(
|
||||
ir.argumentFlows.map(({ argumentIndex, parameterBindingId }) => [
|
||||
argumentIndex,
|
||||
semanticBinding(ir, parameterBindingId)?.name,
|
||||
]),
|
||||
).toEqual(expected);
|
||||
},
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user