Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 1004481

Browse files
authored
Check redeclared promoted readonly properties
1 parent 1f95482 commit 1004481

9 files changed

+568
-0
lines changed

‎src/Analyser/NodeScopeResolver.php‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@
177177
use PHPStan\Type\TypeTraverser;
178178
use PHPStan\Type\TypeUtils;
179179
use PHPStan\Type\UnionType;
180+
use ReflectionProperty;
180181
use Throwable;
181182
use Traversable;
182183
use TypeError;
@@ -2722,6 +2723,26 @@ static function (): void {
27222723
$scope = $scope->invalidateExpression(new Variable('this'), true);
27232724
}
27242725

2726+
if (
2727+
$methodReflection !== null
2728+
&& !$methodReflection->isStatic()
2729+
&& $methodReflection->getName() === '__construct'
2730+
&& $scopeFunction instanceof MethodReflection
2731+
&& !$scopeFunction->isStatic()
2732+
&& $scope->isInClass()
2733+
&& $scope->getClassReflection()->isSubclassOf($methodReflection->getDeclaringClass()->getName())
2734+
) {
2735+
$thisType = $scope->getType(new Variable('this'));
2736+
$methodClassReflection = $methodReflection->getDeclaringClass();
2737+
foreach ($methodClassReflection->getNativeReflection()->getProperties(ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED) as $property) {
2738+
if (!$property->isPromoted() || $property->getDeclaringClass()->getName() !== $methodClassReflection->getName()) {
2739+
continue;
2740+
}
2741+
2742+
$scope = $scope->assignInitializedProperty($thisType, $property->getName());
2743+
}
2744+
}
2745+
27252746
$hasYield = $hasYield || $result->hasYield();
27262747
$throwPoints = array_merge($throwPoints, $result->getThrowPoints());
27272748
$impurePoints = array_merge($impurePoints, $result->getImpurePoints());

‎src/Node/ClassStatementsGatherer.php‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use PHPStan\Reflection\ClassReflection;
1919
use PHPStan\ShouldNotHappenException;
2020
use PHPStan\Type\TypeUtils;
21+
use ReflectionProperty;
2122
use function count;
2223
use function in_array;
2324
use function strtolower;
@@ -157,6 +158,9 @@ private function gatherNodes(Node $node, Scope $scope): void
157158
}
158159
if ($node instanceof MethodCall || $node instanceof StaticCall) {
159160
$this->methodCalls[] = new \PHPStan\Node\Method\MethodCall($node, $scope);
161+
if ($node instanceof StaticCall && $node->name instanceof Identifier && $node->name->toLowerString() === '__construct') {
162+
$this->tryToApplyPropertyWritesFromAncestorConstructor($node, $scope);
163+
}
160164
return;
161165
}
162166
if ($node instanceof MethodCallableNode || $node instanceof StaticMethodCallableNode) {
@@ -252,4 +256,28 @@ private function tryToApplyPropertyReads(Expr\FuncCall $node, Scope $scope): voi
252256
}
253257
}
254258

259+
private function tryToApplyPropertyWritesFromAncestorConstructor(StaticCall $ancestorConstructorCall, Scope $scope): void
260+
{
261+
if (!$ancestorConstructorCall->class instanceof Node\Name) {
262+
return;
263+
}
264+
265+
$calledOnType = $scope->resolveTypeByName($ancestorConstructorCall->class);
266+
if ($calledOnType->getClassReflection() === null || TypeUtils::findThisType($calledOnType) === null) {
267+
return;
268+
}
269+
270+
$classReflection = $calledOnType->getClassReflection()->getNativeReflection();
271+
foreach ($classReflection->getProperties(ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED) as $property) {
272+
if (!$property->isPromoted() || $property->getDeclaringClass()->getName() !== $classReflection->getName()) {
273+
continue;
274+
}
275+
$this->propertyUsages[] = new PropertyWrite(
276+
new PropertyFetch(new Expr\Variable('this'), new Identifier($property->getName()), $ancestorConstructorCall->getAttributes()),
277+
$scope,
278+
false,
279+
);
280+
}
281+
}
282+
255283
}

