Skip to content

Commit 54a24bd

Browse files
villfaondrejmirtes
authored andcommitted
Add support for data provider attributes
1 parent 7f7b59b commit 54a24bd

5 files changed

+192
-57
lines changed

src/Rules/PHPUnit/DataProviderDeclarationRule.php

+5-28
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
use PhpParser\Node;
66
use PHPStan\Analyser\Scope;
77
use PHPStan\Rules\Rule;
8-
use PHPStan\Type\FileTypeMapper;
98
use PHPUnit\Framework\TestCase;
109
use function array_merge;
1110

@@ -22,13 +21,6 @@ class DataProviderDeclarationRule implements Rule
2221
*/
2322
private $dataProviderHelper;
2423

25-
/**
26-
* The file type mapper.
27-
*
28-
* @var FileTypeMapper
29-
*/
30-
private $fileTypeMapper;
31-
3224
/**
3325
* When set to true, it reports data provider method with incorrect name case.
3426
*
@@ -45,13 +37,11 @@ class DataProviderDeclarationRule implements Rule
4537

4638
public function __construct(
4739
DataProviderHelper $dataProviderHelper,
48-
FileTypeMapper $fileTypeMapper,
4940
bool $checkFunctionNameCase,
5041
bool $deprecationRulesInstalled
5142
)
5243
{
5344
$this->dataProviderHelper = $dataProviderHelper;
54-
$this->fileTypeMapper = $fileTypeMapper;
5545
$this->checkFunctionNameCase = $checkFunctionNameCase;
5646
$this->deprecationRulesInstalled = $deprecationRulesInstalled;
5747
}
@@ -69,29 +59,16 @@ public function processNode(Node $node, Scope $scope): array
6959
return [];
7060
}
7161

72-
$docComment = $node->getDocComment();
73-
if ($docComment === null) {
74-
return [];
75-
}
76-
77-
$methodPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
78-
$scope->getFile(),
79-
$classReflection->getName(),
80-
$scope->isInTrait() ? $scope->getTraitReflection()->getName() : null,
81-
$node->name->toString(),
82-
$docComment->getText()
83-
);
84-
85-
$annotations = $this->dataProviderHelper->getDataProviderAnnotations($methodPhpDoc);
86-
8762
$errors = [];
8863

89-
foreach ($annotations as $annotation) {
64+
foreach ($this->dataProviderHelper->getDataProviderMethods($scope, $node, $classReflection) as $dataProviderValue => [$dataProviderClassReflection, $dataProviderMethodName, $lineNumber]) {
9065
$errors = array_merge(
9166
$errors,
9267
$this->dataProviderHelper->processDataProvider(
93-
$scope,
94-
$annotation,
68+
$dataProviderValue,
69+
$dataProviderClassReflection,
70+
$dataProviderMethodName,
71+
$lineNumber,
9572
$this->checkFunctionNameCase,
9673
$this->deprecationRulesInstalled
9774
)

src/Rules/PHPUnit/DataProviderHelper.php

+144-20
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
namespace PHPStan\Rules\PHPUnit;
44

5+
use PhpParser\Node\Attribute;
6+
use PhpParser\Node\Expr\ClassConstFetch;
7+
use PhpParser\Node\Name;
8+
use PhpParser\Node\Scalar\String_;
9+
use PhpParser\Node\Stmt\ClassMethod;
510
use PHPStan\Analyser\Scope;
611
use PHPStan\PhpDoc\ResolvedPhpDocBlock;
712
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
@@ -10,6 +15,7 @@
1015
use PHPStan\Reflection\ReflectionProvider;
1116
use PHPStan\Rules\RuleError;
1217
use PHPStan\Rules\RuleErrorBuilder;
18+
use PHPStan\Type\FileTypeMapper;
1319
use function array_merge;
1420
use function count;
1521
use function explode;
@@ -26,19 +32,84 @@ class DataProviderHelper
2632
*/
2733
private $reflectionProvider;
2834

35+
/**
36+
* The file type mapper.
37+
*
38+
* @var FileTypeMapper
39+
*/
40+
private $fileTypeMapper;
41+
2942
/** @var bool */
3043
private $phpunit10OrNewer;
3144

32-
public function __construct(ReflectionProvider $reflectionProvider, bool $phpunit10OrNewer)
45+
public function __construct(
46+
ReflectionProvider $reflectionProvider,
47+
FileTypeMapper $fileTypeMapper,
48+
bool $phpunit10OrNewer
49+
)
3350
{
3451
$this->reflectionProvider = $reflectionProvider;
52+
$this->fileTypeMapper = $fileTypeMapper;
3553
$this->phpunit10OrNewer = $phpunit10OrNewer;
3654
}
3755

