Skip to content

Commit 8f25178

Browse files
committed
improve data provider methods finder
1 parent 77e5a62 commit 8f25178

4 files changed

Lines changed: 133 additions & 148 deletions

File tree

rules/TypeDeclaration/Rector/ClassMethod/AddParamTypeBasedOnPHPUnitDataProviderRector.php

Lines changed: 13 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -4,32 +4,25 @@
44

55
namespace Rector\TypeDeclaration\Rector\ClassMethod;
66

7-
use Nette\Utils\Strings;
87
use PhpParser\Node;
98
use PhpParser\Node\ArrayItem;
10-
use PhpParser\Node\Attribute;
11-
use PhpParser\Node\AttributeGroup;
129
use PhpParser\Node\Expr\Array_;
1310
use PhpParser\Node\Expr\Yield_;
14-
use PhpParser\Node\Scalar\String_;
1511
use PhpParser\Node\Stmt\Class_;
1612
use PhpParser\Node\Stmt\ClassMethod;
1713
use PhpParser\Node\Stmt\Return_;
18-
use PHPStan\PhpDocParser\Ast\PhpDoc\GenericTagValueNode;
19-
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
2014
use PHPStan\Type\Constant\ConstantArrayType;
2115
use PHPStan\Type\MixedType;
2216
use PHPStan\Type\Type;
2317
use PHPStan\Type\TypeCombinator;
24-
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo;
25-
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory;
2618
use Rector\NodeTypeResolver\PHPStan\Type\TypeFactory;
2719
use Rector\PhpParser\Node\BetterNodeFinder;
2820
use Rector\PHPStanStaticTypeMapper\Enum\TypeKind;
2921
use Rector\PHPUnit\NodeAnalyzer\TestsNodeAnalyzer;
3022
use Rector\Rector\AbstractRector;
3123
use Rector\StaticTypeMapper\StaticTypeMapper;
3224
use Rector\TypeDeclaration\ValueObject\DataProviderNodes;
25+
use Rector\TypeDeclarationDocblocks\NodeFinder\DataProviderMethodsFinder;
3326
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
3427
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
3528

@@ -43,16 +36,10 @@ final class AddParamTypeBasedOnPHPUnitDataProviderRector extends AbstractRector
4336
*/
4437
private const ERROR_MESSAGE = 'Adds param type declaration based on PHPUnit provider return type declaration';
4538

46-
/**
47-
* @see https://regex101.com/r/hW09Vt/1
48-
* @var string
49-
*/
50-
private const METHOD_NAME_REGEX = '#^(?<method_name>\w+)(\(\))?#';
51-
5239
public function __construct(
5340
private readonly TypeFactory $typeFactory,
5441
private readonly TestsNodeAnalyzer $testsNodeAnalyzer,
55-
private readonly PhpDocInfoFactory $phpDocInfoFactory,
42+
private readonly DataProviderMethodsFinder $dataProviderMethodsFinder,
5643
private readonly BetterNodeFinder $betterNodeFinder,
5744
private readonly StaticTypeMapper $staticTypeMapper,
5845
) {
@@ -131,12 +118,12 @@ public function refactor(Node $node): ?Node
131118
continue;
132119
}
133120

134-
$dataProviderNodes = $this->resolveDataProviderNodes($classMethod);
135-
if ($dataProviderNodes->isEmpty()) {
121+
$dataProviderNodes = $this->dataProviderMethodsFinder->findDataProviderNodes($node, $classMethod);
122+
if ($dataProviderNodes->getClassMethods() === []) {
136123
continue;
137124
}
138125

139-
$hasClassMethodChanged = $this->refactorClassMethod($classMethod, $node, $dataProviderNodes->nodes);
126+
$hasClassMethodChanged = $this->refactorClassMethod($classMethod, $dataProviderNodes);
140127
if ($hasClassMethodChanged) {
141128
$hasChanged = true;
142129
}
@@ -149,16 +136,8 @@ public function refactor(Node $node): ?Node
149136
return null;
150137
}
151138

