Skip to content

Commit

Permalink
validate dataProvider return types
Browse files Browse the repository at this point in the history
  • Loading branch information
staabm committed Dec 7, 2022
1 parent f9bfc19 commit c8d5d0d
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 2 deletions.
8 changes: 7 additions & 1 deletion src/Rules/PHPUnit/DataProviderDeclarationRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ public function processNode(Node $node, Scope $scope): array

$annotations = $this->dataProviderHelper->getDataProviderAnnotations($methodPhpDoc);

$testMethodReflection = null;
if ($classReflection->hasMethod($node->name->toString())) {
$testMethodReflection = $classReflection->getMethod($node->name->toString(), $scope);
}

$errors = [];

foreach ($annotations as $annotation) {
Expand All @@ -93,7 +98,8 @@ public function processNode(Node $node, Scope $scope): array
$scope,
$annotation,
$this->checkFunctionNameCase,
$this->deprecationRulesInstalled
$this->deprecationRulesInstalled,
$testMethodReflection
)
);
}
Expand Down
60 changes: 59 additions & 1 deletion src/Rules/PHPUnit/DataProviderHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,15 @@
use PHPStan\Analyser\Scope;
use PHPStan\PhpDoc\ResolvedPhpDocBlock;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
use PHPStan\Reflection\ExtendedMethodReflection;
use PHPStan\Reflection\MissingMethodFromReflectionException;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Rules\RuleError;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\UnionType;
use PHPStan\Type\VerbosityLevel;
use function array_merge;
use function count;
use function preg_match;
use function sprintf;

Expand Down Expand Up @@ -45,7 +50,8 @@ public function processDataProvider(
Scope $scope,
PhpDocTagNode $phpDocTag,
bool $checkFunctionNameCase,
bool $deprecationRulesInstalled
bool $deprecationRulesInstalled,
?ExtendedMethodReflection $testMethodReflection
): array
{
$dataProviderName = $this->getDataProviderName($phpDocTag);
Expand Down Expand Up @@ -95,6 +101,58 @@ public function processDataProvider(
))->build();
}

$dataProviderParameterAcceptor = ParametersAcceptorSelector::selectSingle($dataProviderMethodReflection->getVariants());
$providerReturnType = $dataProviderParameterAcceptor->getReturnType();
if ($providerReturnType->isIterable()->yes()) {
$collectionType = $providerReturnType->getIterableValueType();

if ($collectionType->isIterable()->yes()) {
$testParameterAcceptor = ParametersAcceptorSelector::selectSingle($testMethodReflection->getVariants());

$valueType = $collectionType->getIterableValueType();

if ($valueType instanceof UnionType) {
if (count($valueType->getTypes()) !== count($testParameterAcceptor->getParameters())) {
$errors[] = RuleErrorBuilder::message(sprintf(
'@dataProvider %s returns a different number of values the test method expects.',
$dataProviderName
))->build();

return $errors;
}

foreach ($valueType->getTypes() as $i => $innerType) {
if (!$testParameterAcceptor->getParameters()[$i]->getType()->accepts($innerType, $scope->isDeclareStrictTypes())->yes()) {
$errors[] = RuleErrorBuilder::message(sprintf(
'@dataProvider %s returns %s which is not compatible with the test method parameters.',
$dataProviderName,
$providerReturnType->describe(VerbosityLevel::precise())
))->build();

return $errors;
}
}
} else {
if (count($testParameterAcceptor->getParameters()) !== 1) {
$errors[] = RuleErrorBuilder::message(sprintf(
'@dataProvider %s returns a different number of values the test method expects.',
$dataProviderName
))->build();

return $errors;
}

if (!$testParameterAcceptor->getParameters()[0]->getType()->accepts($valueType, $scope->isDeclareStrictTypes())->yes()) {
$errors[] = RuleErrorBuilder::message(sprintf(
'@dataProvider %s returns %s which is not compatible with the test method parameters.',
$dataProviderName,
$providerReturnType->describe(VerbosityLevel::precise())
))->build();
}
}
}
}

return $errors;
}

Expand Down
20 changes: 20 additions & 0 deletions tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,26 @@ public function testRule(): void
'@dataProvider provideNonExisting related method not found.',
66,
],
[
'@dataProvider provideMultiple returns a different number of values the test method expects.',
79,
],
[
'@dataProvider provideArray returns iterable<int, array<string>> which is not compatible with the test method parameters.',
101,
],
[
'@dataProvider provideIterator returns Iterator<mixed, array<int, string>> which is not compatible with the test method parameters.',
101,
],
[
'@dataProvider provideMultiple returns a different number of values the test method expects.',
101,
],
[
'@dataProvider provideMultiple returns Iterator<mixed, array{string, int}> which is not compatible with the test method parameters.',
116,
],
]);
}

Expand Down
76 changes: 76 additions & 0 deletions tests/Rules/PHPUnit/data/data-provider-declaration.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,79 @@ public function testIsNotBar(string $subject): void
self::assertNotSame('bar', $subject);
}
}

class FooBarTestCase extends \PHPUnit\Framework\TestCase
{
/**
* @dataProvider provideArray
* @dataProvider provideIterator
* @dataProvider provideMultiple
*/
public function testIsNotFooBar(string $subject): void
{
self::assertNotSame('foo', $subject);
}

/**
* @dataProvider provideArray
* @dataProvider provideIterator
*
* @param string $subject
*/
public function testIsNotFooBarPhpDoc($subject): void
{
self::assertNotSame('foo', $subject);
}


/**
* @dataProvider provideArray
* @dataProvider provideIterator
* @dataProvider provideMultiple
*/
public function testIsFooBar(int $subject): void
{
self::assertNotSame(123, $subject);
}

/**
* @dataProvider provideMultiple
*/
public function testMultipleParams(string $subject, int $i): void
{
}

/**
* @dataProvider provideMultiple
*/
public function testBogusMultipleParams(float $subject, string $i): void
{
}


/**
* @return list<array<string>>
*/
public static function provideArray(): iterable
{
return [
['bar'],
];
}

/**
* @return \Iterator<list<string>>
*/
public static function provideIterator(): \Iterator
{
yield ['bar'];
}

/**
* @return \Iterator<array{string, int}>
*/
public static function provideMultiple(): \Iterator
{
yield ['bar', 1];
}
}

0 comments on commit c8d5d0d

Please sign in to comment.