mirror of
https://github.com/morluto/rea.git
synced 2026-10-11 21:39:55 +00:00
fix(javascript): preserve getter results on direct property writes
Replacing or deleting a class getter property does not mutate the object it would return. Apply the existing object-accessor write boundary to instance and static getters. Validation: source mutation regressions and compiled CLI/stdio MCP acceptance preserve literal 1 for all four assignment/deletion cases; check:fast passes.
This commit is contained in:
@@ -304,6 +304,24 @@ describe("JavaScript semantic mutations through getter results (#1494)", () => {
|
||||
),
|
||||
).toEqual({ status: "literal", value: 1 });
|
||||
});
|
||||
|
||||
it("keeps a shared value known after an optional getter read or ordinary method read", () => {
|
||||
expect(
|
||||
resultValue("const shared = { value: 1 }; const box = { get value() { return shared; } }; const alias = box?.value; return shared.value;"),
|
||||
).toEqual({ status: "literal", value: 1 });
|
||||
expect(
|
||||
resultValue("const shared = { value: 1 }; const box = { value() { return shared; } }; const alias = box.value; return shared.value;"),
|
||||
).toEqual({ status: "literal", value: 1 });
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
["instance getter assignment", "const shared = { value: 1 }; class Box { get value() { return shared; } } const box = new Box(); box.value = {}; return shared.value;"],
|
||||
["instance getter deletion", "const shared = { value: 1 }; class Box { get value() { return shared; } } const box = new Box(); delete box.value; return shared.value;"],
|
||||
["static getter assignment", "const shared = { value: 1 }; class Box { static get value() { return shared; } } Box.value = {}; return shared.value;"],
|
||||
["static getter deletion", "const shared = { value: 1 }; class Box { static get value() { return shared; } } delete Box.value; return shared.value;"],
|
||||
])("does not follow a class getter for direct property replacement or deletion (%s)", (_name, body) => {
|
||||
expect(resultValue(body)).toEqual({ status: "literal", value: 1 });
|
||||
});
|
||||
|
||||
describe("JavaScript semantic values after mutable references escape", () => {
|
||||
|
||||
@@ -635,6 +635,9 @@ const referencedValues = (
|
||||
if (t.isClass(node)) {
|
||||
const [key, ...remaining] = path;
|
||||
if (key === undefined) return [];
|
||||
// Replacing or deleting the getter's property does not use the value it
|
||||
// would return. Only a deeper path can mutate the returned object.
|
||||
if (effect === "write" && remaining.length === 0) return [];
|
||||
return classGetterResults(
|
||||
node,
|
||||
key === null ? null : String(key),
|
||||
@@ -646,6 +649,7 @@ const referencedValues = (
|
||||
if (t.isNewExpression(node)) {
|
||||
const [key, ...remaining] = path;
|
||||
if (key === undefined) return [];
|
||||
if (effect === "write" && remaining.length === 0) return [];
|
||||
return classCandidates(node.callee, state).flatMap((klass) =>
|
||||
classGetterResults(
|
||||
klass,
|
||||
|
||||
@@ -77,6 +77,10 @@ const source = [
|
||||
"export function computedClassGetterOverride(){ const shared = {x: 1}; const key = getKey(); class Box { get value(){return {x: 2};} get [key](){return shared;} } new Box().value.x = 3; return shared.x; }",
|
||||
"export function aliasedClassGetter(){ const shared = {x: 1}; class Box { get value(){return shared;} } const Alias = Box; new Alias().value.x = 2; return shared.x; }",
|
||||
"export function inlineClassGetter(){ const shared = {x: 1}; new (class { get value(){return shared;} })().value.x = 2; return shared.x; }",
|
||||
"export function instanceGetterAssignment(){ const shared = {x: 1}; class Box { get value(){return shared;} } const box = new Box(); try { box.value = {}; } catch {} return shared.x; }",
|
||||
"export function instanceGetterDeletion(){ const shared = {x: 1}; class Box { get value(){return shared;} } const box = new Box(); delete box.value; return shared.x; }",
|
||||
"export function staticGetterAssignment(){ const shared = {x: 1}; class Box { static get value(){return shared;} } try { Box.value = {}; } catch {} return shared.x; }",
|
||||
"export function staticGetterDeletion(){ const shared = {x: 1}; class Box { static get value(){return shared;} } delete Box.value; return shared.x; }",
|
||||
"export function freshGetterRead(){ const box = {get value(){return {x: 1};}}; const first = box.value; first.x = 2; return box.value.x; }",
|
||||
"export function savedFreshGetter(){ const box = {get value(){return {x: 1};}}; const saved = box.value; saved.x = 2; return saved.x; }",
|
||||
"export function directSibling(){ const shared = {x: 1}; const parent = {shared, keep: 7}; const get = () => parent.shared; get().x = 2; return {x: shared.x, keep: parent.keep}; }",
|
||||
@@ -114,6 +118,15 @@ const assertReturns = (fields: JavaScriptReturnFields): void => {
|
||||
expect(fields(name)).toContainEqual(
|
||||
expect.objectContaining({ path: "", state: "literal", value: 1 }),
|
||||
);
|
||||
for (const name of [
|
||||
"instanceGetterAssignment",
|
||||
"instanceGetterDeletion",
|
||||
"staticGetterAssignment",
|
||||
"staticGetterDeletion",
|
||||
])
|
||||
expect(fields(name), name).toContainEqual(
|
||||
expect.objectContaining({ path: "", state: "literal", value: 1 }),
|
||||
);
|
||||
for (const name of [
|
||||
"inheritedGetter",
|
||||
"aliasedClassGetter",
|
||||
|
||||
Reference in New Issue
Block a user