Skip to content

Commit 3fc2d5b

Browse files
committed
Allow reinitialization of a readonly property in __clone since PHP8.3
Closes phpstan/phpstan#11495
1 parent f7fca4a commit 3fc2d5b

File tree

4 files changed

+54
-2
lines changed

4 files changed

+54
-2
lines changed

src/Php/PhpVersion.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,4 +327,9 @@ public function hasDateTimeExceptions(): bool
327327
return $this->versionId >= 80300;
328328
}
329329

330+
public function supportsReadonlyPropertyReinitializationOnClone(): bool
331+
{
332+
return $this->versionId >= 80300;
333+
}
334+
330335
}

src/Rules/Properties/ReadOnlyPropertyAssignRule.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use PhpParser\Node;
66
use PHPStan\Analyser\Scope;
77
use PHPStan\Node\PropertyAssignNode;
8+
use PHPStan\Php\PhpVersion;
89
use PHPStan\Reflection\ConstructorsHelper;
910
use PHPStan\Reflection\MethodReflection;
1011
use PHPStan\Rules\Rule;
@@ -24,6 +25,7 @@ class ReadOnlyPropertyAssignRule implements Rule
2425
public function __construct(
2526
private PropertyReflectionFinder $propertyReflectionFinder,
2627
private ConstructorsHelper $constructorsHelper,
28+
private PhpVersion $phpVersion,
2729
)
2830
{
2931
}
@@ -76,9 +78,11 @@ public function processNode(Node $node, Scope $scope): array
7678
throw new ShouldNotHappenException();
7779
}
7880

81+
$methodName = $scopeMethod->getName();
7982
if (
80-
in_array($scopeMethod->getName(), $this->constructorsHelper->getConstructors($scopeClassReflection), true)
81-
|| strtolower($scopeMethod->getName()) === '__unserialize'
83+
in_array($methodName, $this->constructorsHelper->getConstructors($scopeClassReflection), true)
84+
|| strtolower($methodName) === '__unserialize'
85+
|| ($this->phpVersion->supportsReadonlyPropertyReinitializationOnClone() && strtolower($methodName) === '__clone')
8286
) {
8387
if (TypeUtils::findThisType($scope->getType($propertyFetch->var)) === null) {
8488
$errors[] = RuleErrorBuilder::message(sprintf('Readonly property %s::$%s is not assigned on $this.', $declaringClass->getDisplayName(), $propertyReflection->getName()))

tests/PHPStan/Rules/Properties/ReadOnlyPropertyAssignRuleTest.php

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

33
namespace PHPStan\Rules\Properties;
44

5+
use PHPStan\Php\PhpVersion;
56
use PHPStan\Reflection\ConstructorsHelper;
67
use PHPStan\Rules\Rule;
78
use PHPStan\Testing\RuleTestCase;
@@ -23,6 +24,7 @@ protected function getRule(): Rule
2324
'ReadonlyPropertyAssign\\TestCase::setUp',
2425
],
2526
),
27+
new PhpVersion(PHP_VERSION_ID),
2628
);
2729
}
2830

@@ -154,4 +156,21 @@ public function testBug6773(): void
154156
]);
155157
}
156158

159+
public function testBug11495(): void
160+
{
161+
if (PHP_VERSION_ID < 80100) {
162+
$this->markTestSkipped('Test requires PHP 8.1.');
163+
}
164+
165+
$errors = [];
166+
if (PHP_VERSION_ID < 80300) {
167+
$errors[] = [
168+
'Readonly property Bug11495\HelloWorld::$foo is assigned outside of the constructor.',
169+
17,
170+
];
171+
}
172+
173+
$this->analyse([__DIR__ . '/data/bug-11495.php'], $errors);
174+
}
175+
157176
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php // lint >= 8.1
2+
declare(strict_types = 1);
3+
4+
namespace Bug11495;
5+
6+
class HelloWorld
7+
{
8+
private readonly string $foo;
9+
10+
public function __construct()
11+
{
12+
$this->foo = 'bar';
13+
}
14+
15+
public function __clone()
16+
{
17+
$this->foo = 'baz';
18+
}
19+
20+
public function getFoo(): string
21+
{
22+
return $this->foo;
23+
}
24+
}

0 commit comments

Comments
 (0)