Skip to content

Commit dce9aa0

Browse files
committed
Fix readonly property assign with ArrayAccess offset
1 parent 2d24ffc commit dce9aa0

8 files changed

+136
-0
lines changed

src/Rules/Properties/ReadOnlyByPhpDocPropertyAssignRule.php

+12
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,19 @@
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;
9+
use PHPStan\Node\Expr\UnsetOffsetExpr;
710
use PHPStan\Node\PropertyAssignNode;
811
use PHPStan\Reflection\ConstructorsHelper;
912
use PHPStan\Reflection\MethodReflection;
1013
use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection;
1114
use PHPStan\Rules\Rule;
1215
use PHPStan\Rules\RuleErrorBuilder;
1316
use PHPStan\ShouldNotHappenException;
17+
use PHPStan\Type\ObjectType;
1418
use PHPStan\Type\TypeUtils;
1519
use function in_array;
1620
use function sprintf;
@@ -106,6 +110,14 @@ public function processNode(Node $node, Scope $scope): array
106110
continue;
107111
}
108112

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

src/Rules/Properties/ReadOnlyPropertyAssignRule.php

+12
Original file line numberDiff line numberDiff line change
@@ -2,14 +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;
9+
use PHPStan\Node\Expr\UnsetOffsetExpr;
710
use PHPStan\Node\PropertyAssignNode;
811
use PHPStan\Reflection\ConstructorsHelper;
912
use PHPStan\Reflection\MethodReflection;
1013
use PHPStan\Rules\Rule;
1114
use PHPStan\Rules\RuleErrorBuilder;
1215
use PHPStan\ShouldNotHappenException;
16+
use PHPStan\Type\ObjectType;
1317
use PHPStan\Type\TypeUtils;
1418
use function in_array;
1519
use function sprintf;
@@ -89,6 +93,14 @@ public function processNode(Node $node, Scope $scope): array
8993
continue;
9094
}
9195

96+
$assignedExpr = $node->getAssignedExpr();
97+
if (
98+
($assignedExpr instanceof SetOffsetValueTypeExpr || $assignedExpr instanceof UnsetOffsetExpr)
99+
&& (new ObjectType(ArrayAccess::class))->isSuperTypeOf($scope->getType($assignedExpr->getVar()))->yes()
100+
) {
101+
continue;
102+
}
103+
92104
$errors[] = RuleErrorBuilder::message(sprintf('Readonly property %s::$%s is assigned outside of the constructor.', $declaringClass->getDisplayName(), $propertyReflection->getName()))
93105
->identifier('property.readOnlyAssignNotInConstructor')
94106
->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+
311,
131+
],
128132
]);
129133
}
130134

tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php

+23
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+
212,
129+
];
130+
126131
$this->analyse([__DIR__ . '/data/readonly-assign.php'], $errors);
127132
}
128133

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

176+
public function testBug8929(): void
177+
{
178+
if (PHP_VERSION_ID < 80100) {
179+
$this->markTestSkipped('Test requires PHP 8.1.');
180+
}
181+
182+
$this->analyse([__DIR__ . '/data/bug-8929.php'], []);
183+
}
184+
185+
public function testBug12537(): void
186+
{
187+
if (PHP_VERSION_ID < 80100) {
188+
$this->markTestSkipped('Test requires PHP 8.1.');
189+
}
190+
191+
$this->analyse([__DIR__ . '/data/bug-12537.php'], []);
192+
}
193+
171194
}
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));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php declare(strict_types = 1); // lint >= 8.1
2+
3+
namespace Bug8929;
4+
5+
class Test
6+
{
7+
/** @var \WeakMap<object, mixed> */
8+
protected readonly \WeakMap $cache;
9+
10+
public function __construct()
11+
{
12+
$this->cache = new \WeakMap();
13+
}
14+
15+
public function add(object $key, mixed $value): void
16+
{
17+
$this->cache[$key] = $value;
18+
}
19+
}

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

+18
Original file line numberDiff line numberDiff line change
@@ -294,3 +294,21 @@ 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+
unset($this->storage[$class]);
311+
$this->storage = new \WeakMap(); // invalid
312+
}
313+
314+
}

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

+17
Original file line numberDiff line numberDiff line change
@@ -196,3 +196,20 @@ 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+
unset($this->storage[$class]);
212+
$this->storage = new \WeakMap(); // invalid
213+
}
214+
215+
}

0 commit comments

Comments
 (0)