108 lines
2.8 KiB
TypeScript
108 lines
2.8 KiB
TypeScript
import { expect, it } from "vitest";
|
|||
|
|
|
||
|
|
import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js";
|
||
|
|
|
||
|
|
const returned = (source: string) => {
|
||
|
|
const ir = analyzeJavaScriptSemantics(source);
|
||
|
|
const callable = ir.callables.find(({ name }) => name === "observe");
|
||
|
|
return callable?.returnSites[0]?.value;
|
||
|
|
};
|
||
|
|
|
||
|
|
it("keeps members unknown after a write through a local setter", () => {
|
||
|
|
// The setter runs on assignment and may replace any member of `this`.
|
||
|
|
expect(
|
||
|
|
returned(`
|
||
|
|
export function observe() {
|
||
|
|
const state = { value: 1, set update(next) { this.value = next; } };
|
||
|
|
state.update = 2;
|
||
|
|
return state;
|
||
|
|
}
|
||
|
|
`),
|
||
|
|
).toMatchObject({ status: "unknown" });
|
||
|
|
});
|
||
|
|
|
||
|
|
it("keeps sibling literals when the written key is an ordinary property", () => {
|
||
|
|
expect(
|
||
|
|
returned(`
|
||
|
|
export function observe() {
|
||
|
|
const state = { value: 1, update: 0 };
|
||
|
|
state.update = 2;
|
||
|
|
return state;
|
||
|
|
}
|
||
|
|
`),
|
||
|
|
).toMatchObject({
|
||
|
|
status: "object",
|
||
|
|
properties: expect.arrayContaining([
|
||
|
|
expect.objectContaining({
|
||
|
|
name: "value",
|
||
|
|
value: { status: "literal", value: 1 },
|
||
|
|
}),
|
||
|
|
]),
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it("keeps nested members unknown after a write through a nested setter", () => {
|
||
|
|
const value = returned(`
|
||
|
|
export function observe() {
|
||
|
|
const state = {
|
||
|
|
label: "kept",
|
||
|
|
nested: { value: 1, set update(next) { this.value = next; } },
|
||
|
|
};
|
||
|
|
state.nested.update = 2;
|
||
|
|
return state;
|
||
|
|
}
|
||
|
|
`);
|
||
|
|
expect(value).toMatchObject({
|
||
|
|
status: "object",
|
||
|
|
properties: expect.arrayContaining([
|
||
|
|
expect.objectContaining({
|
||
|
|
name: "label",
|
||
|
|
value: { status: "literal", value: "kept" },
|
||
|
|
}),
|
||
|
|
expect.objectContaining({
|
||
|
|
name: "nested",
|
||
|
|
value: expect.objectContaining({ status: "unknown" }),
|
||
|
|
}),
|
||
|
|
]),
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it("keeps sibling literals when an accessor is deleted rather than assigned", () => {
|
||
|
|
expect(
|
||
|
|
returned(`
|
||
|
|
export function observe() {
|
||
|
|
const state = { value: 1, set update(next) {} };
|
||
|
|
delete state.update;
|
||
|
|
return state;
|
||
|
|
}
|
||
|
|
`),
|
||
|
|
).toMatchObject({
|
||
|
|
status: "object",
|
||
|
|
properties: expect.arrayContaining([
|
||
|
|
expect.objectContaining({
|
||
|
|
name: "value",
|
||
|
|
value: { status: "literal", value: 1 },
|
||
|
|
}),
|
||
|
|
]),
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it("keeps sibling literals when a later data property replaces the setter", () => {
|
||
|
|
expect(
|
||
|
|
returned(`
|
||
|
|
export function observe() {
|
||
|
|
const state = { value: 1, set update(next) {}, update: 0 };
|
||
|
|
state.update = 2;
|
||
|
|
return state;
|
||
|
|
}
|
||
|
|
`),
|
||
|
|
).toMatchObject({
|
||
|
|
status: "object",
|
||
|
|
properties: expect.arrayContaining([
|
||
|
|
expect.objectContaining({
|
||
|
|
name: "value",
|
||
|
|
value: { status: "literal", value: 1 },
|
||
|
|
}),
|
||
|
|
]),
|
||
|
|
});
|
||
|
|
});
|