Skip to content

Commit 99d83ed

Browse files
phpstan-botgithub-actions[bot]claude
authored
Fix #6777: Bogus write-only error when appending to an array-like object in private class property (#5102)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d8f5be7 commit 99d83ed

File tree

3 files changed

+94
-11
lines changed

3 files changed

+94
-11
lines changed

src/Node/ClassStatementsGatherer.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
use PhpParser\Node\Identifier;
1414
use PHPStan\Analyser\Scope;
1515
use PHPStan\Node\Constant\ClassConstantFetch;
16+
use PHPStan\Node\Expr\SetExistingOffsetValueTypeExpr;
17+
use PHPStan\Node\Expr\SetOffsetValueTypeExpr;
1618
use PHPStan\Node\Property\PropertyAssign;
1719
use PHPStan\Node\Property\PropertyRead;
1820
use PHPStan\Node\Property\PropertyWrite;
@@ -200,7 +202,15 @@ private function gatherNodes(Node $node, Scope $scope): void
200202
return;
201203
}
202204
if ($node instanceof PropertyAssignNode) {
203-
$this->propertyUsages[] = new PropertyWrite($node->getPropertyFetch(), $scope, false);
205+
$propertyFetch = $node->getPropertyFetch();
206+
$assignedExpr = $node->getAssignedExpr();
207+
if ($assignedExpr instanceof SetOffsetValueTypeExpr || $assignedExpr instanceof SetExistingOffsetValueTypeExpr) {
208+
$propertyType = $scope->getType($propertyFetch);
209+
if (!$propertyType->isObject()->no()) {
210+
$this->propertyUsages[] = new PropertyRead($propertyFetch, $scope);
211+
}
212+
}
213+
$this->propertyUsages[] = new PropertyWrite($propertyFetch, $scope, false);
204214
$this->propertyAssigns[] = new PropertyAssign($node, $scope);
205215
return;
206216
}

tests/PHPStan/Rules/DeadCode/UnusedPrivatePropertyRuleTest.php

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -130,16 +130,6 @@ public function testRule(): void
130130
191,
131131
$tip,
132132
],
133-
[
134-
'Property UnusedPrivateProperty\WriteToCollection::$collection1 is never read, only written.',
135-
221,
136-
$tip,
137-
],
138-
[
139-
'Property UnusedPrivateProperty\WriteToCollection::$collection2 is never read, only written.',
140-
224,
141-
$tip,
142-
],
143133
]);
144134
$this->analyse([__DIR__ . '/data/TestExtension.php'], [
145135
[
@@ -410,4 +400,13 @@ public function testBug9213(): void
410400
$this->analyse([__DIR__ . '/data/bug-9213.php'], []);
411401
}
412402

403+
#[RequiresPhp('>= 8.0')]
404+
public function testBug6777(): void
405+
{
406+
$this->alwaysWrittenTags = [];
407+
$this->alwaysReadTags = [];
408+
409+
$this->analyse([__DIR__ . '/data/bug-6777.php'], []);
410+
}
411+
413412
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php // lint >= 8.0
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug6777;
6+
7+
class HelloWorld
8+
{
9+
/** @param \ArrayObject<int, string> $array */
10+
public function __construct(private \ArrayObject $array){}
11+
12+
public function send(string $s) : void{
13+
$this->array[] = $s;
14+
}
15+
}
16+
17+
class WithThreaded
18+
{
19+
private \Threaded $collection;
20+
21+
public function __construct()
22+
{
23+
$this->collection = new \Threaded();
24+
}
25+
26+
public function add(string $s): void
27+
{
28+
$this->collection[] = $s;
29+
}
30+
}
31+
32+
class WithUnionObjectArray
33+
{
34+
/** @var \ArrayObject<int, string>|array<int, string> */
35+
private \ArrayObject|array $collection;
36+
37+
/** @param \ArrayObject<int, string>|array<int, string> $collection */
38+
public function __construct(\ArrayObject|array $collection)
39+
{
40+
$this->collection = $collection;
41+
}
42+
43+
public function add(string $s): void
44+
{
45+
$this->collection[] = $s;
46+
}
47+
}
48+
49+
class WithAssignByReference
50+
{
51+
/** @param \ArrayObject<int, string> $array */
52+
public function __construct(private \ArrayObject $array){}
53+
54+
public function send(string &$s) : void{
55+
$this->array[] =& $s;
56+
}
57+
}
58+
59+
class WithUnionObjectString
60+
{
61+
/** @var \ArrayObject<int, string>|string */
62+
private \ArrayObject|string $collection;
63+
64+
/** @param \ArrayObject<int, string>|string $collection */
65+
public function __construct(\ArrayObject|string $collection)
66+
{
67+
$this->collection = $collection;
68+
}
69+
70+
public function add(string $s): void
71+
{
72+
$this->collection[] = $s;
73+
}
74+
}

0 commit comments

Comments
 (0)