56+
/**
57+
* @return iterable<array{ClassReflection|null, string, int}>
58+
*/
59+
public function getDataProviderMethods(
60+
Scope $scope,
61+
ClassMethod $node,
62+
ClassReflection $classReflection
63+
): iterable
64+
{
65+
$docComment = $node->getDocComment();
66+
if ($docComment !== null) {
67+
$methodPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
68+
$scope->getFile(),
69+
$classReflection->getName(),
70+
$scope->isInTrait() ? $scope->getTraitReflection()->getName() : null,
71+
$node->name->toString(),
72+
$docComment->getText()
73+
);
74+
foreach ($this->getDataProviderAnnotations($methodPhpDoc) as $annotation) {
75+
$dataProviderValue = $this->getDataProviderAnnotationValue($annotation);
76+
if ($dataProviderValue === null) {
77+
// Missing value is already handled in NoMissingSpaceInMethodAnnotationRule
78+
continue;
79+
}
80+
81+
$dataProviderMethod = $this->parseDataProviderAnnotationValue($scope, $dataProviderValue);
82+
$dataProviderMethod[] = $node->getLine();
83+
84+
yield $dataProviderValue => $dataProviderMethod;
85+
}
86+
}
87+
88+
if (!$this->phpunit10OrNewer) {
89+
return;
90+
}
91+
92+
foreach ($node->attrGroups as $attrGroup) {
93+
foreach ($attrGroup->attrs as $attr) {
94+
$dataProviderMethod = null;
95+
if ($attr->name->toLowerString() === 'phpunit\\framework\\attributes\\dataprovider') {
96+
$dataProviderMethod = $this->parseDataProviderAttribute($attr, $classReflection);
97+
} elseif ($attr->name->toLowerString() === 'phpunit\\framework\\attributes\\dataproviderexternal') {
98+
$dataProviderMethod = $this->parseDataProviderExternalAttribute($attr);
99+
}
100+
if ($dataProviderMethod === null) {
101+
continue;
102+
}
103+
104+
yield from $dataProviderMethod;
105+
}
106+
}
107+
}
108+
38109
/**
39110
* @return array<PhpDocTagNode>
40111
*/
41-
public function getDataProviderAnnotations(?ResolvedPhpDocBlock $phpDoc): array
112+
private function getDataProviderAnnotations(?ResolvedPhpDocBlock $phpDoc): array
42113
{
43114
if ($phpDoc === null) {
44115
return [];
@@ -62,67 +133,62 @@ public function getDataProviderAnnotations(?ResolvedPhpDocBlock $phpDoc): array
62133
* @return RuleError[] errors
63134
*/
64135
public function processDataProvider(
65-
Scope $scope,
66-
PhpDocTagNode $phpDocTag,
136+
string $dataProviderValue,
137+
?ClassReflection $classReflection,
138+
string $methodName,
139+
int $lineNumber,
67140
bool $checkFunctionNameCase,
68141
bool $deprecationRulesInstalled
69142
): array
70143
{
71-
$dataProviderValue = $this->getDataProviderValue($phpDocTag);
72-
if ($dataProviderValue === null) {
73-
// Missing value is already handled in NoMissingSpaceInMethodAnnotationRule
74-
return [];
75-
}
76-
77-
[$classReflection, $method] = $this->parseDataProviderValue($scope, $dataProviderValue);
78144
if ($classReflection === null) {
79145
$error = RuleErrorBuilder::message(sprintf(
80146
'@dataProvider %s related class not found.',
81147
$dataProviderValue
82-
))->build();
148+
))->line($lineNumber)->build();
83149

84150
return [$error];
85151
}
86152

87153
try {
88-
$dataProviderMethodReflection = $classReflection->getNativeMethod($method);
154+
$dataProviderMethodReflection = $classReflection->getNativeMethod($methodName);
89155
} catch (MissingMethodFromReflectionException $missingMethodFromReflectionException) {
90156
$error = RuleErrorBuilder::message(sprintf(
91157
'@dataProvider %s related method not found.',
92158
$dataProviderValue
93-
))->build();
159+
))->line($lineNumber)->build();
94160

95161
return [$error];
96162
}
97163

98164
$errors = [];
99165

100-
if ($checkFunctionNameCase && $method !== $dataProviderMethodReflection->getName()) {
166+
if ($checkFunctionNameCase && $methodName !== $dataProviderMethodReflection->getName()) {
101167
$errors[] = RuleErrorBuilder::message(sprintf(
102168
'@dataProvider %s related method is used with incorrect case: %s.',
103169
$dataProviderValue,
104170
$dataProviderMethodReflection->getName()
105-
))->build();
171+
))->line($lineNumber)->build();
106172
}
107173

108174
if (!$dataProviderMethodReflection->isPublic()) {
109175
$errors[] = RuleErrorBuilder::message(sprintf(
110176
'@dataProvider %s related method must be public.',
111177
$dataProviderValue
112-
))->build();
178+
))->line($lineNumber)->build();
113179
}
114180

