Skip to content

Commit

Permalink
Discover data providers from other classes
Browse files Browse the repository at this point in the history
  • Loading branch information
villfa authored Dec 12, 2022
1 parent 008f5da commit b9827cf
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 18 deletions.
63 changes: 50 additions & 13 deletions src/Rules/PHPUnit/DataProviderHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,32 @@
use PHPStan\Analyser\Scope;
use PHPStan\PhpDoc\ResolvedPhpDocBlock;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\MissingMethodFromReflectionException;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Rules\RuleError;
use PHPStan\Rules\RuleErrorBuilder;
use function array_merge;
use function count;
use function explode;
use function preg_match;
use function sprintf;

class DataProviderHelper
{

/**
* Reflection provider.
*
* @var ReflectionProvider
*/
private $reflectionProvider;

public function __construct(ReflectionProvider $reflectionProvider)
{
$this->reflectionProvider = $reflectionProvider;
}

/**
* @return array<PhpDocTagNode>
*/
Expand Down Expand Up @@ -48,57 +64,61 @@ public function processDataProvider(
bool $deprecationRulesInstalled
): array
{
$dataProviderName = $this->getDataProviderName($phpDocTag);
if ($dataProviderName === null) {
// Missing name is already handled in NoMissingSpaceInMethodAnnotationRule
$dataProviderValue = $this->getDataProviderValue($phpDocTag);
if ($dataProviderValue === null) {
// Missing value is already handled in NoMissingSpaceInMethodAnnotationRule
return [];
}

$classReflection = $scope->getClassReflection();
[$classReflection, $method] = $this->parseDataProviderValue($scope, $dataProviderValue);
if ($classReflection === null) {
// Should not happen
return [];
$error = RuleErrorBuilder::message(sprintf(
'@dataProvider %s related class not found.',
$dataProviderValue
))->build();

return [$error];
}

try {
$dataProviderMethodReflection = $classReflection->getNativeMethod($dataProviderName);
$dataProviderMethodReflection = $classReflection->getNativeMethod($method);
} catch (MissingMethodFromReflectionException $missingMethodFromReflectionException) {
$error = RuleErrorBuilder::message(sprintf(
'@dataProvider %s related method not found.',
$dataProviderName
$dataProviderValue
))->build();

return [$error];
}

$errors = [];

if ($checkFunctionNameCase && $dataProviderName !== $dataProviderMethodReflection->getName()) {
if ($checkFunctionNameCase && $method !== $dataProviderMethodReflection->getName()) {
$errors[] = RuleErrorBuilder::message(sprintf(
'@dataProvider %s related method is used with incorrect case: %s.',
$dataProviderName,
$dataProviderValue,
$dataProviderMethodReflection->getName()
))->build();
}

if (!$dataProviderMethodReflection->isPublic()) {
$errors[] = RuleErrorBuilder::message(sprintf(
'@dataProvider %s related method must be public.',
$dataProviderName
$dataProviderValue
))->build();
}

if ($deprecationRulesInstalled && !$dataProviderMethodReflection->isStatic()) {
$errors[] = RuleErrorBuilder::message(sprintf(
'@dataProvider %s related method must be static.',
$dataProviderName
$dataProviderValue
))->build();
}

return $errors;
}

private function getDataProviderName(PhpDocTagNode $phpDocTag): ?string
private function getDataProviderValue(PhpDocTagNode $phpDocTag): ?string
{
if (preg_match('/^[^ \t]+/', (string) $phpDocTag->value, $matches) !== 1) {
return null;
Expand All @@ -107,4 +127,21 @@ private function getDataProviderName(PhpDocTagNode $phpDocTag): ?string
return $matches[0];
}

/**
* @return array{ClassReflection|null, string}
*/
private function parseDataProviderValue(Scope $scope, string $dataProviderValue): array
{
$parts = explode('::', $dataProviderValue, 2);
if (count($parts) <= 1) {
return [$scope->getClassReflection(), $dataProviderValue];
}

if ($this->reflectionProvider->hasClass($parts[0])) {
return [$this->reflectionProvider->getClass($parts[0]), $parts[1]];
}

return [null, $dataProviderValue];
}

}
16 changes: 11 additions & 5 deletions tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ class DataProviderDeclarationRuleTest extends RuleTestCase

protected function getRule(): Rule
{
$reflection = $this->createReflectionProvider();

return new DataProviderDeclarationRule(
new DataProviderHelper(),
new DataProviderHelper($reflection),
self::getContainer()->getByType(FileTypeMapper::class),
true,
true
Expand All @@ -27,19 +29,23 @@ public function testRule(): void
$this->analyse([__DIR__ . '/data/data-provider-declaration.php'], [
[
'@dataProvider providebaz related method is used with incorrect case: provideBaz.',
13,
14,
],
[
'@dataProvider provideQux related method must be static.',
13,
14,
],
[
'@dataProvider provideQuux related method must be public.',
13,
14,
],
[
'@dataProvider provideNonExisting related method not found.',
66,
68,
],
[
'@dataProvider NonExisting::provideNonExisting related class not found.',
68,
],
]);
}
Expand Down
9 changes: 9 additions & 0 deletions tests/Rules/PHPUnit/data/data-provider-declaration.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class FooTestCase extends \PHPUnit\Framework\TestCase
* @dataProvider providebaz
* @dataProvider provideQux
* @dataProvider provideQuux
* @dataProvider \ExampleTestCase\BarTestCase::provideToOtherClass
*/
public function testIsNotFoo(string $subject): void
{
Expand Down Expand Up @@ -61,10 +62,18 @@ class BarTestCase extends \PHPUnit\Framework\TestCase

/**
* @dataProvider provideNonExisting
* @dataProvider NonExisting::provideNonExisting
* @dataProvider provideCorge
*/
public function testIsNotBar(string $subject): void
{
self::assertNotSame('bar', $subject);
}

public static function provideToOtherClass(): iterable
{
return [
['toOtherClass'],
];
}
}

0 comments on commit b9827cf

Please sign in to comment.