Skip to content

Commit 1dd180c

Browse files
committed
Implement reflection based dataprovider detection
1 parent ff6843e commit 1dd180c

File tree

4 files changed

+144
-3
lines changed

4 files changed

+144
-3
lines changed

extension.neon

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ services:
4949
class: PHPStan\Rules\PHPUnit\CoversHelper
5050
-
5151
class: PHPStan\Rules\PHPUnit\AnnotationHelper
52+
53+
-
54+
class: PHPStan\Rules\PHPUnit\TestMethodsHelper
55+
arguments:
56+
parser: @defaultAnalysisParser
57+
5258
-
5359
class: PHPStan\Rules\PHPUnit\PHPUnitVersionDetector
5460

src/Rules/PHPUnit/DataProviderDataRule.php

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,15 @@ class DataProviderDataRule implements Rule
1717
{
1818
private ReflectionProvider $reflectionProvider;
1919

20+
private TestMethodsHelper $testMethodsHelper;
21+
2022
public function __construct(
2123
ReflectionProvider $reflectionProvider,
24+
TestMethodsHelper $testMethodsHelper,
2225
)
2326
{
2427
$this->reflectionProvider = $reflectionProvider;
28+
$this->testMethodsHelper = $testMethodsHelper;
2529
}
2630

2731
public function getNodeType(): string
@@ -58,7 +62,22 @@ public function processNode(Node $node, Scope $scope): array
5862
return [];
5963
}
6064

61-
// XXX check whether the method is used as a data provider
65+
$testsWithProvider = [];
66+
$testMethods = $this->testMethodsHelper->getTestMethods($classReflection);
67+
foreach($testMethods as $testMethod)
68+
{
69+
foreach($this->testMethodsHelper->getDataProviderMethods($scope, $testMethod, $classReflection) as [$providerMethod])
70+
{
71+
if ($providerMethod === $method->getName()) {
72+
$testsWithProvider[] = $testMethod;
73+
continue 2;
74+
}
75+
}
76+
}
77+
78+
if (count($testsWithProvider) === 0) {
79+
return [];
80+
}
6281

6382
foreach($node->expr->items as $item) {
6483
if (!$item->value instanceof Node\Expr\Array_) {
@@ -69,7 +88,7 @@ public function processNode(Node $node, Scope $scope): array
6988
$var = new Node\Expr\New_(new Node\Name($classReflection->getName()));
7089
$scope->invokeNodeCallback(new Node\Expr\MethodCall(
7190
$var,
72-
'testTrim',
91+
$testsWithProvider[0]->getName(),
7392
$args,
7493
['startLine' => $item->getStartLine()]
7594
));
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Rules\PHPUnit;
4+
5+
use PhpParser\Node;
6+
use PhpParser\Node\Name;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Node\ClassMethod;
9+
use PHPStan\Parser\Parser;
10+
use PHPStan\PhpDoc\ResolvedPhpDocBlock;
11+
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
12+
use PHPStan\Reflection\ClassReflection;
13+
use PHPStan\Reflection\ReflectionProvider;
14+
use PHPStan\Rules\IdentifierRuleError;
15+
use PHPStan\Rules\RuleErrorBuilder;
16+
use PHPStan\Type\FileTypeMapper;
17+
use ReflectionMethod;
18+
use function array_merge;
19+
use function explode;
20+
use function sprintf;
21+
use function strpos;
22+
23+
final class TestMethodsHelper
24+
{
25+
private ReflectionProvider $reflectionProvider;
26+
27+
private FileTypeMapper $fileTypeMapper;
28+
29+
private Parser $parser;
30+
31+
public function __construct(
32+
ReflectionProvider $reflectionProvider,
33+
FileTypeMapper $fileTypeMapper,
34+
Parser $parser
35+
)
36+
{
37+
$this->reflectionProvider = $reflectionProvider;
38+
$this->fileTypeMapper = $fileTypeMapper;
39+
$this->parser = $parser;
40+
}
41+
42+
/**
43+
* @return array<ReflectionMethod>
44+
*/
45+
public function getTestMethods(ClassReflection $class): array
46+
{
47+
$testMethods = [];
48+
foreach($class->getNativeReflection()->getMethods() as $reflectionMethod) {
49+
if (str_starts_with(strtolower($reflectionMethod->getName()), 'test')) {
50+
$testMethods[] = $reflectionMethod;
51+
continue;
52+
}
53+
54+
// todo: detect tests with @test annotation
55+
56+
$testAttributes = $reflectionMethod->getAttributes('PHPUnit\Framework\Attribute\Test');
57+
if ($testAttributes !== []) {
58+
$testMethods[] = $reflectionMethod;
59+
}
60+
}
61+
62+
return $testMethods;
63+
}
64+
65+
/**
66+
* @return iterable<array{string}>
67+
*/
68+
public function getDataProviderMethods(
69+
Scope $scope,
70+
ReflectionMethod $node,
71+
ClassReflection $classReflection
72+
): iterable
73+
{
74+
/*
75+
$docComment = $node->getDocComment();
76+
if ($docComment !== null) {
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+
foreach ($this->getDataProviderAnnotations($methodPhpDoc) as $annotation) {
85+
$dataProviderValue = $this->getDataProviderAnnotationValue($annotation);
86+
if ($dataProviderValue === null) {
87+
// Missing value is already handled in NoMissingSpaceInMethodAnnotationRule
88+
continue;
89+
}
90+
91+
$dataProviderMethod = $this->parseDataProviderAnnotationValue($scope, $dataProviderValue);
92+
$dataProviderMethod[] = $node->getStartLine();
93+
94+
yield $dataProviderValue => $dataProviderMethod;
95+
}
96+
}
97+
98+
if (!$this->phpunit10OrNewer) {
99+
return;
100+
}
101+
*/
102+
103+
foreach ($node->getAttributes('PHPUnit\Framework\Attributes\DataProvider') as $attr) {
104+
$args = $attr->getArguments();
105+
if (count($args) !== 1) {
106+
continue;
107+
}
108+
109+
yield [$args[0]];
110+
}
111+
}
112+
113+
}

tests/Rules/PHPUnit/DataProviderDataRuleTest.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@
1010
use PHPStan\Rules\NullsafeCheck;
1111
use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper;
1212
use PHPStan\Rules\PHPUnit\DataProviderDataRule;
13+
use PHPStan\Rules\PHPUnit\TestMethodsHelper;
1314
use PHPStan\Rules\Properties\PropertyReflectionFinder;
1415
use PHPStan\Rules\Rule;
1516
use PHPStan\Rules\RuleLevelHelper;
1617
use PHPStan\Testing\RuleTestCase;
18+
use PHPStan\Type\FileTypeMapper;
1719

1820
/**
1921
* @extends RuleTestCase<DataProviderDataRule>
@@ -30,7 +32,8 @@ protected function getRule(): Rule
3032

3133
return new CompositeRule(new DirectRegistry([
3234
new DataProviderDataRule(
33-
$reflectionProvider
35+
$reflectionProvider,
36+
new TestMethodsHelper($reflectionProvider, self::getContainer()->getByType(FileTypeMapper::class), self::getContainer()->getService('defaultAnalysisParser'))
3437
),
3538
new CallMethodsRule(
3639
new MethodCallCheck($reflectionProvider, $ruleLevelHelper, true, true),

0 commit comments

Comments
 (0)