diff --git a/src/Rules/PHPUnit/DataProviderDeclarationRule.php b/src/Rules/PHPUnit/DataProviderDeclarationRule.php index 612cf06..96fde37 100644 --- a/src/Rules/PHPUnit/DataProviderDeclarationRule.php +++ b/src/Rules/PHPUnit/DataProviderDeclarationRule.php @@ -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) { @@ -93,7 +98,8 @@ public function processNode(Node $node, Scope $scope): array $scope, $annotation, $this->checkFunctionNameCase, - $this->deprecationRulesInstalled + $this->deprecationRulesInstalled, + $testMethodReflection ) ); } diff --git a/src/Rules/PHPUnit/DataProviderHelper.php b/src/Rules/PHPUnit/DataProviderHelper.php index e88f293..c3455a1 100644 --- a/src/Rules/PHPUnit/DataProviderHelper.php +++ b/src/Rules/PHPUnit/DataProviderHelper.php @@ -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; @@ -45,7 +50,8 @@ public function processDataProvider( Scope $scope, PhpDocTagNode $phpDocTag, bool $checkFunctionNameCase, - bool $deprecationRulesInstalled + bool $deprecationRulesInstalled, + ?ExtendedMethodReflection $testMethodReflection ): array { $dataProviderName = $this->getDataProviderName($phpDocTag); @@ -95,6 +101,58 @@ public function processDataProvider( ))->build(); } + $dataProviderParameterAcceptor = ParametersAcceptorSelector::selectSingle($dataProviderMethodReflection->getVariants()); + $providerReturnType = $dataProviderParameterAcceptor->getReturnType(); + if ($testMethodReflection !== null && $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; } diff --git a/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php b/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php index cfd27ae..a36528b 100644 --- a/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php +++ b/tests/Rules/PHPUnit/DataProviderDeclarationRuleTest.php @@ -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> which is not compatible with the test method parameters.', + 101, + ], + [ + '@dataProvider provideIterator returns Iterator> 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 which is not compatible with the test method parameters.', + 116, + ], ]); } diff --git a/tests/Rules/PHPUnit/data/data-provider-declaration.php b/tests/Rules/PHPUnit/data/data-provider-declaration.php index 2690d02..c58c802 100644 --- a/tests/Rules/PHPUnit/data/data-provider-declaration.php +++ b/tests/Rules/PHPUnit/data/data-provider-declaration.php @@ -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> + */ + public static function provideArray(): iterable + { + return [ + ['bar'], + ]; + } + + /** + * @return \Iterator> + */ + public static function provideIterator(): \Iterator + { + yield ['bar']; + } + + /** + * @return \Iterator + */ + public static function provideMultiple(): \Iterator + { + yield ['bar', 1]; + } +}