diff --git a/src/domain/javascript/javascriptSemanticAnalysis.member_mutations.test.ts b/src/domain/javascript/javascriptSemanticAnalysis.member_mutations.test.ts index 24bc4116f8..3ad7105889 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.member_mutations.test.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.member_mutations.test.ts @@ -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", () => { diff --git a/src/domain/javascript/javascriptSemanticMemberMutations.ts b/src/domain/javascript/javascriptSemanticMemberMutations.ts index 5471ee3ab5..245184e676 100644 --- a/src/domain/javascript/javascriptSemanticMemberMutations.ts +++ b/src/domain/javascript/javascriptSemanticMemberMutations.ts @@ -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, diff --git a/tests/acceptance/applications/javascriptReturnedReferences.test.ts b/tests/acceptance/applications/javascriptReturnedReferences.test.ts index 82b4716387..e5d00fcf40 100644 --- a/tests/acceptance/applications/javascriptReturnedReferences.test.ts +++ b/tests/acceptance/applications/javascriptReturnedReferences.test.ts @@ -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",