‎tests/PHPStan/Rules/Properties/MissingReadOnlyPropertyAssignRuleTest.php‎

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,4 +287,101 @@ public function testBug10822(): void
287287
$this->analyse([__DIR__ . '/data/bug-10822.php'], []);
288288
}
289289

290+
public function testRedeclaredReadonlyProperties(): void
291+
{
292+
if (PHP_VERSION_ID < 80100) {
293+
$this->markTestSkipped('Test requires PHP 8.1.');
294+
}
295+
296+
$this->analyse([__DIR__ . '/data/redeclare-readonly-property.php'], [
297+
[
298+
'Readonly property RedeclareReadonlyProperty\B1::$myProp is already assigned.',
299+
16,
300+
],
301+
[
302+
'Readonly property RedeclareReadonlyProperty\B5::$myProp is already assigned.',
303+
50,
304+
],
305+
[
306+
'Readonly property RedeclareReadonlyProperty\B7::$myProp is already assigned.',
307+
70,
308+
],
309+
[
310+
'Readonly property class@anonymous/tests/PHPStan/Rules/Properties/data/redeclare-readonly-property.php:117::$myProp is already assigned.',
311+
121,
312+
],
313+
[
314+
'Class RedeclareReadonlyProperty\B16 has an uninitialized readonly property $myProp. Assign it in the constructor.',
315+
195,
316+
],
317+
[
318+
'Class RedeclareReadonlyProperty\C17 has an uninitialized readonly property $aProp. Assign it in the constructor.',
319+
218,
320+
],
321+
[
322+
'Class RedeclareReadonlyProperty\B18 has an uninitialized readonly property $aProp. Assign it in the constructor.',
323+
233,
324+
],
325+
]);
326+
}
327+
328+
public function testRedeclaredPropertiesOfReadonlyClass(): void
329+
{
330+
if (PHP_VERSION_ID < 80200) {
331+
$this->markTestSkipped('Test requires PHP 8.2.');
332+
}
333+
334+
$this->analyse([__DIR__ . '/data/redeclare-property-of-readonly-class.php'], [
335+
[
336+
'Readonly property RedeclarePropertyOfReadonlyClass\B1::$promotedProp is already assigned.',
337+
15,
338+
],
339+
]);
340+
}
341+
342+
public function testBug8101(): void
343+
{
344+
if (PHP_VERSION_ID < 80100) {
345+
$this->markTestSkipped('Test requires PHP 8.1.');
346+
}
347+
348+
$this->analyse([__DIR__ . '/data/bug-8101.php'], [
349+
[
350+
'Readonly property Bug8101\B::$myProp is already assigned.',
351+
12,
352+
],
353+
]);
354+
}
355+
356+
public function testBug9863(): void
357+
{
358+
if (PHP_VERSION_ID < 80100) {
359+
$this->markTestSkipped('Test requires PHP 8.1.');
360+
}
361+
362+
$this->analyse([__DIR__ . '/data/bug-9863.php'], [
363+
[
364+
'Readonly property Bug9863\ReadonlyChildWithoutIsset::$foo is already assigned.',
365+
17,
366+
],
367+
[
368+
'Class Bug9863\ReadonlyParentWithIsset has an uninitialized readonly property $foo. Assign it in the constructor.',
369+
23,
370+
],
371+
[
372+
'Access to an uninitialized readonly property Bug9863\ReadonlyParentWithIsset::$foo.',
373+
28,
374+
],
375+
]);
376+
}
377+
378+
public function testBug9864(): void
379+
{
380+
if (PHP_VERSION_ID < 80100) {
381+
$this->markTestSkipped('Test requires PHP 8.1.');
382+
}
383+
384+
$this->analyse([__DIR__ . '/data/bug-9864.php'], []);
385+
}
386+
290387
}

‎tests/PHPStan/Rules/Properties/UninitializedPropertyRuleTest.php‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,4 +202,18 @@ public function testBug9831(): void
202202
]);
203203
}
204204

