Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 4c06b7e

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

File tree

6 files changed

+321
-0
lines changed

6 files changed

+321
-0
lines changed

‎extension.neon‎

Lines changed: 2 additions & 0 deletions
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‎

Lines changed: 6 additions & 0 deletions
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:
Lines changed: 90 additions & 0 deletions
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+
}
Lines changed: 102 additions & 0 deletions
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+
}
Lines changed: 51 additions & 0 deletions
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+
}
Lines changed: 70 additions & 0 deletions
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)

AltStyle によって変換されたページ (->オリジナル) /