Skip to content

Commit df1391d

Browse files
authored
[PHP 8.0] Retype expr by attribute parameter type (#1376)
1 parent 24cd533 commit df1391d

19 files changed

Lines changed: 201 additions & 144 deletions

packages-tests/PhpAttribute/Printer/PhpAttributeGroupFactoryTest.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use PhpParser\Node\AttributeGroup;
99
use Rector\PhpAttribute\Printer\PhpAttributeGroupFactory;
1010
use Rector\Testing\PHPUnit\AbstractTestCase;
11+
use Rector\Tests\Transform\Rector\FuncCall\ArgumentFuncCallToMethodCallRector\Fixture\Route;
1112

1213
final class PhpAttributeGroupFactoryTest extends AbstractTestCase
1314
{
@@ -37,7 +38,7 @@ public function testCreateArgsFromItems(): void
3738
$args = $this->phpAttributeGroupFactory->createArgsFromItems([
3839
'path' => '/path',
3940
'name' => 'action',
40-
]);
41+
], Route::class);
4142

4243
$this->assertCount(2, $args);
4344
$this->assertContainsOnlyInstancesOf(Arg::class, $args);
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\PhpAttribute\NodeAnalyzer;
6+
7+
use PhpParser\Node\Expr;
8+
use PhpParser\Node\Scalar\LNumber;
9+
use PhpParser\Node\Scalar\String_;
10+
use PHPStan\Reflection\ParameterReflection;
11+
use PHPStan\Reflection\ParametersAcceptorSelector;
12+
use PHPStan\Reflection\ReflectionProvider;
13+
use PHPStan\Type\BooleanType;
14+
use PHPStan\Type\IntegerType;
15+
use PHPStan\Type\TypeCombinator;
16+
use Rector\Core\PhpParser\Node\NodeFactory;
17+
use Rector\StaticTypeMapper\StaticTypeMapper;
18+
19+
final class ExprParameterReflectionTypeCorrector
20+
{
21+
public function __construct(
22+
private StaticTypeMapper $staticTypeMapper,
23+
private ReflectionProvider $reflectionProvider,
24+
private NodeFactory $nodeFactory
25+
) {
26+
}
27+
28+
/**
29+
* @param array<string|int, Expr|mixed> $items
30+
* @return array<string|int, Expr|mixed>
31+
*/
32+
public function correctItemsByAttributeClass(array $items, string $attributeClass): array
33+
{
34+
if (! $this->reflectionProvider->hasClass($attributeClass)) {
35+
return $items;
36+
}
37+
38+
$attributeClassReflection = $this->reflectionProvider->getClass($attributeClass);
39+
40+
// nothing to retype by constructor
41+
if (! $attributeClassReflection->hasConstructor()) {
42+
return $items;
43+
}
44+
45+
$constructorClassMethodReflection = $attributeClassReflection->getConstructor();
46+
47+
$parametersAcceptor = ParametersAcceptorSelector::selectSingle(
48+
$constructorClassMethodReflection->getVariants()
49+
);
50+
51+
foreach ($items as $name => $item) {
52+
foreach ($parametersAcceptor->getParameters() as $parameterReflection) {
53+
$correctedItem = $this->correctItemByParameterReflection($name, $item, $parameterReflection);
54+
if (! $correctedItem instanceof Expr) {
55+
continue;
56+
}
57+
58+
$items[$name] = $correctedItem;
59+
continue 2;
60+
}
61+
}
62+
63+
return $items;
64+
}
65+
66+
private function correctItemByParameterReflection(
67+
string|int $name,
68+
mixed $item,
69+
ParameterReflection $parameterReflection,
70+
): Expr|null {
71+
if (! $item instanceof Expr) {
72+
return null;
73+
}
74+
75+
if ($name !== $parameterReflection->getName()) {
76+
return null;
77+
}
78+
79+
$parameterType = $parameterReflection->getType();
80+
81+
$currentType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($item);
82+
83+
// all good
84+
if ($parameterType->accepts($currentType, false)->yes()) {
85+
return null;
86+
}
87+
88+
$clearParameterType = TypeCombinator::removeNull($parameterType);
89+
90+
// correct type
91+
if ($clearParameterType instanceof IntegerType && $item instanceof String_) {
92+
return new LNumber((int) $item->value);
93+
}
94+
95+
if ($clearParameterType instanceof BooleanType && $item instanceof String_) {
96+
if (strtolower($item->value) === 'true') {
97+
return $this->nodeFactory->createTrue();
98+
}
99+
100+
if (strtolower($item->value) === 'false') {
101+
return $this->nodeFactory->createFalse();
102+
}
103+
}
104+
105+
return null;
106+
}
107+
}

