diff --git a/src/Rules/Properties/ReadOnlyPropertyAssignRule.php b/src/Rules/Properties/ReadOnlyPropertyAssignRule.php index 4e9673070f..a623d48536 100644 --- a/src/Rules/Properties/ReadOnlyPropertyAssignRule.php +++ b/src/Rules/Properties/ReadOnlyPropertyAssignRule.php @@ -2,14 +2,18 @@ namespace PHPStan\Rules\Properties; +use ArrayAccess; use PhpParser\Node; use PHPStan\Analyser\Scope; +use PHPStan\Node\Expr\SetOffsetValueTypeExpr; +use PHPStan\Node\Expr\UnsetOffsetExpr; use PHPStan\Node\PropertyAssignNode; use PHPStan\Reflection\ConstructorsHelper; use PHPStan\Reflection\MethodReflection; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\ShouldNotHappenException; +use PHPStan\Type\ObjectType; use PHPStan\Type\TypeUtils; use function in_array; use function sprintf; @@ -89,6 +93,14 @@ public function processNode(Node $node, Scope $scope): array continue; } + $expr = $node->getAssignedExpr(); + if ( + (new ObjectType(ArrayAccess::class))->isSuperTypeOf($propertyReflection->getNativeType())->yes() + && (($expr instanceof SetOffsetValueTypeExpr) || ($expr instanceof UnsetOffsetExpr)) + ) { + continue; + } + $errors[] = RuleErrorBuilder::message(sprintf('Readonly property %s::$%s is assigned outside of the constructor.', $declaringClass->getDisplayName(), $propertyReflection->getName())) ->identifier('property.readOnlyAssignNotInConstructor') ->build(); diff --git a/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php b/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php index 966f8e9e41..a37df3bb13 100644 --- a/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php +++ b/tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php @@ -168,4 +168,18 @@ public function testBug6773(): void ]); } + public function testBug8929(): void + { + if (PHP_VERSION_ID < 80100) { + self::markTestSkipped('Test requires PHP 8.1'); + } + + $this->analyse([__DIR__ . '/data/bug-8929.php'], [ + [ + 'Readonly property Bug8929\Test::$cache is assigned outside of the constructor.', + 19, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Properties/data/bug-8929.php b/tests/PHPStan/Rules/Properties/data/bug-8929.php new file mode 100644 index 0000000000..006f3aecf5 --- /dev/null +++ b/tests/PHPStan/Rules/Properties/data/bug-8929.php @@ -0,0 +1,21 @@ += 8.1 + +namespace Bug8929; + +class Test +{ + /** @var \WeakMap */ + protected readonly \WeakMap $cache; + + public function __construct() + { + $this->cache = new \WeakMap(); + } + + public function add(object $key, mixed $value): void + { + $this->cache[$key] = $value; // valid offset access + unset($this->cache[$key]); // valid offset access + $this->cache = new \WeakMap(); // reassigning is invalid however + } +}