Skip to content

Commit 770d90c

Browse files
committed
Fix readonly property assign with ArrayAccess offset
1 parent e140197 commit 770d90c

7 files changed

+104
-0
lines changed

src/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRule.php

+8
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,18 @@
22

33
namespace PHPStan\Rules\Properties;
44

5+
use ArrayAccess;
56
use PhpParser\Node;
67
use PHPStan\Analyser\Scope;
8+
use PHPStan\Node\Expr\SetOffsetValueTypeExpr;
79
use PHPStan\Node\PropertyAssignNode;
810
use PHPStan\Reflection\ConstructorsHelper;
911
use PHPStan\Reflection\MethodReflection;
1012
use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection;
1113
use PHPStan\Rules\Rule;
1214
use PHPStan\Rules\RuleErrorBuilder;
1315
use PHPStan\ShouldNotHappenException;
16+
use PHPStan\Type\ObjectType;
1417
use PHPStan\Type\TypeUtils;
1518
use function in_array;
1619
use function sprintf;
@@ -106,6 +109,11 @@ public function processNode(Node $node, Scope $scope): array
106109
continue;
107110
}
108111

112+
$assignedExpr = $node->getAssignedExpr();
113+
if ($assignedExpr instanceof SetOffsetValueTypeExpr && (new ObjectType(ArrayAccess::class))->isSuperTypeOf($scope->getType($assignedExpr->getVar()))->yes()) {
114+
continue;
115+
}
116+
109117
$errors[] = RuleErrorBuilder::message(sprintf('@readonly property %s::$%s is assigned outside of the constructor.', $declaringClass->getDisplayName(), $propertyReflection->getName()))
110118
->identifier('property.readOnlyByPhpDocAssignNotInConstructor')
111119
->build();

src/Rules/Properties/ReadOnlyPropertyAssignRule.php

+8
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@
22

33
namespace PHPStan\Rules\Properties;
44

5+
use ArrayAccess;
56
use PhpParser\Node;
67
use PHPStan\Analyser\Scope;
8+
use PHPStan\Node\Expr\SetOffsetValueTypeExpr;
79
use PHPStan\Node\PropertyAssignNode;
810
use PHPStan\Reflection\ConstructorsHelper;
911
use PHPStan\Reflection\MethodReflection;
1012
use PHPStan\Rules\Rule;
1113
use PHPStan\Rules\RuleErrorBuilder;
1214
use PHPStan\ShouldNotHappenException;
15+
use PHPStan\Type\ObjectType;
1316
use PHPStan\Type\TypeUtils;
1417
use function in_array;
1518
use function sprintf;
@@ -89,6 +92,11 @@ public function processNode(Node $node, Scope $scope): array
8992
continue;
9093
}
9194

95+
$assignedExpr = $node->getAssignedExpr();
96+
if ($assignedExpr instanceof SetOffsetValueTypeExpr && (new ObjectType(ArrayAccess::class))->isSuperTypeOf($scope->getType($assignedExpr->getVar()))->yes()) {
97+
continue;
98+
}
99+
92100
$errors[] = RuleErrorBuilder::message(sprintf('Readonly property %s::$%s is assigned outside of the constructor.', $declaringClass->getDisplayName(), $propertyReflection->getName()))
93101
->identifier('property.readOnlyAssignNotInConstructor')
94102
->build();

tests/PHPStan/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRuleTest.php

+4
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,10 @@ public function testRule(): void
125125
'@readonly property ReadonlyPropertyAssignPhpDoc\C::$c is assigned outside of the constructor.',
126126
293,
127127
],
128+
[
129+
'@readonly property ReadonlyPropertyAssignPhpDoc\ArrayAccessPropertyFetch::$storage is assigned outside of the constructor.',
130+
313,
131+
],
128132
]);
129133
}
130134

tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php

+14
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,11 @@ public function testRule(): void
123123
];
124124
}
125125

126+
$errors[] = [
127+
'Readonly property ReadonlyPropertyAssign\ArrayAccessPropertyFetch::$storage is assigned outside of the constructor.',
128+
214,
129+
];
130+
126131
$this->analyse([__DIR__ . '/data/readonly-assign.php'], $errors);
127132
}
128133

@@ -168,4 +173,13 @@ public function testBug6773(): void
168173
]);
169174
}
170175

176+
public function testBug12537(): void
177+
{
178+
if (PHP_VERSION_ID < 80100) {
179+
$this->markTestSkipped('Test requires PHP 8.1.');
180+
}
181+
182+
$this->analyse([__DIR__ . '/data/bug-12537.php'], []);
183+
}
184+
171185
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php declare(strict_types = 1); // lint >= 8.1
2+
3+
namespace Bug12537;
4+
5+
use WeakMap;
6+
7+
class Metadata {
8+
/**
9+
* @var WeakMap<stdClass, int>
10+
*/
11+
private readonly WeakMap $storage;
12+
13+
public function __construct() {
14+
$this->storage = new WeakMap();
15+
}
16+
17+
public function set(stdClass $class, int $value): void {
18+
$this->storage[$class] = $value;
19+
}
20+
21+
public function get(stdClass $class): mixed {
22+
return $this->storage[$class] ?? null;
23+
}
24+
}
25+
26+
$class = new stdClass();
27+
$meta = new Metadata();
28+
29+
$meta->set($class, 123);
30+
31+
var_dump($meta->get($class));

tests/PHPStan/Rules/Properties/data/readonly-assign-phpdoc.php

+20
Original file line numberDiff line numberDiff line change
@@ -294,3 +294,23 @@ public function mod()
294294
}
295295

296296
}
297+
298+
class ArrayAccessPropertyFetch
299+
{
300+
301+
/** @readonly */
302+
private \ArrayObject $storage;
303+
304+
public function __construct() {
305+
$this->storage = new \ArrayObject();
306+
}
307+
308+
public function set(\stdClass $class, int $value): void {
309+
$this->storage[$class] = $value;
310+
}
311+
312+
public function invalidAssign() {
313+
$this->storage = new \WeakMap();
314+
}
315+
316+
}

tests/PHPStan/Rules/Properties/data/readonly-assign.php

+19
Original file line numberDiff line numberDiff line change
@@ -196,3 +196,22 @@ protected function setUp(): void
196196
}
197197

198198
}
199+
200+
class ArrayAccessPropertyFetch
201+
{
202+
203+
private readonly \ArrayObject $storage;
204+
205+
public function __construct() {
206+
$this->storage = new \ArrayObject();
207+
}
208+
209+
public function set(\stdClass $class, int $value): void {
210+
$this->storage[$class] = $value;
211+
}
212+
213+
public function invalidAssign() {
214+
$this->storage = new \WeakMap();
215+
}
216+
217+
}

0 commit comments

Comments
 (0)