Skip to content

Commit 4c06b7e

Browse files
authored
Add rule to check @dataProvider
1 parent 8313d41 commit 4c06b7e

6 files changed

+321
-0
lines changed

extension.neon

+2
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ services:
5555
class: PHPStan\Rules\PHPUnit\CoversHelper
5656
-
5757
class: PHPStan\Rules\PHPUnit\AnnotationHelper
58+
-
59+
class: PHPStan\Rules\PHPUnit\DataProviderHelper
5860

5961
conditionalTags:
6062
PHPStan\PhpDoc\PHPUnit\MockObjectTypeNodeResolverExtension:

rules.neon

+6
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ rules:
88
services:
99
- class: PHPStan\Rules\PHPUnit\ClassCoversExistsRule
1010
- class: PHPStan\Rules\PHPUnit\ClassMethodCoversExistsRule
11+
-
12+
class: PHPStan\Rules\PHPUnit\DataProviderDeclarationRule
13+
arguments:
14+
checkFunctionNameCase: %checkFunctionNameCase%
1115
- class: PHPStan\Rules\PHPUnit\NoMissingSpaceInClassAnnotationRule
1216
- class: PHPStan\Rules\PHPUnit\NoMissingSpaceInMethodAnnotationRule
1317

@@ -16,6 +20,8 @@ conditionalTags:
1620
phpstan.rules.rule: %featureToggles.bleedingEdge%
1721
PHPStan\Rules\PHPUnit\ClassMethodCoversExistsRule:
1822
phpstan.rules.rule: %featureToggles.bleedingEdge%
23+
PHPStan\Rules\PHPUnit\DataProviderDeclarationRule:
24+
phpstan.rules.rule: %featureToggles.bleedingEdge%
1925
PHPStan\Rules\PHPUnit\NoMissingSpaceInClassAnnotationRule:
2026
phpstan.rules.rule: %featureToggles.bleedingEdge%
2127
PHPStan\Rules\PHPUnit\NoMissingSpaceInMethodAnnotationRule:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\PHPUnit;
4+
5+
use PhpParser\Node;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Rules\Rule;
8+
use PHPStan\Type\FileTypeMapper;
9+
use PHPUnit\Framework\TestCase;
10+
use function array_merge;
11+
12+
/**
13+
* @implements Rule<Node\Stmt\ClassMethod>
14+
*/
15+
class DataProviderDeclarationRule implements Rule
16+
{
17+
18+
/**
19+
* Data provider helper.
20+
*
21+
* @var DataProviderHelper
22+
*/
23+
private $dataProviderHelper;
24+
25+
/**
26+
* The file type mapper.
27+
*
28+
* @var FileTypeMapper
29+
*/
30+
private $fileTypeMapper;
31+
32+
/**
33+
* When set to true, it reports data provider method with incorrect name case.
34+
*
35+
* @var bool
36+
*/
37+
private $checkFunctionNameCase;
38+
39+
public function __construct(
40+
DataProviderHelper $dataProviderHelper,
41+
FileTypeMapper $fileTypeMapper,
42+
bool $checkFunctionNameCase
43+
)
44+
{
45+
$this->dataProviderHelper = $dataProviderHelper;
46+
$this->fileTypeMapper = $fileTypeMapper;
47+
$this->checkFunctionNameCase = $checkFunctionNameCase;
48+
}
49+
50+
public function getNodeType(): string
51+
{
52+
return Node\Stmt\ClassMethod::class;
53+
}
54+
55+
public function processNode(Node $node, Scope $scope): array
56+
{
57+
$classReflection = $scope->getClassReflection();
58+
59+
if ($classReflection === null || !$classReflection->isSubclassOf(TestCase::class)) {
60+
return [];
61+
}
62+
63+
$docComment = $node->getDocComment();
64+
if ($docComment === null) {
65+
return [];
66+
}
67+
68+
$methodPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
69+
$scope->getFile(),
70+
$classReflection->getName(),
71+
$scope->isInTrait() ? $scope->getTraitReflection()->getName() : null,
72+
$node->name->toString(),
73+
$docComment->getText()
74+
);
75+
76+
$annotations = $this->dataProviderHelper->getDataProviderAnnotations($methodPhpDoc);
77+
78+
$errors = [];
79+
80+
foreach ($annotations as $annotation) {
81+
$errors = array_merge(
82+
$errors,
83+
$this->dataProviderHelper->processDataProvider($scope, $annotation, $this->checkFunctionNameCase)
84+
);
85+
}
86+
87+
return $errors;
88+
}
89+
90+
}
+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\PHPUnit;
4+
5+
use PHPStan\Analyser\Scope;
6+
use PHPStan\PhpDoc\ResolvedPhpDocBlock;
7+
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
8+
use PHPStan\Reflection\MissingMethodFromReflectionException;
9+
use PHPStan\Rules\RuleError;
10+
use PHPStan\Rules\RuleErrorBuilder;
11+
use function array_merge;
12+
use function preg_match;
13+
use function sprintf;
14+
15+
class DataProviderHelper
16+
{
17+
18+
/**
19+
* @return array<PhpDocTagNode>
20+
*/
21+
public function getDataProviderAnnotations(?ResolvedPhpDocBlock $phpDoc): array
22+
{
23+
if ($phpDoc === null) {
24+
return [];
25+
}
26+
27+
$phpDocNodes = $phpDoc->getPhpDocNodes();
28+
29+
$annotations = [];
30+
31+
foreach ($phpDocNodes as $docNode) {
32+
$annotations = array_merge(
33+
$annotations,
34+
$docNode->getTagsByName('@dataProvider')
35+
);
36+
}
37+
38+
return $annotations;
39+
}
40+
41+
/**
42+
* @return RuleError[] errors
43+
*/
44+
public function processDataProvider(
45+
Scope $scope,
46+
PhpDocTagNode $phpDocTag,
47+
bool $checkFunctionNameCase
48+
): array
49+
{
50+
$dataProviderName = $this->getDataProviderName($phpDocTag);
51+
if ($dataProviderName === null) {
52+
// Missing name is already handled in NoMissingSpaceInMethodAnnotationRule
53+
return [];
54+
}
55+
56+
$classReflection = $scope->getClassReflection();
57+
if ($classReflection === null) {
58+
// Should not happen
59+
return [];
60+
}
61+
62+
try {
63+
$dataProviderMethodReflection = $classReflection->getNativeMethod($dataProviderName);
64+
} catch (MissingMethodFromReflectionException $missingMethodFromReflectionException) {
65+
$error = RuleErrorBuilder::message(sprintf(
66+
'@dataProvider %s related method not found.',
67+
$dataProviderName
68+
))->build();
69+
70+
return [$error];
71+
}
72+
73+
$errors = [];
74+
75+
if ($checkFunctionNameCase && $dataProviderName !== $dataProviderMethodReflection->getName()) {
76+
$errors[] = RuleErrorBuilder::message(sprintf(
77+
'@dataProvider %s related method is used with incorrect case: %s.',
78+
$dataProviderName,
79+
$dataProviderMethodReflection->getName()
80+
))->build();
81+
}
82+
83+
if (!$dataProviderMethodReflection->isPublic()) {
84+
$errors[] = RuleErrorBuilder::message(sprintf(
85+
'@dataProvider %s related method must be public.',
86+
$dataProviderName
87+
))->build();
88+
}
89+
90+
return $errors;
91+
}
92+
93+
private function getDataProviderName(PhpDocTagNode $phpDocTag): ?string
94+
{
95+
if (preg_match('/^[^ \t]+/', (string) $phpDocTag->value, $matches) !== 1) {
96+
return null;
97+
}
98+
99+
return $matches[0];
100+
}
101+
102+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace PHPStan\Rules\PHPUnit;
4+
5+
use PHPStan\Rules\Rule;
6+
use PHPStan\Testing\RuleTestCase;
7+
use PHPStan\Type\FileTypeMapper;
8+
9+
/**
10+
* @extends RuleTestCase<DataProviderDeclarationRule>
11+
*/
12+
class DataProviderDeclarationRuleTest extends RuleTestCase
13+
{
14+
15+
protected function getRule(): Rule
16+
{
17+
return new DataProviderDeclarationRule(
18+
new DataProviderHelper(),
19+
self::getContainer()->getByType(FileTypeMapper::class),
20+
true
21+
);
22+
}
23+
24+
public function testRule(): void
25+
{
26+
$this->analyse([__DIR__ . '/data/data-provider-declaration.php'], [
27+
[
28+
'@dataProvider providebaz related method is used with incorrect case: provideBaz.',
29+
13,
30+
],
31+
[
32+
'@dataProvider provideQuux related method must be public.',
33+
13,
34+
],
35+
[
36+
'@dataProvider provideNonExisting related method not found.',
37+
66,
38+
],
39+
]);
40+
}
41+
42+
/**
43+
* @return string[]
44+
*/
45+
public static function getAdditionalConfigFiles(): array
46+
{
47+
return [
48+
__DIR__ . '/../../../extension.neon',
49+
];
50+
}
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace ExampleTestCase;
4+
5+
class FooTestCase extends \PHPUnit\Framework\TestCase
6+
{
7+
/**
8+
* @dataProvider provideBar Comment.
9+
* @dataProvider providebaz
10+
* @dataProvider provideQux
11+
* @dataProvider provideQuux
12+
*/
13+
public function testIsNotFoo(string $subject): void
14+
{
15+
self::assertNotSame('foo', $subject);
16+
}
17+
18+
public static function provideBar(): iterable
19+
{
20+
return [
21+
['bar'],
22+
];
23+
}
24+
25+
public static function provideBaz(): iterable
26+
{
27+
return [
28+
['baz'],
29+
];
30+
}
31+
32+
public function provideQux(): iterable
33+
{
34+
return [
35+
['qux'],
36+
];
37+
}
38+
39+
protected static function provideQuux(): iterable
40+
{
41+
42+
return [
43+
['quux'],
44+
];
45+
}
46+
}
47+
48+
trait BarProvider
49+
{
50+
public static function provideCorge(): iterable
51+
{
52+
return [
53+
['corge'],
54+
];
55+
}
56+
}
57+
58+
class BarTestCase extends \PHPUnit\Framework\TestCase
59+
{
60+
use BarProvider;
61+
62+
/**
63+
* @dataProvider provideNonExisting
64+
* @dataProvider provideCorge
65+
*/
66+
public function testIsNotBar(string $subject): void
67+
{
68+
self::assertNotSame('bar', $subject);
69+
}
70+
}

0 commit comments

Comments
 (0)