rea/src/domain/javascriptSemanticAnalysis....

118 lines
5.6 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js";
import { onlyCallable } from "./javascriptSemanticAnalysis.fixture.js";
const resultValue = (body: string) =>
onlyCallable(
analyzeJavaScriptSemantics(`export function result() { ${body} }`),
"result",
).returnSites[0]?.value;
const mutations = [
'const shared = { mode: "initial" }; const options = { [key]: shared }; options.foo.mode = "updated"; return shared.mode;',
'const shared = { mode: "initial" }; const original = [shared]; const copy = [...original]; copy[0].mode = "updated"; return shared.mode;',
'const original = { nested: { mode: "initial" } }; const copy = { ...original }; copy.nested.mode = "updated"; return original.nested.mode;',
'const shared = { mode: "initial" }; const options = flag ? shared : { mode: "other" }; options.mode = "updated"; return shared.mode;',
'const child = { mode: "initial" }; const options = { layer: { child } }; options.layer.child.mode = "updated"; return child.mode;',
'const options = { mode: "initial" }; options.mode = "updated"; return options.mode;',
"const options = { count: 1 }; options.count += 1; return options.count;",
"const options = { count: 1 }; options.count++; return options.count;",
'const options = { mode: "initial" }; delete options.mode; return options.mode;',
'const options = ["initial"]; options[0] = "updated"; return options[0];',
'const options = { nested: { mode: "initial" } }; options.nested.mode = "updated"; return options.nested.mode;',
'const options = { mode: "initial" }; const alias = options; alias.mode = "updated"; return options.mode;',
'const options = { nested: { mode: "initial" } }; const { nested } = options; nested.mode = "updated"; return options.nested.mode;',
'const nested = { mode: "initial" }; const options = { nested }; options.nested.mode = "updated"; return nested.mode;',
'const options = { mode: "initial" }; [options.mode] = ["updated"]; return options.mode;',
'const options = { mode: "initial" }; for (options.mode of ["updated"]) {} return options.mode;',
'const options = { mode: "initial" }; const key = getKey(); options[key] = "updated"; return options.mode;',
];
describe("JavaScript semantic values after explicit property mutations", () => {
it.each(mutations)("keeps the mutated value unknown: %s", (body) => {
expect(resultValue(body)?.status).toBe("unknown");
});
it("keeps an unrelated shadowed object literal known", () => {
expect(
resultValue(`
const options = { mode: "initial" };
{ const options = { mode: "inner" }; options.mode = "updated"; }
return options.mode;
`),
).toEqual({ status: "literal", value: "initial" });
});
it("keeps scalar slots in a spread source known after a copy slot write", () => {
expect(
resultValue(`
const original = { mode: "initial" };
const copy = { ...original };
copy.mode = "updated";
return original.mode;
`),
).toEqual({ status: "literal", value: "initial" });
});
it("keeps conditional object branches unknown after a property write", () => {
expect(
resultValue(`
const options = flag ? { mode: "initial" } : { mode: "other" };
options.mode = "updated";
return options.mode;
`)?.status,
).toBe("unknown");
});
it("keeps a copied primitive initializer known", () => {
expect(
resultValue(`
const mode = "initial";
const options = { mode };
options.mode = "updated";
return mode;
`),
).toEqual({ status: "literal", value: "initial" });
});
it("retains ordinary immutable object and array projection", () => {
expect(
resultValue(
'const options = { mode: ["initial"] }; return options.mode[0];',
),
).toEqual({ status: "literal", value: "initial" });
});
});
const unchangedProperties = [
'const source = { token: "TOKEN", count: 1 }; source.count = 2; return source.token;',
'const source = { token: "TOKEN", count: 1 }; const alias = source; alias.count++; return source.token;',
'const source = { token: "TOKEN", count: 1 }; delete source.count; return source.token;',
'const source = { nested: { token: "TOKEN", count: 1 } }; source.nested.count = 2; return source.nested.token;',
'const source = { nested: { token: "TOKEN", count: 1 } }; const { nested } = source; nested.count = 2; return source.nested.token;',
'const child = { token: "TOKEN", count: 1 }; const source = { child }; source.child.count = 2; return child.token;',
'const source = ["TOKEN", 1]; source[1] = 2; return source[0];',
'const child = { token: "TOKEN", count: 1 }; const source = [child]; source[0].count = 2; return child.token;',
'const source = { token: "TOKEN" }; source.added = 2; return source.token;',
'const source = { token: "TOKEN", count: 1, other: 1 }; source.count = 2; source.other = 2; return source.token;',
'const left = { token: "TOKEN", count: 1 }; const right = { count: 1 }; const source = { left, right }; source.right.count = 2; return left.count + ":" + left.token;',
];
describe("JavaScript semantic values for properties unaffected by a mutation", () => {
it.each(unchangedProperties)("retains an unaffected property: %s", (body) => {
expect(resultValue(body)).toEqual({
status: "literal",
value: body.includes("left.count") ? "1:TOKEN" : "TOKEN",
});
});
it("keeps the changed slot unknown alongside an unchanged literal slot", () => {
expect(
resultValue(
'const source = { token: "TOKEN", count: 1 }; source.count = 2; return source.count;',
)?.status,
).toBe("unknown");
});
});