Files
rea/src/domain/javascript/javascriptMemberMutationCoverage.test.ts
T

174 lines
6.6 KiB
TypeScript

import { expect, it } from "vitest";
import {
analyzeJavaScriptSemantics,
indexJavaScriptBindingsWithReceiversSteps,
} from "./javascriptSemanticAnalysis.js";
import { onlyCallable } from "./javascriptSemanticAnalysis.fixture.js";
import { semanticResourceLimitReason } from "./javascriptSemanticResourceLimits.js";
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
import { parseJavaScriptSource } from "./javascriptSourceParser.js";
import { completeSemanticSteps } from "./javascriptSemanticTraversal.js";
import {
joinSemanticPropertyPaths,
semanticPropertyPathKeyMatches,
} from "./javascriptSemanticPropertyPaths.js";
const resultValue = (body: string) =>
onlyCallable(
analyzeJavaScriptSemantics(`export function result() { ${body} }`),
"result",
).returnSites[0]?.value;
it("retains common copy exclusions and the union of array prefixes", () => {
const path = joinSemanticPropertyPaths(
[{ excludedKeys: ["1", "2"], startIndex: 1 }],
[{ excludedKeys: ["2", "3"], startIndex: 2 }],
);
const key = path?.[0];
if (key === undefined) throw new Error("Expected joined projection");
expect(
["0", "1", "2", "3", "4", "named"].filter((name) =>
semanticPropertyPathKeyMatches(key, name),
),
).toEqual(["1", "3", "4"]);
const withScalar = joinSemanticPropertyPaths(path ?? [], [0]);
const scalarKey = withScalar?.[0];
if (scalarKey === undefined) throw new Error("Expected joined scalar");
expect(
["0", "1", "2", "3", "named"].filter((name) =>
semanticPropertyPathKeyMatches(scalarKey, name),
),
).toEqual(["0", "1", "3"]);
const exact = [1] as const;
expect(joinSemanticPropertyPaths(exact, ["1"])).toBe(exact);
});
const memberAliasSource = (count: number) => `
import { Worker } from "node:worker_threads";
const alias0 = globalThis;
${Array.from(
{ length: count },
(_, index) =>
`const alias${index + 1} = flag${index} ? alias${index}.globalThis : alias${index}.window;`,
).join("\n")}
alias${count}.URL = replacement;
new Worker(new URL("./worker.js", import.meta.url));
`;
it("joins overflowing member-alias histories into disclosed possible effects", () => {
const source = memberAliasSource(16);
const ir = analyzeJavaScriptSemantics(source);
expect(ir.coverage).toMatchObject({
status: "partial",
omittedCount: null,
resourceLimits: ["alias-selections"],
});
expect(ir.limitations).toContain(
semanticResourceLimitReason("alias-selections"),
);
const facts = analyzeJavaScriptStaticSource(source);
expect(facts.limitations).toContain(
semanticResourceLimitReason("alias-selections"),
);
expect(
facts.references.filter(({ kind }) => kind === "worker"),
).toMatchObject([{ specifier: null }]);
});
it("never records a joined selector as a definite receiver replacement", () => {
const parsed = parseJavaScriptSource(memberAliasSource(16));
if (parsed === null) throw new Error("Expected parsed source");
const index = completeSemanticSteps(
indexJavaScriptBindingsWithReceiversSteps(parsed.program),
);
completeSemanticSteps(index.indexMemberMutationFactsSteps());
const joined = [...index.bindings.bindingsById.values()]
.flatMap(({ memberWrites }) => memberWrites)
.filter(({ path }) => path.includes(null));
expect(joined.length).toBeGreaterThan(0);
expect(
joined.every(({ receiverIdentity }) => receiverIdentity === "possible"),
).toBe(true);
});
it("keeps small selector alternatives exact and preserves an untouched third sibling", () => {
const ir = analyzeJavaScriptSemantics(`
export function result() {
const source = { left: { value: 1 }, right: { value: 3 }, third: { value: 2 } };
const selected = flag ? source.left : source.right;
selected.value = replacement;
return source.third.value;
}
`);
expect(onlyCallable(ir, "result").returnSites[0]?.value).toEqual({
status: "literal",
value: 2,
});
expect(ir.coverage.resourceLimits ?? []).not.toContain("alias-selections");
});
it.each([
"const left={value:1}; const right={value:2}; const window={left,right}; const selected=flag?window.left:window.right; selected.value=9; return left.value;",
"const left={value:1}; const right={value:2}; const globalThis={window:left,globalThis:right}; const selected=flag?globalThis.window:globalThis.globalThis; selected.value=9; return left.value;",
"const local={value:1}; const selected=flag?local:external; selected.value=9; return local.value;",
])("retains local and unresolved mutation targets: %s", (body) => {
expect(resultValue(body)?.status).toBe("unknown");
});
it.each(["first", "second"])(
"retains both captured targets across replacement: %s",
(name) => {
expect(
resultValue(`
const first = { value: 1 };
const second = { value: 2 };
let current = first;
const before = current;
current = second;
const after = current;
const selected = flag ? before : after;
selected.value = 9;
return ${name}.value;
`)?.status,
).toBe("unknown");
},
);
it.each([
"const first={value:1}; const second={value:2}; const captured=first; let selected=first; selected=second; const alias=flag?captured:captured; alias.value=9; return second.value;",
"const first={value:2}; const second={value:1}; let selected=first; selected=second; const captured=selected; const alias=flag?captured:captured; alias.value=9; return first.value;",
"const source={left:{value:1},right:{value:2}}; source.left.value=9; return source.right.value;",
])("preserves captured ownership and unjoined siblings: %s", (body) => {
expect(resultValue(body)).toEqual({ status: "literal", value: 2 });
});
it("preserves exclusions shared by shallow-copy alternatives", () => {
expect(
resultValue(`
const excluded = { value: 2 };
const first = { value: 1 };
const second = { value: 3 };
const source = { excluded, first, second };
const { excluded: leftExcluded, first: leftFirst, ...left } = source;
const { excluded: rightExcluded, second: rightSecond, ...right } = source;
const selected = flag ? left : right;
mutate(selected);
return excluded.value;
`),
).toEqual({ status: "literal", value: 2 });
});
it("keeps a preceding global self-property replacement unresolved", () => {
const facts = analyzeJavaScriptStaticSource(`
import { Worker } from "node:worker_threads";
globalThis.window = replacement;
const selected = flag ? globalThis.window : globalThis;
selected.URL = replacement;
new Worker(new URL("./worker.js", import.meta.url));
`);
expect(
facts.references.filter(({ kind }) => kind === "worker"),
).toMatchObject([{ specifier: null }]);
});