mirror of
https://github.com/morluto/rea
synced 2026-10-27 14:42:18 -04:00
acca91d22b
* fix(javascript): keep members unknown after a write through a local setter Assigning to a key that a setter of the binding's own object literal defines runs that setter, which may replace any member of its receiver. The member-mutation collector recorded it as an ordinary write to that one key, so sibling literals such as value: 1 stayed known and export-shape comparisons reported complete, unchanged shapes. Record such a write with an unknown key, which invalidates the object's known members. A computed setter key may match any written key. Fixes #3312 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(javascript): follow nested setters, skip deletes and cache setter keys Resolve the written receiver along nested object-literal members so a write through a nested setter widens that receiver's members. A delete never calls a setter, so it keeps its exact path. Setter keys are computed once per object literal, including the no-setter result. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(javascript): ignore setters replaced by a later data property Build cached setter keys from the effective definitions: a later data property or method with the same static name replaces the accessor, so assigning that key no longer runs a setter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
109 lines
2.8 KiB
TypeScript
109 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 },
|
|
}),
|
|
]),
|
|
});
|
|
});
|