118 lines
5.6 KiB
TypeScript
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");
|
|
});
|
|
});
|