152-
private function inferParam(
153-
Class_ $class,
154-
int $parameterPosition,
155-
PhpDocTagNode | Attribute $dataProviderNode
156-
): Type {
157-
$dataProviderClassMethod = $this->resolveDataProviderClassMethod($class, $dataProviderNode);
158-
if (! $dataProviderClassMethod instanceof ClassMethod) {
159-
return new MixedType();
160-
}
161-
139+
private function inferParam(int $parameterPosition, ClassMethod $dataProviderClassMethod): Type
140+
{
162141
$returns = $this->betterNodeFinder->findReturnsScoped($dataProviderClassMethod);
163142
if ($returns !== []) {
164143
return $this->resolveReturnStaticArrayTypeByParameterPosition($returns, $parameterPosition);
@@ -169,33 +148,6 @@ private function inferParam(
169148
return $this->resolveYieldStaticArrayTypeByParameterPosition($yields, $parameterPosition);
170149
}
171150

172-
private function resolveDataProviderClassMethod(
173-
Class_ $class,
174-
Attribute | PhpDocTagNode $dataProviderNode
175-
): ?ClassMethod {
176-
if ($dataProviderNode instanceof Attribute) {
177-
$value = $dataProviderNode->args[0]->value;
178-
179-
if (! $value instanceof String_) {
180-
return null;
181-
}
182-
183-
$content = $value->value;
184-
} elseif ($dataProviderNode->value instanceof GenericTagValueNode) {
185-
$content = $dataProviderNode->value->value;
186-
} else {
187-
return null;
188-
}
189-
190-
$match = Strings::match($content, self::METHOD_NAME_REGEX);
191-
if ($match === null) {
192-
return null;
193-
}
194-
195-
$methodName = $match['method_name'];
196-
return $class->getMethod($methodName);
197-
}
198-
199151
/**
200152
* @param Return_[] $returns
201153
*/
@@ -289,47 +241,7 @@ private function resolveParamOnPositionTypes(Array_ $array, int $parameterPositi
289241
return $paramOnPositionTypes;
290242
}
291243

292-
private function resolveDataProviderNodes(ClassMethod $classMethod): DataProviderNodes
293-
{
294-
$attributes = $this->getPhpDataProviderAttributes($classMethod);
295-
296-
$classMethodPhpDocInfo = $this->phpDocInfoFactory->createFromNode($classMethod);
297-
298-
$phpdocNodes = $classMethodPhpDocInfo instanceof PhpDocInfo ?
299-
$classMethodPhpDocInfo->getTagsByName('@dataProvider') : [];
300-
301-
return new DataProviderNodes([...$attributes, ...$phpdocNodes]);
302-
}
303-
304-
/**
305-
* @return array<array-key, Attribute>
306-
*/
307-
private function getPhpDataProviderAttributes(ClassMethod $classMethod): array
308-
{
309-
$attributeName = 'PHPUnit\Framework\Attributes\DataProvider';
310-
311-
/** @var AttributeGroup[] $attrGroups */
312-
$attrGroups = $classMethod->attrGroups;
313-
314-
$dataProviders = [];
315-
316-
foreach ($attrGroups as $attrGroup) {
317-
foreach ($attrGroup->attrs as $attribute) {
318-
if (! $this->isName($attribute->name, $attributeName)) {
319-
continue;
320-
}
321-
322-
$dataProviders[] = $attribute;
323-
}
324-
}
325-
326-
return $dataProviders;
327-
}
328-
329-
/**
330-
* @param array<Attribute|PhpDocTagNode> $dataProviderNodes
331-
*/
332-
private function refactorClassMethod(ClassMethod $classMethod, Class_ $class, array $dataProviderNodes): bool
244+
private function refactorClassMethod(ClassMethod $classMethod, DataProviderNodes $dataProviderNodes): bool
333245
{
334246
$hasChanged = false;
335247

@@ -343,19 +255,18 @@ private function refactorClassMethod(ClassMethod $classMethod, Class_ $class, ar
343255
}
344256

345257
$paramTypes = [];
346-
foreach ($dataProviderNodes as $dataProviderNode) {
347-
$paramTypes[] = $this->inferParam($class, $parameterPosition, $dataProviderNode);
258+
foreach ($dataProviderNodes->getClassMethods() as $dataProviderClassMethod) {
259+
$paramTypes[] = $this->inferParam($parameterPosition, $dataProviderClassMethod);
348260
}
349261

350262
$paramTypeDeclaration = TypeCombinator::union(...$paramTypes);
351-
352263
if ($paramTypeDeclaration instanceof MixedType) {
353264
continue;
354265
}
355266

356-
$type = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($paramTypeDeclaration, TypeKind::PARAM);
357-
if ($type instanceof Node) {
358-
$param->type = $type;
267+
$typeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($paramTypeDeclaration, TypeKind::PARAM);
268+
if ($typeNode instanceof Node) {
269+
$param->type = $typeNode;
359270
$hasChanged = true;
360271
}
361272
}

rules/TypeDeclaration/ValueObject/DataProviderNodes.php

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,93 @@
44

55
namespace Rector\TypeDeclaration\ValueObject;
66

7+
use Nette\Utils\Strings;
78
use PhpParser\Node\Attribute;
9+
use PhpParser\Node\Scalar\String_;
10+
use PhpParser\Node\Stmt\Class_;
11+
use PhpParser\Node\Stmt\ClassMethod;
12+
use PHPStan\PhpDocParser\Ast\PhpDoc\GenericTagValueNode;
813
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
14+
use Webmozart\Assert\Assert;
915

1016
final readonly class DataProviderNodes
1117
{
1218
/**
13-
* @param array<Attribute|PhpDocTagNode> $nodes
19+
* @see https://regex101.com/r/hW09Vt/1
20+
* @var string
21+
*/
22+
private const METHOD_NAME_REGEX = '#^(?<method_name>\w+)(\(\))?#';
23+
24+
/**
25+
* @param Attribute[] $attributes
26+
* @param PhpDocTagNode[] $phpDocTagNodes
1427
*/
1528
public function __construct(
16-
public array $nodes,
29+
private Class_ $class,
30+
private array $attributes,
31+
private array $phpDocTagNodes,
1732
) {
33+
Assert::allIsInstanceOf($attributes, Attribute::class);
34+
Assert::allIsInstanceOf($phpDocTagNodes, PhpDocTagNode::class);
1835
}
1936

2037
public function isEmpty(): bool
2138
{
22-
return $this->nodes === [];
39+
return $this->getClassMethods() === [];
40+
}
41+
42+
/**
43+
* @return ClassMethod[]
44+
*/
45+
public function getClassMethods(): array
46+
{
47+
$classMethods = [];
48+
49+
foreach ($this->phpDocTagNodes as $phpDocTagNode) {
50+
if ($phpDocTagNode->value instanceof GenericTagValueNode) {
51+
$methodName = $this->matchMethodName($phpDocTagNode->value->value);
52+
if (! is_string($methodName)) {
53+
continue;
54+
}
55+
56+
$classMethod = $this->class->getMethod($methodName);
57+
if (! $classMethod instanceof ClassMethod) {
58+
continue;
59+
}
60+
61+
$classMethods[] = $classMethod;
62+
}
63+
}
64+
65+
foreach ($this->attributes as $attribute) {
66+
$value = $attribute->args[0]->value;
67+
if (! $value instanceof String_) {
68+
continue;
69+
}
70+
71+
$methodName = $this->matchMethodName($value->value);
72+
if (! is_string($methodName)) {
73+
continue;
74+
}
75+
76+
$classMethod = $this->class->getMethod($methodName);
77+
if (! $classMethod instanceof ClassMethod) {
78+
continue;
79+
}
80+
81+
$classMethods[] = $classMethod;
82+
}
83+
84+
return $classMethods;
85+
}
86+
87+
private function matchMethodName(string $content): ?string
88+
{
89+
$match = Strings::match($content, self::METHOD_NAME_REGEX);
90+
if ($match === null) {
91+
return null;
92+
}
93+
94+
return $match['method_name'];
2395
}
2496
}

rules/TypeDeclarationDocblocks/NodeFinder/DataProviderMethodsFinder.php

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,14 @@
55
namespace Rector\TypeDeclarationDocblocks\NodeFinder;
66

77
use PhpParser\Node\Attribute;
8+
use PhpParser\Node\Stmt\Class_;
89
use PhpParser\Node\Stmt\ClassMethod;
910
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo;
1011
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory;
1112
use Rector\NodeNameResolver\NodeNameResolver;
1213
use Rector\TypeDeclaration\ValueObject\DataProviderNodes;
1314
use Rector\TypeDeclarationDocblocks\Enum\TestClassName;
1415

15-
/**
16-
* @todo re-use in rector-phpunit
17-
* @see AddParamTypeBasedOnPHPUnitDataProviderRector
18-
*/
1916
final readonly class DataProviderMethodsFinder
2017
{
2118
public function __construct(
@@ -24,7 +21,7 @@ public function __construct(
2421
) {
2522
}
2623

27-
public function findDataProviderNodes(ClassMethod $classMethod): DataProviderNodes
24+
public function findDataProviderNodes(Class_ $class, ClassMethod $classMethod): DataProviderNodes
2825
{
2926
$phpDocInfo = $this->phpDocInfoFactory->createFromNode($classMethod);
3027
if ($phpDocInfo instanceof PhpDocInfo) {
@@ -35,7 +32,7 @@ public function findDataProviderNodes(ClassMethod $classMethod): DataProviderNod
3532

3633
$attributes = $this->findDataProviderAttributes($classMethod);
3734

38-
return new DataProviderNodes([...$attributes, ...$phpdocNodes]);
35+
return new DataProviderNodes($class, $attributes, $phpdocNodes);
3936
}
4037

4138
/**

0 commit comments

Comments
 (0)