Skip to content

Commit dedef50

Browse files
committed
PhpUnit provider: support @dataProvider
1 parent de2d776 commit dedef50

File tree

3 files changed

+93
-13
lines changed

3 files changed

+93
-13
lines changed

src/Provider/PhpUnitEntrypointProvider.php

Lines changed: 72 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
namespace ShipMonk\PHPStan\DeadCode\Provider;
44

5+
use PHPStan\PhpDocParser\Lexer\Lexer;
6+
use PHPStan\PhpDocParser\Parser\PhpDocParser;
7+
use PHPStan\PhpDocParser\Parser\TokenIterator;
58
use PHPUnit\Framework\Attributes\DataProvider;
69
use PHPUnit\Framework\TestCase;
710
use ReflectionMethod;
@@ -17,9 +20,15 @@ class PhpUnitEntrypointProvider implements EntrypointProvider
1720

1821
private bool $enabled;
1922

20-
public function __construct(bool $enabled)
23+
private PhpDocParser $phpDocParser;
24+
25+
private Lexer $lexer;
26+
27+
public function __construct(bool $enabled, PhpDocParser $phpDocParser, Lexer $lexer)
2128
{
2229
$this->enabled = $enabled;
30+
$this->lexer = $lexer;
31+
$this->phpDocParser = $phpDocParser;
2332
}
2433

2534
public function isEntrypoint(ReflectionMethod $method): bool
@@ -28,6 +37,8 @@ public function isEntrypoint(ReflectionMethod $method): bool
2837
return false;
2938
}
3039

40+
$this->gatherDataProviders($method);
41+
3142
return $this->isTestCaseMethod($method)
3243
|| $this->isDataProviderMethod($method);
3344
}
@@ -40,21 +51,73 @@ private function isTestCaseMethod(ReflectionMethod $method): bool
4051

4152
private function isDataProviderMethod(ReflectionMethod $originalMethod): bool
4253
{
54+
if (!$originalMethod->getDeclaringClass()->isSubclassOf(TestCase::class)) {
55+
return false;
56+
}
57+
58+
$declaringClass = $originalMethod->getDeclaringClass();
59+
$declaringClassName = $declaringClass->getName();
60+
61+
return $this->dataProviders[$declaringClassName][$originalMethod->getName()] ?? false;
62+
}
63+
64+
private function gatherDataProviders(ReflectionMethod $originalMethod): void
65+
{
66+
if (!$originalMethod->getDeclaringClass()->isSubclassOf(TestCase::class)) {
67+
return;
68+
}
69+
4370
$declaringClass = $originalMethod->getDeclaringClass();
4471
$declaringClassName = $declaringClass->getName();
4572

46-
if (!isset($this->dataProviders[$declaringClassName])) {
47-
foreach ($declaringClass->getMethods() as $method) {
48-
foreach ($method->getAttributes(DataProvider::class) as $providerAttributeReflection) {
49-
/** @var DataProvider $providerAttribute */
50-
$providerAttribute = $providerAttributeReflection->newInstance();
73+
if (isset($this->dataProviders[$declaringClassName])) {
74+
return;
75+
}
76+
77+
foreach ($declaringClass->getMethods() as $method) {
78+
if ($method->getDeclaringClass()->getName() !== $declaringClassName) {
79+
continue; // dont iterate parents
80+
}
81+
82+
foreach ($this->getDataProvidersFromAnnotations($method->getDocComment()) as $dataProvider) {
83+
$this->dataProviders[$declaringClassName][$dataProvider] = true;
84+
}
5185

52-
$this->dataProviders[$declaringClassName][$providerAttribute->methodName()] = true;
53-
}
86+
foreach ($this->getDataProvidersFromAttributes($method) as $dataProvider) {
87+
$this->dataProviders[$declaringClassName][$dataProvider] = true;
5488
}
5589
}
90+
}
5691

57-
return $this->dataProviders[$declaringClassName][$originalMethod->getName()] ?? false;
92+
/**
93+
* @param false|string $rawPhpDoc
94+
* @return iterable<string>
95+
*/
96+
private function getDataProvidersFromAnnotations($rawPhpDoc): iterable
97+
{
98+
if ($rawPhpDoc === false) {
99+
return;
100+
}
101+
102+
$tokens = new TokenIterator($this->lexer->tokenize($rawPhpDoc));
103+
$phpDoc = $this->phpDocParser->parse($tokens);
104+
105+
foreach ($phpDoc->getTagsByName('@dataProvider') as $tag) {
106+
yield (string) $tag->value;
107+
}
108+
}
109+
110+
/**
111+
* @return iterable<string>
112+
*/
113+
private function getDataProvidersFromAttributes(ReflectionMethod $method): iterable
114+
{
115+
foreach ($method->getAttributes(DataProvider::class) as $providerAttributeReflection) {
116+
/** @var DataProvider $providerAttribute */
117+
$providerAttribute = $providerAttributeReflection->newInstance();
118+
119+
yield $providerAttribute->methodName();
120+
}
58121
}
59122

60123
}

tests/Rule/DeadMethodRuleTest.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
use PhpParser\Node;
66
use PHPStan\Collectors\Collector;
7+
use PHPStan\PhpDocParser\Lexer\Lexer;
8+
use PHPStan\PhpDocParser\Parser\PhpDocParser;
79
use PHPStan\Reflection\ReflectionProvider;
810
use PHPUnit\Framework\Attributes\DataProvider;
911
use ReflectionMethod;
@@ -45,7 +47,11 @@ public function isEntrypoint(ReflectionMethod $method): bool
4547
self::getContainer()->getByType(ReflectionProvider::class),
4648
enabled: true,
4749
),
48-
new PhpUnitEntrypointProvider(enabled: true),
50+
new PhpUnitEntrypointProvider(
51+
true,
52+
self::getContainer()->getByType(PhpDocParser::class),
53+
self::getContainer()->getByType(Lexer::class),
54+
),
4955
new SymfonyEntrypointProvider(enabled: true),
5056
];
5157
return [

tests/Rule/data/DeadMethodRule/providers/phpunit.php

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,24 @@
88
class SomeTest extends TestCase
99
{
1010

11-
12-
#[DataProvider('provide')]
11+
#[DataProvider('provideFromAttribute')]
1312
public function testFoo(string $arg): void
1413
{
1514
}
1615

17-
public static function provide(): array
16+
/**
17+
* @dataProvider provideFromPhpDoc
18+
*/
19+
public function testBar(string $arg): void
20+
{
21+
}
22+
23+
public static function provideFromAttribute(): array
24+
{
25+
return [];
26+
}
27+
28+
public static function provideFromPhpDoc(): array
1829
{
1930
return [];
2031
}

0 commit comments

Comments
 (0)