Files
rea/src/domain/javascript/javascriptSemanticAnalysis.accessor_writes.test.ts
T
Kaoru0822 acca91d22b 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>
2026-10-14 21:39:39 -07:00

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 },
}),
]),
});
});