diff --git a/src/application/JavaScriptSemanticGraphResources.test.ts b/src/application/JavaScriptSemanticGraphResources.test.ts index 3f88e0721a..65d3c68a6b 100644 --- a/src/application/JavaScriptSemanticGraphResources.test.ts +++ b/src/application/JavaScriptSemanticGraphResources.test.ts @@ -170,6 +170,18 @@ it("projects built-in resource acquisition and release", () => { ).toMatchObject({ status: "partial" }); }); +it.each(["+= 1", "++"])( + "projects both property dependencies for %s", + (operator) => { + const graph = graphFor(` + const source = { count: 1 }; + source.count ${operator}; + `); + const relations = graph.relations.map(({ relation }) => relation); + expect(relations).toContain("reads-property"); + expect(relations).toContain("writes-property"); + }, +); it("does not project a literal property slot for dynamic destructuring", () => { const graph = graphFor(` const source = { token: "TOKEN" }; diff --git a/src/domain/javascriptSemanticAnalysis.dataflow.test.ts b/src/domain/javascriptSemanticAnalysis.dataflow.test.ts index cd44801f77..394f271e30 100644 --- a/src/domain/javascriptSemanticAnalysis.dataflow.test.ts +++ b/src/domain/javascriptSemanticAnalysis.dataflow.test.ts @@ -149,6 +149,34 @@ describe("JavaScript semantic analysis: dataflow 1", () => { }); }); +describe("JavaScript semantic analysis: read-modify-write", () => { + it.each(["+= 1", "++", "--", "||= 1", "&&= 1", "??= 1"])( + "retains the property read in a read-modify-write operation: %s", + (operator) => { + const ir = analyzeJavaScriptSemantics(` + const source = { count: 1 }; + source.count ${operator}; + `); + expect(ir.objectOperations.map(({ kind }) => kind)).toEqual([ + "read", + "write", + ]); + }, + ); + + it("keeps plain member assignment write-only and member access read-only", () => { + const ir = analyzeJavaScriptSemantics(` + const source = { count: 1 }; + source.count = 2; + const value = source.count; + `); + expect(ir.objectOperations.map(({ kind }) => kind)).toEqual([ + "write", + "read", + ]); + }); +}); + describe("JavaScript semantic analysis: dataflow 2", () => { it("retains partial property coverage and empty returns", () => { const partial = analyzeJavaScriptSemantics(` diff --git a/src/domain/javascriptSemanticObjects.ts b/src/domain/javascriptSemanticObjects.ts index f7f8fa18df..add50ea854 100644 --- a/src/domain/javascriptSemanticObjects.ts +++ b/src/domain/javascriptSemanticObjects.ts @@ -79,6 +79,22 @@ const collectMember = ( const write = (t.isAssignmentExpression(parent) && parent.left === node) || (t.isUpdateExpression(parent) && parent.argument === node); + const readsBeforeWrite = + (t.isAssignmentExpression(parent) && parent.operator !== "=" && write) || + (t.isUpdateExpression(parent) && write); + if (readsBeforeWrite) + addObjectOperation( + { + node, + kind: "read", + ownerCallableId, + objectBindingId: expressionBindingId(node.object, state), + targetBindingId: null, + propertyName: name, + }, + state, + output, + ); addObjectOperation( { node,