115181
if ($deprecationRulesInstalled && $this->phpunit10OrNewer && !$dataProviderMethodReflection->isStatic()) {
116182
$errors[] = RuleErrorBuilder::message(sprintf(
117183
'@dataProvider %s related method must be static in PHPUnit 10 and newer.',
118184
$dataProviderValue
119-
))->build();
185+
))->line($lineNumber)->build();
120186
}
121187

122188
return $errors;
123189
}
124190

125-
private function getDataProviderValue(PhpDocTagNode $phpDocTag): ?string
191+
private function getDataProviderAnnotationValue(PhpDocTagNode $phpDocTag): ?string
126192
{
127193
if (preg_match('/^[^ \t]+/', (string) $phpDocTag->value, $matches) !== 1) {
128194
return null;
@@ -134,7 +200,7 @@ private function getDataProviderValue(PhpDocTagNode $phpDocTag): ?string
134200
/**
135201
* @return array{ClassReflection|null, string}
136202
*/
137-
private function parseDataProviderValue(Scope $scope, string $dataProviderValue): array
203+
private function parseDataProviderAnnotationValue(Scope $scope, string $dataProviderValue): array
138204
{
139205
$parts = explode('::', $dataProviderValue, 2);
140206
if (count($parts) <= 1) {
@@ -148,4 +214,62 @@ private function parseDataProviderValue(Scope $scope, string $dataProviderValue)
148214
return [null, $dataProviderValue];
149215
}
150216

217+
/**
218+
* @return array<string, array{(ClassReflection|null), string, int}>|null
219+
*/
220+
private function parseDataProviderExternalAttribute(Attribute $attribute): ?array
221+
{
222+
if (count($attribute->args) !== 2) {
223+
return null;
224+
}
225+
$methodNameArg = $attribute->args[1]->value;
226+
if (!$methodNameArg instanceof String_) {
227+
return null;
228+
}
229+
$classNameArg = $attribute->args[0]->value;
230+
if ($classNameArg instanceof ClassConstFetch && $classNameArg->class instanceof Name) {
231+
$className = $classNameArg->class->toString();
232+
} elseif ($classNameArg instanceof String_) {
233+
$className = $classNameArg->value;
234+
} else {
235+
return null;
236+
}
237+
238+
$dataProviderClassReflection = null;
239+
if ($this->reflectionProvider->hasClass($className)) {
240+
$dataProviderClassReflection = $this->reflectionProvider->getClass($className);
241+
$className = $dataProviderClassReflection->getName();
242+
}
243+
244+
return [
245+
sprintf('%s::%s', $className, $methodNameArg->value) => [
246+
$dataProviderClassReflection,
247+
$methodNameArg->value,
248+
$attribute->getLine(),
249+
],
250+
];
251+
}
252+
253+
/**
254+
* @return array<string, array{(ClassReflection|null), string, int}>|null
255+
*/
256+
private function parseDataProviderAttribute(Attribute $attribute, ClassReflection $classReflection): ?array
257+
{
258+
if (count($attribute->args) !== 1) {
259+
return null;
260+
}
261+
$methodNameArg = $attribute->args[0]->value;
262+
if (!$methodNameArg instanceof String_) {
263+
return null;
264+
}
265+
266+
return [
267+
$methodNameArg->value => [
268+
$classReflection,
269+
$methodNameArg->value,
270+
$attribute->getLine(),
271+
],
272+
];
273+
}
274+
151275
}

src/Rules/PHPUnit/DataProviderHelperFactory.php

+7-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace PHPStan\Rules\PHPUnit;
44

55
use PHPStan\Reflection\ReflectionProvider;
6+
use PHPStan\Type\FileTypeMapper;
67
use PHPUnit\Framework\TestCase;
78
use function dirname;
89
use function explode;
@@ -16,9 +17,13 @@ class DataProviderHelperFactory
1617
/** @var ReflectionProvider */
1718
private $reflectionProvider;
1819

19-
public function __construct(ReflectionProvider $reflectionProvider)
20+
/** @var FileTypeMapper */
21+
private $fileTypeMapper;
22+
23+
public function __construct(ReflectionProvider $reflectionProvider, FileTypeMapper $fileTypeMapper)
2024
{
2125
$this->reflectionProvider = $reflectionProvider;
26+
$this->fileTypeMapper = $fileTypeMapper;
2227
}
2328

2429
public function create(): DataProviderHelper
@@ -46,7 +51,7 @@ public function create(): DataProviderHelper
4651
}
4752
}
4853

49-
return new DataProviderHelper($this->reflectionProvider, $phpUnit10OrNewer);
54+
return new DataProviderHelper($this->reflectionProvider, $this->fileTypeMapper, $phpUnit10OrNewer);
5055
}
5156

5257
}

0 commit comments

Comments
 (0)