From 579d38f602071c3da20f28a598a087ce70beab7f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 28 Feb 2026 14:50:56 +0000 Subject: [PATCH] Fix bogus write-only error when appending to ArrayAccess property - In ClassStatementsGatherer, also record a PropertyRead when a PropertyAssignNode is for an ArrayAccess property via array dim fetch (e.g. $this->array[] = $s), because this is semantically $this->array->offsetSet() which reads the property - New regression test in tests/PHPStan/Rules/DeadCode/data/bug-6777.php - Updated existing WriteToCollection test expectations Closes https://github.com/phpstan/phpstan/issues/6777 --- src/Node/ClassStatementsGatherer.php | 16 +++++++++++++++- .../UnusedPrivatePropertyRuleTest.php | 19 +++++++++---------- .../PHPStan/Rules/DeadCode/data/bug-6777.php | 15 +++++++++++++++ 3 files changed, 39 insertions(+), 11 deletions(-) create mode 100644 tests/PHPStan/Rules/DeadCode/data/bug-6777.php diff --git a/src/Node/ClassStatementsGatherer.php b/src/Node/ClassStatementsGatherer.php index a2bb6889d5..663d8c4e0d 100644 --- a/src/Node/ClassStatementsGatherer.php +++ b/src/Node/ClassStatementsGatherer.php @@ -13,11 +13,14 @@ use PhpParser\Node\Identifier; use PHPStan\Analyser\Scope; use PHPStan\Node\Constant\ClassConstantFetch; +use PHPStan\Node\Expr\SetExistingOffsetValueTypeExpr; +use PHPStan\Node\Expr\SetOffsetValueTypeExpr; use PHPStan\Node\Property\PropertyAssign; use PHPStan\Node\Property\PropertyRead; use PHPStan\Node\Property\PropertyWrite; use PHPStan\Reflection\ClassReflection; use PHPStan\ShouldNotHappenException; +use PHPStan\Type\ObjectType; use PHPStan\Type\TypeUtils; use ReflectionProperty; use function count; @@ -200,7 +203,18 @@ private function gatherNodes(Node $node, Scope $scope): void return; } if ($node instanceof PropertyAssignNode) { - $this->propertyUsages[] = new PropertyWrite($node->getPropertyFetch(), $scope, false); + $propertyFetch = $node->getPropertyFetch(); + $assignedExpr = $node->getAssignedExpr(); + if ($assignedExpr instanceof SetOffsetValueTypeExpr || $assignedExpr instanceof SetExistingOffsetValueTypeExpr) { + $propertyType = $scope->getType($propertyFetch); + if ( + !$propertyType->isArray()->yes() + && (new ObjectType(\ArrayAccess::class))->isSuperTypeOf($propertyType)->yes() + ) { + $this->propertyUsages[] = new PropertyRead($propertyFetch, $scope); + } + } + $this->propertyUsages[] = new PropertyWrite($propertyFetch, $scope, false); $this->propertyAssigns[] = new PropertyAssign($node, $scope); return; } diff --git a/tests/PHPStan/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php index 199e22d1a2..6cc5e5e230 100644 --- a/tests/PHPStan/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php @@ -130,16 +130,6 @@ public function testRule(): void 191, $tip, ], - [ - 'Property UnusedPrivateProperty\WriteToCollection::$collection1 is never read, only written.', - 221, - $tip, - ], - [ - 'Property UnusedPrivateProperty\WriteToCollection::$collection2 is never read, only written.', - 224, - $tip, - ], ]); $this->analyse([__DIR__ . '/data/TestExtension.php'], [ [ @@ -410,4 +400,13 @@ public function testBug9213(): void $this->analyse([__DIR__ . '/data/bug-9213.php'], []); } + #[RequiresPhp('>= 8.0')] + public function testBug6777(): void + { + $this->alwaysWrittenTags = []; + $this->alwaysReadTags = []; + + $this->analyse([__DIR__ . '/data/bug-6777.php'], []); + } + } diff --git a/tests/PHPStan/Rules/DeadCode/data/bug-6777.php b/tests/PHPStan/Rules/DeadCode/data/bug-6777.php new file mode 100644 index 0000000000..0e24d4eced --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/bug-6777.php @@ -0,0 +1,15 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug6777; + +class HelloWorld +{ + /** @param \ArrayObject $array */ + public function __construct(private \ArrayObject $array){} + + public function send(string $s) : void{ + $this->array[] = $s; + } +}