Skip to content

Commit b6e9f5d

Browse files
author
ILJA MANYUTIN
committed
[Php80] Skip promotion when property type is wider than constructor param on ClassPropertyAssignToConstructorPromotionRector (#9809)
1 parent d67958f commit b6e9f5d

3 files changed

Lines changed: 78 additions & 0 deletions

File tree

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace Rector\Tests\Php80\Rector\Class_\ClassPropertyAssignToConstructorPromotionRector\Fixture;
4+
5+
final class NarrowerPropertyType
6+
{
7+
private object $entity;
8+
9+
public function __construct(?object $entity)
10+
{
11+
$this->entity = $entity;
12+
}
13+
}
14+
15+
?>
16+
-----
17+
<?php
18+
19+
namespace Rector\Tests\Php80\Rector\Class_\ClassPropertyAssignToConstructorPromotionRector\Fixture;
20+
21+
final class NarrowerPropertyType
22+
{
23+
public function __construct(private object $entity)
24+
{
25+
}
26+
}
27+
28+
?>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
namespace Rector\Tests\Php80\Rector\Class_\ClassPropertyAssignToConstructorPromotionRector\Fixture;
4+
5+
final class SkipWiderPropertyType
6+
{
7+
public private(set) ?object $entity = null;
8+
9+
public function __construct(object $entity)
10+
{
11+
$this->entity = $entity;
12+
}
13+
14+
public function clear(): void
15+
{
16+
$this->entity = null;
17+
}
18+
}

rules/Php80/Rector/Class_/ClassPropertyAssignToConstructorPromotionRector.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,10 @@ public function refactor(Node $node): ?Node
220220
continue;
221221
}
222222

223+
if ($this->shouldSkipWiderPropertyType($property, $param)) {
224+
continue;
225+
}
226+
223227
$hasChanged = true;
224228

225229
// remove property from class
@@ -479,4 +483,32 @@ private function shouldUsePropertyTypeForPromotedParam(Property $property, Param
479483

480484
return $this->typeComparator->areTypesEqual($type, $paramTypeWithoutNull);
481485
}
486+
487+
/**
488+
* Promotion shares one type. When processUnionType would copy the property type onto the
489+
* param and that type is strictly wider (e.g. ?object vs object), skip — otherwise the
490+
* constructor contract is loosened. Narrowing (object vs ?object) stays allowed.
491+
* Interface vs implementation does not fire here: shouldUsePropertyTypeForPromotedParam
492+
* is false when the non-null base types differ.
493+
*/
494+
private function shouldSkipWiderPropertyType(Property $property, Param $param): bool
495+
{
496+
if (! $this->shouldUsePropertyTypeForPromotedParam($property, $param)) {
497+
return false;
498+
}
499+
500+
if (! $property->type instanceof Node || ! $param->type instanceof Node) {
501+
return false;
502+
}
503+
504+
$propertyType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($property->type);
505+
$paramType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($param->type);
506+
507+
if ($param->default instanceof Expr) {
508+
$paramType = TypeCombinator::union($paramType, $this->getType($param->default));
509+
}
510+
511+
return $this->typeComparator->isSubtype($paramType, $propertyType)
512+
&& ! $this->typeComparator->areTypesEqual($propertyType, $paramType);
513+
}
482514
}

0 commit comments

Comments
 (0)