Skip to content

Commit 1198766

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 1eead47 commit 1198766

File tree

5 files changed

+69
-3
lines changed

5 files changed

+69
-3
lines changed

src/Node/ClassPropertiesNode.php

Lines changed: 17 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,6 +14,8 @@
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;
1720
use PHPStan\Node\Property\PropertyAssign;
1821
use PHPStan\Node\Property\PropertyRead;
@@ -22,6 +25,7 @@
2225
use PHPStan\Rules\Properties\ReadWritePropertiesExtensionProvider;
2326
use PHPStan\TrinaryLogic;
2427
use PHPStan\Type\NeverType;
28+
use PHPStan\Type\ObjectType;
2529
use PHPStan\Type\TypeUtils;
2630
use function array_diff_key;
2731
use function array_key_exists;
@@ -211,6 +215,19 @@ public function getUninitializedProperties(
211215

212216
if ($usage instanceof PropertyWrite) {
213217
if (array_key_exists($propertyName, $initializedPropertiesMap)) {
218+
$originalNode = $usage->getOriginalNode();
219+
220+
if ($originalNode instanceof PropertyAssignNode) {
221+
$assignedExpr = $originalNode->getAssignedExpr();
222+
223+
if (
224+
($assignedExpr instanceof SetOffsetValueTypeExpr || $assignedExpr instanceof UnsetOffsetExpr)
225+
&& (new ObjectType(ArrayAccess::class))->isSuperTypeOf($scope->getType($assignedExpr->getVar()))->yes()
226+
) {
227+
continue;
228+
}
229+
}
230+
214231
$hasInitialization = $initializedPropertiesMap[$propertyName]->or($usageScope->hasExpressionType(new PropertyInitializationExpr($propertyName)));
215232
if (
216233
!$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: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,20 @@
22

33
namespace PHPStan\Node\Property;
44

5+
use PhpParser\Node\Expr\AssignRef;
56
use PhpParser\Node\Expr\PropertyFetch;
67
use PhpParser\Node\Expr\StaticPropertyFetch;
78
use PHPStan\Analyser\Scope;
9+
use PHPStan\Node\ClassPropertyNode;
10+
use PHPStan\Node\PropertyAssignNode;
811

912
/**
1013
* @api
1114
*/
1215
final class PropertyWrite
1316
{
1417

15-
public function __construct(private PropertyFetch|StaticPropertyFetch $fetch, private Scope $scope, private bool $promotedPropertyWrite)
18+
public function __construct(private PropertyFetch|StaticPropertyFetch $fetch, private Scope $scope, private bool $promotedPropertyWrite, private ClassPropertyNode|PropertyAssignNode|AssignRef|null $originalNode = null)
1619
{
1720
}
1821

@@ -34,4 +37,9 @@ public function isPromotedPropertyWrite(): bool
3437
return $this->promotedPropertyWrite;
3538
}
3639

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

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+
27,
56+
],
57+
]);
58+
}
59+
4960
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php // lint >= 8.1
2+
3+
namespace Bug13856;
4+
5+
use SplObjectStorage;
6+
7+
class foo
8+
{
9+
/** @var SplObjectStorage<object, mixed> */
10+
private readonly SplObjectStorage $store;
11+
12+
public function __construct()
13+
{
14+
$this->store = new SplObjectStorage();
15+
$this->store[(object) ['foo' => 'bar']] = true;
16+
}
17+
}
18+
19+
class foo2
20+
{
21+
/** @var array<int, bool> */
22+
private readonly array $store;
23+
24+
public function __construct()
25+
{
26+
$this->store[1] = true;
27+
$this->store[2] = false;
28+
}
29+
}

0 commit comments

Comments
 (0)