From c52c41b9380fee59865f59a1e24f2a6337518cc9 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 27 Oct 2025 07:17:03 +0100 Subject: [PATCH 1/9] Ignore missingType.iterableValue for data-providers --- data-provider-iterable-value.php | 43 +++++++++++++++ extension.neon | 5 ++ src/Rules/PHPUnit/DataProviderHelper.php | 8 +-- .../DataProviderReturnTypeIgnoreExtension.php | 53 +++++++++++++++++++ ...aProviderReturnTypeIgnoreExtensionTest.php | 30 +++++++++++ .../data/data-provider-iterable-value.php | 31 +++++++++++ 6 files changed, 166 insertions(+), 4 deletions(-) create mode 100644 data-provider-iterable-value.php create mode 100644 src/Type/PHPUnit/DataProviderReturnTypeIgnoreExtension.php create mode 100644 tests/Type/PHPUnit/DataProviderReturnTypeIgnoreExtensionTest.php create mode 100644 tests/Type/PHPUnit/data/data-provider-iterable-value.php diff --git a/data-provider-iterable-value.php b/data-provider-iterable-value.php new file mode 100644 index 00000000..e9a26405 --- /dev/null +++ b/data-provider-iterable-value.php @@ -0,0 +1,43 @@ + */ public function getDataProviderMethods( Scope $scope, - $node, + $testMethod, ClassReflection $classReflection ): iterable { - yield from $this->yieldDataProviderAnnotations($node, $scope, $classReflection); + yield from $this->yieldDataProviderAnnotations($testMethod, $scope, $classReflection); if (!$this->phpunit10OrNewer) { return; } - yield from $this->yieldDataProviderAttributes($node, $classReflection); + yield from $this->yieldDataProviderAttributes($testMethod, $classReflection); } /** diff --git a/src/Type/PHPUnit/DataProviderReturnTypeIgnoreExtension.php b/src/Type/PHPUnit/DataProviderReturnTypeIgnoreExtension.php new file mode 100644 index 00000000..bb195b44 --- /dev/null +++ b/src/Type/PHPUnit/DataProviderReturnTypeIgnoreExtension.php @@ -0,0 +1,53 @@ +testMethodsHelper = $testMethodsHelper; + $this->dataProviderHelper = $dataProviderHelper; + } + + public function shouldIgnore(Error $error, Node $node, Scope $scope): bool + { + if (! $node instanceof InClassMethodNode) { // @phpstan-ignore phpstanApi.instanceofAssumption + return false; + } + + if ($error->getIdentifier() !== 'missingType.iterableValue') { + return false; + } + + $classReflection = $node->getClassReflection(); + $methodReflection = $node->getMethodReflection(); + $testMethods = $this->testMethodsHelper->getTestMethods($classReflection, $scope); + foreach ($testMethods as $testMethod) { + foreach ($this->dataProviderHelper->getDataProviderMethods($scope, $testMethod, $classReflection) as [, $providerMethodName]) { + if ($providerMethodName === $methodReflection->getName()) { + return true; + } + } + } + + return false; + } + +} diff --git a/tests/Type/PHPUnit/DataProviderReturnTypeIgnoreExtensionTest.php b/tests/Type/PHPUnit/DataProviderReturnTypeIgnoreExtensionTest.php new file mode 100644 index 00000000..eeb311fe --- /dev/null +++ b/tests/Type/PHPUnit/DataProviderReturnTypeIgnoreExtensionTest.php @@ -0,0 +1,30 @@ + + */ +class DataProviderReturnTypeIgnoreExtensionTest extends RuleTestCase { + protected function getRule(): Rule + { + /** @phpstan-ignore phpstanApi.classConstant */ + $rule = self::getContainer()->getByType(MissingMethodReturnTypehintRule::class); + return $rule; + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/data-provider-iterable-value.php'], [ + ]); + } + + static public function getAdditionalConfigFiles(): array + { + return [__DIR__ . '/../../../extension.neon']; + } +} diff --git a/tests/Type/PHPUnit/data/data-provider-iterable-value.php b/tests/Type/PHPUnit/data/data-provider-iterable-value.php new file mode 100644 index 00000000..528ab426 --- /dev/null +++ b/tests/Type/PHPUnit/data/data-provider-iterable-value.php @@ -0,0 +1,31 @@ + Date: Mon, 27 Oct 2025 07:18:38 +0100 Subject: [PATCH 2/9] Delete data-provider-iterable-value.php --- data-provider-iterable-value.php | 43 -------------------------------- 1 file changed, 43 deletions(-) delete mode 100644 data-provider-iterable-value.php diff --git a/data-provider-iterable-value.php b/data-provider-iterable-value.php deleted file mode 100644 index e9a26405..00000000 --- a/data-provider-iterable-value.php +++ /dev/null @@ -1,43 +0,0 @@ - Date: Mon, 27 Oct 2025 07:50:07 +0100 Subject: [PATCH 3/9] conditional tag --- extension.neon | 6 ++++-- rules.neon | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/extension.neon b/extension.neon index 7a51ef32..dcf43818 100644 --- a/extension.neon +++ b/extension.neon @@ -1,6 +1,7 @@ parameters: phpunit: convertUnionToIntersectionType: true + checkDataProviderData: %featureToggles.bleedingEdge% additionalConstructors: - PHPUnit\Framework\TestCase::setUp earlyTerminatingMethodCalls: @@ -24,6 +25,7 @@ parameters: parametersSchema: phpunit: structure([ convertUnionToIntersectionType: bool() + checkDataProviderData: bool(), ]) services: @@ -69,9 +71,9 @@ services: - class: PHPStan\Type\PHPUnit\DataProviderReturnTypeIgnoreExtension - tags: - - phpstan.ignoreErrorExtension conditionalTags: PHPStan\PhpDoc\PHPUnit\MockObjectTypeNodeResolverExtension: phpstan.phpDoc.typeNodeResolverExtension: %phpunit.convertUnionToIntersectionType% + PHPStan\Type\PHPUnit\DataProviderReturnTypeIgnoreExtension: + phpstan.ignoreErrorExtension: %phpunit.checkDataProviderData% diff --git a/rules.neon b/rules.neon index 63e10b47..8272f47a 100644 --- a/rules.neon +++ b/rules.neon @@ -14,7 +14,7 @@ conditionalTags: phpstan.rules.rule: [%strictRulesInstalled%, %featureToggles.bleedingEdge%] PHPStan\Rules\PHPUnit\DataProviderDataRule: - phpstan.rules.rule: %featureToggles.bleedingEdge% + phpstan.rules.rule: %phpunit.checkDataProviderData% services: - From 3ce0a23529d14c308e73d80fb6a0c39d1eaa0456 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 27 Oct 2025 08:11:40 +0100 Subject: [PATCH 4/9] added test --- .../PHPUnit/DataProviderReturnTypeIgnoreExtensionTest.php | 4 ++++ tests/Type/PHPUnit/data/data-provider-iterable-value.php | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/tests/Type/PHPUnit/DataProviderReturnTypeIgnoreExtensionTest.php b/tests/Type/PHPUnit/DataProviderReturnTypeIgnoreExtensionTest.php index eeb311fe..3dec302b 100644 --- a/tests/Type/PHPUnit/DataProviderReturnTypeIgnoreExtensionTest.php +++ b/tests/Type/PHPUnit/DataProviderReturnTypeIgnoreExtensionTest.php @@ -20,6 +20,10 @@ protected function getRule(): Rule public function testRule(): void { $this->analyse([__DIR__ . '/data/data-provider-iterable-value.php'], [ + [ + 'Method DataProviderIterableValueTest\Foo::notADataProvider() return type has no value type specified in iterable type iterable.', + 32 + ], ]); } diff --git a/tests/Type/PHPUnit/data/data-provider-iterable-value.php b/tests/Type/PHPUnit/data/data-provider-iterable-value.php index 528ab426..613d3b14 100644 --- a/tests/Type/PHPUnit/data/data-provider-iterable-value.php +++ b/tests/Type/PHPUnit/data/data-provider-iterable-value.php @@ -28,4 +28,12 @@ public function dataProvider2(): iterable { [$i, 2], ]; } + + public function notADataProvider(): iterable { + return [ + [1, 2], + [3, 4], + [5, 6], + ]; + } } From be6fe1e83189b437f0101d79612427bd37aa50b1 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 27 Oct 2025 08:19:55 +0100 Subject: [PATCH 5/9] fix --- .../PHPUnit/DataProviderReturnTypeIgnoreExtensionTest.php | 7 +++++-- tests/Type/PHPUnit/data/data-provider-iterable-value.neon | 6 ++++++ 2 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 tests/Type/PHPUnit/data/data-provider-iterable-value.neon diff --git a/tests/Type/PHPUnit/DataProviderReturnTypeIgnoreExtensionTest.php b/tests/Type/PHPUnit/DataProviderReturnTypeIgnoreExtensionTest.php index 3dec302b..1767c96b 100644 --- a/tests/Type/PHPUnit/DataProviderReturnTypeIgnoreExtensionTest.php +++ b/tests/Type/PHPUnit/DataProviderReturnTypeIgnoreExtensionTest.php @@ -22,13 +22,16 @@ public function testRule(): void $this->analyse([__DIR__ . '/data/data-provider-iterable-value.php'], [ [ 'Method DataProviderIterableValueTest\Foo::notADataProvider() return type has no value type specified in iterable type iterable.', - 32 + 32, + 'See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type' ], ]); } static public function getAdditionalConfigFiles(): array { - return [__DIR__ . '/../../../extension.neon']; + return [ + __DIR__ . '/data/data-provider-iterable-value.neon' + ]; } } diff --git a/tests/Type/PHPUnit/data/data-provider-iterable-value.neon b/tests/Type/PHPUnit/data/data-provider-iterable-value.neon new file mode 100644 index 00000000..eed12a5b --- /dev/null +++ b/tests/Type/PHPUnit/data/data-provider-iterable-value.neon @@ -0,0 +1,6 @@ +parameters: + phpunit: + checkDataProviderData: true + +includes: + - ../../../../extension.neon From c4a826d86c67e2b94880c08d2ea894ef40901d24 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 27 Oct 2025 08:36:14 +0100 Subject: [PATCH 6/9] Update DataProviderReturnTypeIgnoreExtensionTest.php --- tests/Type/PHPUnit/DataProviderReturnTypeIgnoreExtensionTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Type/PHPUnit/DataProviderReturnTypeIgnoreExtensionTest.php b/tests/Type/PHPUnit/DataProviderReturnTypeIgnoreExtensionTest.php index 1767c96b..3fcf57a5 100644 --- a/tests/Type/PHPUnit/DataProviderReturnTypeIgnoreExtensionTest.php +++ b/tests/Type/PHPUnit/DataProviderReturnTypeIgnoreExtensionTest.php @@ -14,6 +14,7 @@ protected function getRule(): Rule { /** @phpstan-ignore phpstanApi.classConstant */ $rule = self::getContainer()->getByType(MissingMethodReturnTypehintRule::class); + return $rule; } From 6e321dd8805c83b167a0f9090e0aac791edbd632 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 27 Oct 2025 08:39:11 +0100 Subject: [PATCH 7/9] move is-test-case check into TestMethodsHelper --- src/Rules/PHPUnit/DataProviderDataRule.php | 6 +----- src/Rules/PHPUnit/TestMethodsHelper.php | 5 +++++ .../PHPUnit/DataProviderReturnTypeIgnoreExtensionTest.php | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Rules/PHPUnit/DataProviderDataRule.php b/src/Rules/PHPUnit/DataProviderDataRule.php index 45a7fa16..6f250fee 100644 --- a/src/Rules/PHPUnit/DataProviderDataRule.php +++ b/src/Rules/PHPUnit/DataProviderDataRule.php @@ -8,7 +8,6 @@ use PHPStan\Rules\Rule; use PHPStan\Type\ObjectType; use PHPStan\Type\Type; -use PHPUnit\Framework\TestCase; use function array_slice; use function count; use function max; @@ -62,10 +61,7 @@ public function processNode(Node $node, Scope $scope): array $method = $scope->getFunction(); $classReflection = $scope->getClassReflection(); - if ( - $classReflection === null - || !$classReflection->is(TestCase::class) - ) { + if ($classReflection === null) { return []; } diff --git a/src/Rules/PHPUnit/TestMethodsHelper.php b/src/Rules/PHPUnit/TestMethodsHelper.php index d0984442..67b888e7 100644 --- a/src/Rules/PHPUnit/TestMethodsHelper.php +++ b/src/Rules/PHPUnit/TestMethodsHelper.php @@ -6,6 +6,7 @@ use PHPStan\PhpDoc\ResolvedPhpDocBlock; use PHPStan\Reflection\ClassReflection; use PHPStan\Type\FileTypeMapper; +use PHPUnit\Framework\TestCase; use ReflectionMethod; use function str_starts_with; use function strtolower; @@ -31,6 +32,10 @@ public function __construct( */ public function getTestMethods(ClassReflection $classReflection, Scope $scope): array { + if (!$classReflection->is(TestCase::class)) { + return []; + } + $testMethods = []; foreach ($classReflection->getNativeReflection()->getMethods() as $reflectionMethod) { if (!$reflectionMethod->isPublic()) { diff --git a/tests/Type/PHPUnit/DataProviderReturnTypeIgnoreExtensionTest.php b/tests/Type/PHPUnit/DataProviderReturnTypeIgnoreExtensionTest.php index 3fcf57a5..fb5b927a 100644 --- a/tests/Type/PHPUnit/DataProviderReturnTypeIgnoreExtensionTest.php +++ b/tests/Type/PHPUnit/DataProviderReturnTypeIgnoreExtensionTest.php @@ -14,7 +14,7 @@ protected function getRule(): Rule { /** @phpstan-ignore phpstanApi.classConstant */ $rule = self::getContainer()->getByType(MissingMethodReturnTypehintRule::class); - + return $rule; } From a9225a69d3207555f8cd4d89893e72332221a66d Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 27 Oct 2025 09:47:41 +0100 Subject: [PATCH 8/9] fix --- .../PHPUnit/DataProviderReturnTypeIgnoreExtension.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Type/PHPUnit/DataProviderReturnTypeIgnoreExtension.php b/src/Type/PHPUnit/DataProviderReturnTypeIgnoreExtension.php index bb195b44..d33035d3 100644 --- a/src/Type/PHPUnit/DataProviderReturnTypeIgnoreExtension.php +++ b/src/Type/PHPUnit/DataProviderReturnTypeIgnoreExtension.php @@ -6,7 +6,6 @@ use PHPStan\Analyser\Error; use PHPStan\Analyser\IgnoreErrorExtension; use PHPStan\Analyser\Scope; -use PHPStan\Node\InClassMethodNode; use PHPStan\Rules\PHPUnit\DataProviderHelper; use PHPStan\Rules\PHPUnit\TestMethodsHelper; @@ -28,7 +27,7 @@ public function __construct( public function shouldIgnore(Error $error, Node $node, Scope $scope): bool { - if (! $node instanceof InClassMethodNode) { // @phpstan-ignore phpstanApi.instanceofAssumption + if (!$scope->isInClass()) { return false; } @@ -36,8 +35,12 @@ public function shouldIgnore(Error $error, Node $node, Scope $scope): bool return false; } - $classReflection = $node->getClassReflection(); - $methodReflection = $node->getMethodReflection(); + $methodReflection = $scope->getFunction(); + if ($methodReflection === null) { + return false; + } + + $classReflection = $scope->getClassReflection(); $testMethods = $this->testMethodsHelper->getTestMethods($classReflection, $scope); foreach ($testMethods as $testMethod) { foreach ($this->dataProviderHelper->getDataProviderMethods($scope, $testMethod, $classReflection) as [, $providerMethodName]) { From 8b8376765250556065371abdbb9f46992d868470 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 27 Oct 2025 09:49:58 +0100 Subject: [PATCH 9/9] Update DataProviderReturnTypeIgnoreExtension.php --- src/Type/PHPUnit/DataProviderReturnTypeIgnoreExtension.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Type/PHPUnit/DataProviderReturnTypeIgnoreExtension.php b/src/Type/PHPUnit/DataProviderReturnTypeIgnoreExtension.php index d33035d3..be6af678 100644 --- a/src/Type/PHPUnit/DataProviderReturnTypeIgnoreExtension.php +++ b/src/Type/PHPUnit/DataProviderReturnTypeIgnoreExtension.php @@ -27,20 +27,20 @@ public function __construct( public function shouldIgnore(Error $error, Node $node, Scope $scope): bool { - if (!$scope->isInClass()) { + if ($error->getIdentifier() !== 'missingType.iterableValue') { return false; } - if ($error->getIdentifier() !== 'missingType.iterableValue') { + if (!$scope->isInClass()) { return false; } + $classReflection = $scope->getClassReflection(); $methodReflection = $scope->getFunction(); if ($methodReflection === null) { return false; } - $classReflection = $scope->getClassReflection(); $testMethods = $this->testMethodsHelper->getTestMethods($classReflection, $scope); foreach ($testMethods as $testMethod) { foreach ($this->dataProviderHelper->getDataProviderMethods($scope, $testMethod, $classReflection) as [, $providerMethodName]) {