packages/PhpAttribute/Printer/PhpAttributeGroupFactory.php

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use Rector\BetterPhpDocParser\PhpDoc\DoctrineAnnotationTagValueNode;
1414
use Rector\Php80\ValueObject\AnnotationToAttribute;
1515
use Rector\PhpAttribute\AnnotationToAttributeMapper;
16+
use Rector\PhpAttribute\NodeAnalyzer\ExprParameterReflectionTypeCorrector;
1617
use Rector\PhpAttribute\NodeAnalyzer\NamedArgumentsResolver;
1718
use Rector\PhpAttribute\NodeFactory\AttributeNameFactory;
1819
use Rector\PhpAttribute\NodeFactory\NamedArgsFactory;
@@ -27,7 +28,8 @@ public function __construct(
2728
private NamedArgumentsResolver $namedArgumentsResolver,
2829
private AnnotationToAttributeMapper $annotationToAttributeMapper,
2930
private AttributeNameFactory $attributeNameFactory,
30-
private NamedArgsFactory $namedArgsFactory
31+
private NamedArgsFactory $namedArgsFactory,
32+
private ExprParameterReflectionTypeCorrector $exprParameterReflectionTypeCorrector
3133
) {
3234
}
3335

@@ -49,7 +51,7 @@ public function createFromClass(string $attributeClass): AttributeGroup
4951
public function createFromClassWithItems(string $attributeClass, array $items): AttributeGroup
5052
{
5153
$fullyQualified = new FullyQualified($attributeClass);
52-
$args = $this->createArgsFromItems($items);
54+
$args = $this->createArgsFromItems($items, $attributeClass);
5355
$attribute = new Attribute($fullyQualified, $args);
5456

5557
return new AttributeGroup([$attribute]);
@@ -61,7 +63,7 @@ public function create(
6163
): AttributeGroup {
6264
$values = $doctrineAnnotationTagValueNode->getValuesWithExplicitSilentAndWithoutQuotes();
6365

64-
$args = $this->createArgsFromItems($values);
66+
$args = $this->createArgsFromItems($values, $annotationToAttribute->getAttributeClass());
6567
$argumentNames = $this->namedArgumentsResolver->resolveFromClass($annotationToAttribute->getAttributeClass());
6668

6769
$this->completeNamedArguments($args, $argumentNames);
@@ -76,10 +78,13 @@ public function create(
7678
* @param mixed[] $items
7779
* @return Arg[]
7880
*/
79-
public function createArgsFromItems(array $items): array
81+
public function createArgsFromItems(array $items, string $attributeClass): array
8082
{
8183
/** @var Expr[] $items */
8284
$items = $this->annotationToAttributeMapper->map($items);
85+
86+
$items = $this->exprParameterReflectionTypeCorrector->correctItemsByAttributeClass($items, $attributeClass);
87+
8388
return $this->namedArgsFactory->createFromValues($items);
8489
}
8590

rules-tests/Php80/Rector/Class_/AnnotationToAttributeRector/Fixture/join_table.php.inc renamed to rules-tests/Php80/Rector/Class_/AnnotationToAttributeRector/Fixture/Doctrine/doctrine_join_table.php.inc

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
<?php
22

3-
namespace Rector\Doctrine\Tests\Set\DoctrineORM29Set\Fixture;
3+
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture\Doctrine;
44

55
use Doctrine\Common\Collections\Collection;
66
use Doctrine\ORM\Mapping as ORM;
77

8-
class AnEntity
8+
final class DoctrineJoinTable
99
{
1010
/**
11-
* @ORM\ManyToMany(targetEntity=AnEntity::class)
11+
* @ORM\ManyToMany(targetEntity=DoctrineJoinTable::class)
1212
* @ORM\JoinTable(
1313
* name="my_join_table",
1414
* joinColumns={@ORM\JoinColumn(name="first_column", referencedColumnName="id", onDelete="CASCADE")},
@@ -22,12 +22,12 @@ class AnEntity
2222
-----
2323
<?php
2424

25-
namespace Rector\Doctrine\Tests\Set\DoctrineORM29Set\Fixture;
25+
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture\Doctrine;
2626

2727
use Doctrine\Common\Collections\Collection;
2828
use Doctrine\ORM\Mapping as ORM;
2929

30-
class AnEntity
30+
final class DoctrineJoinTable
3131
{
3232
/**
3333
* @ORM\JoinTable(
@@ -36,7 +36,7 @@ class AnEntity
3636
* inverseJoinColumns={@ORM\JoinColumn(name="second_column", referencedColumnName="id")}
3737
* )
3838
**/
39-
#[ORM\ManyToMany(targetEntity: AnEntity::class)]
39+
#[ORM\ManyToMany(targetEntity: DoctrineJoinTable::class)]
4040
protected Collection $list;
4141
}
4242

rules-tests/Php80/Rector/Class_/AnnotationToAttributeRector/Fixture/join_columns.php.inc renamed to rules-tests/Php80/Rector/Class_/AnnotationToAttributeRector/Fixture/Doctrine/skip_doctrine_nested_join_columns.php.inc

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
<?php
22

3-
namespace Rector\Doctrine\Tests\Set\DoctrineORM29Set\Fixture;
3+
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture\Doctrine;
44

55
use Doctrine\ORM\Mapping as ORM;
66

7-
class AnEntity
7+
final class SkipDoctrineNestedJoinColumns
88
{
99
/**
1010
* @ORM\JoinColumns({
@@ -14,5 +14,3 @@ class AnEntity
1414
**/
1515
protected $page;
1616
}
17-
18-
?>

rules-tests/Php80/Rector/Class_/AnnotationToAttributeRector/Fixture/symfony_route.php.inc renamed to rules-tests/Php80/Rector/Class_/AnnotationToAttributeRector/Fixture/Symfony/symfony_route.php.inc

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
<?php
22

3-
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture;
3+
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture\Symfony;
44

55
use Symfony\Component\Routing\Annotation\Route;
66

7-
class SymfonyRoute
7+
final class SymfonyRoute
88
{
99
/**
1010
* @Route("/path", name=EntityColumnAndAssertChoice::class)
@@ -18,11 +18,11 @@ class SymfonyRoute
1818
-----
1919
<?php
2020

21-
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture;
21+
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture\Symfony;
2222

2323
use Symfony\Component\Routing\Annotation\Route;
2424

25-
class SymfonyRoute
25+
final class SymfonyRoute
2626
{
2727
#[Route(path: '/path', name: EntityColumnAndAssertChoice::class)]
2828
public function action()
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture\Symfony;
4+
5+
use Symfony\Component\Validator\Constraints as Assert;
6+
7+
final class ValidationIntegerParameter
8+
{
9+
/**
10+
* @Assert\Length(
11+
* min="100",
12+
* max="255",
13+
* maxMessage="some Message",
14+
* allowed="true"
15+
* )
16+
*/
17+
public function action()
18+
{
19+
}
20+
}
21+
22+
?>
23+
-----
24+
<?php
25+
26+
namespace Rector\Tests\Php80\Rector\Class_\AnnotationToAttributeRector\Fixture\Symfony;
27+
28+
use Symfony\Component\Validator\Constraints as Assert;
29+
30+
final class ValidationIntegerParameter
31+
{
32+
#[Assert\Length(min: 100, max: 255, maxMessage: 'some Message', allowed: true)]
33+
public function action()
34+
{
35+
}
36+
}
37+
38+
?>

rules-tests/Php80/Rector/Class_/AnnotationToAttributeRector/Fixture/index.php.inc

Lines changed: 0 additions & 13 deletions
This file was deleted.

rules-tests/Php80/Rector/Class_/AnnotationToAttributeRector/Fixture/nested_unwrap.php.inc

Lines changed: 0 additions & 20 deletions
This file was deleted.

rules-tests/Php80/Rector/Class_/AnnotationToAttributeRector/Fixture/schema_after_table_name_with_unique_constraint.php.inc

Lines changed: 0 additions & 16 deletions
This file was deleted.

0 commit comments

Comments
 (0)