fix(python): count x.prop += v as a read of the property

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B8gwEpjajiWHVW3c9smuKf
This commit is contained in:
rohit-jsfreakyandClaude Opus 5.5 authored and safishamsi committed 2026-10-12 13:02:22 +01:00
1 parent d244be4c97
commit 1723546eec
2 files changed
+12 -2

No files matched your search

+3 -2
View File
@@ -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"
+9
View File
@@ -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, {