205+
public function testRedeclareReadonlyProperties(): void
206+
{
207+
$this->analyse([__DIR__ . '/data/redeclare-readonly-property.php'], [
208+
[
209+
'Class RedeclareReadonlyProperty\B19 has an uninitialized property $prop2. Give it default value or assign it in the constructor.',
210+
249,
211+
],
212+
[
213+
'Access to an uninitialized property RedeclareReadonlyProperty\B19::$prop2.',
214+
260,
215+
],
216+
]);
217+
}
218+
205219
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php declare(strict_types = 1); // lint >= 8.1
2+
3+
namespace Bug8101;
4+
5+
class A {
6+
public function __construct(public readonly int $myProp) {}
7+
}
8+
9+
class B extends A {
10+
// This should be reported as an error, as a readonly prop cannot be redeclared.
11+
public function __construct(public readonly int $myProp) {
12+
parent::__construct($myProp);
13+
}
14+
}
15+
16+
$foo = new B(7);
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php declare(strict_types = 1); // lint >= 8.1
2+
3+
namespace Bug9863;
4+
5+
class ReadonlyParentWithoutIsset
6+
{
7+
public function __construct(
8+
public readonly int $foo
9+
) {}
10+
}
11+
12+
class ReadonlyChildWithoutIsset extends ReadonlyParentWithoutIsset
13+
{
14+
public function __construct(
15+
public readonly int $foo = 42
16+
) {
17+
parent::__construct($foo);
18+
}
19+
}
20+
21+
class ReadonlyParentWithIsset
22+
{
23+
public readonly int $foo;
24+
25+
public function __construct(
26+
int $foo
27+
) {
28+
if (! isset($this->foo)) {
29+
$this->foo = $foo;
30+
}
31+
}
32+
}
33+
34+
class ReadonlyChildWithIsset extends ReadonlyParentWithIsset
35+
{
36+
public function __construct(
37+
public readonly int $foo = 42
38+
) {
39+
parent::__construct($foo);
40+
}
41+
}
42+
43+
$a = new ReadonlyParentWithoutIsset(0);
44+
$b = new ReadonlyChildWithoutIsset();
45+
$c = new ReadonlyChildWithoutIsset(1);
46+
47+
$x = new ReadonlyParentWithIsset(2);
48+
$y = new ReadonlyChildWithIsset();
49+
$z = new ReadonlyChildWithIsset(3);
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php declare(strict_types = 1); // lint >= 8.2
2+
3+
namespace Bug9864;
4+
5+
readonly abstract class UuidValueObject
6+
{
7+
public function __construct(public string $value)
8+
{
9+
$this->ensureIsValidUuid($value);
10+
}
11+
12+
private function ensureIsValidUuid(string $value): void
13+
{
14+
}
15+
}
16+
17+
18+
final readonly class ProductId extends UuidValueObject
19+
{
20+
public string $value;
21+
22+
public function __construct(
23+
string $value
24+
) {
25+
parent::__construct($value);
26+
}
27+
}
28+
29+
var_dump(new ProductId('test'));
30+
31+
// property is assigned on parent class, no need to reassing, specially for readonly properties
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php declare(strict_types = 1); // lint >= 8.2
2+
3+
namespace RedeclarePropertyOfReadonlyClass;
4+
5+
readonly class A {
6+
public function __construct(public int $promotedProp)
7+
{
8+
}
9+
}
10+
11+
readonly class B1 extends A {
12+
// $promotedProp is written twice
13+
public function __construct(public int $promotedProp)
14+
{
15+
parent::__construct(5);
16+
}
17+
}
18+
19+
readonly class B2 extends A {
20+
// Don't get confused by standard parameter with same name
21+
public function __construct(int $promotedProp)
22+
{
23+
parent::__construct($promotedProp);
24+
}
25+
}
26+
27+
readonly class B3 extends A {
28+
// This is allowed, because we don't write to the property.
29+
public int $promotedProp;
30+
31+
public function __construct()
32+
{
33+
parent::__construct(7);
34+
}
35+
}
36+
37+
readonly class B4 extends A {
38+
// The second write is not from the constructor. It is an error, but it is handled by different rule.
39+
public int $promotedProp;
40+
41+
public function __construct()
42+
{
43+
parent::__construct(7);
44+
}
45+
46+
public function set(): void
47+
{
48+
$this->promotedProp = 7;
49+
}
50+
}

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /