Skip to content

Commit 5acd335

Browse files
committed
Don't report already assigned errors when setting or unsetting an offset on array access objects set as class properties.
1 parent 0d793ec commit 5acd335

File tree

5 files changed

+69
-3
lines changed

5 files changed

+69
-3
lines changed

src/Node/ClassPropertiesNode.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace PHPStan\Node;
44

5+
use ArrayAccess;
56
use Override;
67
use PhpParser\Node;
78
use PhpParser\Node\Expr\Array_;
@@ -13,7 +14,10 @@
1314
use PhpParser\NodeAbstract;
1415
use PHPStan\Analyser\Scope;
1516
use PHPStan\Node\Expr\PropertyInitializationExpr;
17+
use PHPStan\Node\Expr\SetOffsetValueTypeExpr;
18+
use PHPStan\Node\Expr\UnsetOffsetExpr;
1619
use PHPStan\Node\Method\MethodCall;
20+
use PHPSTan\Node\PropertyAssignNode;
1721
use PHPStan\Node\Property\PropertyAssign;
1822
use PHPStan\Node\Property\PropertyRead;
1923
use PHPStan\Node\Property\PropertyWrite;
@@ -22,6 +26,7 @@
2226
use PHPStan\Rules\Properties\ReadWritePropertiesExtensionProvider;
2327
use PHPStan\TrinaryLogic;
2428
use PHPStan\Type\NeverType;
29+
use PHPStan\Type\ObjectType;
2530
use PHPStan\Type\TypeUtils;
2631
use function array_diff_key;
2732
use function array_key_exists;
@@ -211,6 +216,19 @@ public function getUninitializedProperties(
211216

212217
if ($usage instanceof PropertyWrite) {
213218
if (array_key_exists($propertyName, $initializedPropertiesMap)) {
219+
$originalNode = $usage->getOriginalNode();
220+
221+
if ($originalNode instanceof PropertyAssignNode) {
222+
$assignedExpr = $originalNode->getAssignedExpr();
223+
224+
if (
225+
($assignedExpr instanceof SetOffsetValueTypeExpr || $assignedExpr instanceof UnsetOffsetExpr)
226+
&& (new ObjectType(ArrayAccess::class))->isSuperTypeOf($scope->getType($assignedExpr->getVar()))->yes()
227+
) {
228+
continue;
229+
}
230+
}
231+
214232
$hasInitialization = $initializedPropertiesMap[$propertyName]->or($usageScope->hasExpressionType(new PropertyInitializationExpr($propertyName)));
215233
if (
216234
!$hasInitialization->no()

src/Node/ClassStatementsGatherer.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ private function gatherNodes(Node $node, Scope $scope): void
150150
new PropertyFetch(new Expr\Variable('this'), new Identifier($node->getName())),
151151
$scope,
152152
true,
153+
$node,
153154
);
154155
}
155156
return;
@@ -194,7 +195,7 @@ private function gatherNodes(Node $node, Scope $scope): void
194195
return;
195196
}
196197
if ($node instanceof PropertyAssignNode) {
197-
$this->propertyUsages[] = new PropertyWrite($node->getPropertyFetch(), $scope, false);
198+
$this->propertyUsages[] = new PropertyWrite($node->getPropertyFetch(), $scope, false, $node);
198199
$this->propertyAssigns[] = new PropertyAssign($node, $scope);
199200
return;
200201
}
@@ -212,7 +213,7 @@ private function gatherNodes(Node $node, Scope $scope): void
212213
}
213214

214215
$this->propertyUsages[] = new PropertyRead($node->expr, $scope);
215-
$this->propertyUsages[] = new PropertyWrite($node->expr, $scope, false);
216+
$this->propertyUsages[] = new PropertyWrite($node->expr, $scope, false, $node);
216217
return;
217218
}
218219
if ($node instanceof FunctionCallableNode) {

src/Node/Property/PropertyWrite.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,21 @@
22

33
namespace PHPStan\Node\Property;
44

5+
use PhpParser\Node;
6+
use PhpParser\Node\Expr\AssignRef;
57
use PhpParser\Node\Expr\PropertyFetch;
68
use PhpParser\Node\Expr\StaticPropertyFetch;
79
use PHPStan\Analyser\Scope;
10+
use PHPStan\Node\ClassPropertyNode;
11+
use PHPStan\Node\PropertyAssignNode;
812

913
/**
1014
* @api
1115
*/
1216
final class PropertyWrite
1317
{
1418

15-
public function __construct(private PropertyFetch|StaticPropertyFetch $fetch, private Scope $scope, private bool $promotedPropertyWrite)
19+
public function __construct(private PropertyFetch|StaticPropertyFetch $fetch, private Scope $scope, private bool $promotedPropertyWrite, private null|ClassPropertyNode|PropertyAssignNode|AssignRef $originalNode = null)
1620
{
1721
}
1822

@@ -34,4 +38,9 @@ public function isPromotedPropertyWrite(): bool
3438
return $this->promotedPropertyWrite;
3539
}
3640

41+
public function getOriginalNode(): null|ClassPropertyNode|PropertyAssignNode|AssignRef
42+
{
43+
return $this->originalNode;
44+
}
45+
3746
}

tests/PHPStan/Rules/Properties/MissingReadOnlyPropertyAssignRuleTest.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,15 @@ public static function getAdditionalConfigFiles(): array
4646
);
4747
}
4848

49+
#[RequiresPhp('>= 8.1')]
50+
public function testBug13856(): void
51+
{
52+
$this->analyse([__DIR__ . '/data/bug-13856.php'], [
53+
[
54+
'Readonly property Bug13856\foo2::$store is already assigned.',
55+
25
56+
],
57+
]);
58+
}
59+
4960
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
namespace Bug13856;
4+
5+
class foo
6+
{
7+
/** @var \SplObjectStorage<object,mixed> */
8+
private readonly \SplObjectStorage $store;
9+
10+
public function __construct()
11+
{
12+
$this->store = new \SplObjectStorage();
13+
$this->store[(object) ['foo' => 'bar']] = true;
14+
}
15+
}
16+
17+
class foo2
18+
{
19+
/** @var array<int, bool> */
20+
private readonly array $store;
21+
22+
public function __construct()
23+
{
24+
$this->store[1] = true;
25+
$this->store[2] = false;
26+
}
27+
}

0 commit comments

Comments
 (0)