diff --git a/graphify/extractors/engine.py b/graphify/extractors/engine.py index 6e54b41ad..adc58856b 100644 --- a/graphify/extractors/engine.py +++ b/graphify/extractors/engine.py @@ -1829,13 +1829,14 @@ def _same_node(a, b) -> bool: def _python_attribute_is_store(node) -> bool: - """`x.a = ...`, `x.a += ...`, `for x.a in ...`, `del x.a`: not a read of `a`.""" + """`x.a = ...`, `for x.a in ...`, `del x.a`: not a read of `a`. `x.a += ...` + is: it reads `a` (the getter runs) before it writes it.""" child, parent = node, node.parent while parent is not None and parent.type in ("pattern_list", "tuple_pattern", "list_pattern"): child, parent = parent, parent.parent if parent is None: return False - if parent.type in ("assignment", "augmented_assignment", "for_statement", "for_in_clause"): + if parent.type in ("assignment", "for_statement", "for_in_clause"): return _same_node(parent.child_by_field_name("left"), child) return parent.type == "delete_statement" diff --git a/tests/test_python_property_reads.py b/tests/test_python_property_reads.py index e176cc7ed..14458c093 100644 --- a/tests/test_python_property_reads.py +++ b/tests/test_python_property_reads.py @@ -80,6 +80,15 @@ def test_fields_method_references_and_stores_are_not_reads(tmp_path: Path): assert not any(e[0] == "poke()" and e[3] == "property_read" for e in edges), edges +def test_augmented_assignment_reads_the_property(tmp_path: Path): + # `r.text += x` runs the getter before the setter. + edges = _edges(tmp_path, { + "resp.py": _RESPONSE, + "use.py": "from resp import Response\n\n\ndef extend():\n r = Response()\n r.text += 'x'\n", + }) + assert ("extend()", "Response", ".text()", "property_read") in edges + + def test_underscored_field_does_not_reach_the_property_without_underscore(tmp_path: Path): # `_items` and `items` fold to one key in the resolver; the read names `_items`. edges = _edges(tmp_path, {