mirror of
https://github.com/morluto/rea
synced 2026-10-27 22:45:19 -04:00
fix(javascript): keep members unknown after a write through a local setter (#3831)
* 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>
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
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 },
|
||||
}),
|
||||
]),
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user