Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
parameters:
phpunit:
convertUnionToIntersectionType: true
checkDataProviderData: %featureToggles.bleedingEdge%
additionalConstructors:
- PHPUnit\Framework\TestCase::setUp
earlyTerminatingMethodCalls:
Expand All @@ -24,6 +25,7 @@ parameters:
parametersSchema:
phpunit: structure([
convertUnionToIntersectionType: bool()
checkDataProviderData: bool(),
])

services:
Expand Down Expand Up @@ -67,6 +69,11 @@ services:
arguments:
parser: @defaultAnalysisParser

-
class: PHPStan\Type\PHPUnit\DataProviderReturnTypeIgnoreExtension

conditionalTags:
PHPStan\PhpDoc\PHPUnit\MockObjectTypeNodeResolverExtension:
phpstan.phpDoc.typeNodeResolverExtension: %phpunit.convertUnionToIntersectionType%
PHPStan\Type\PHPUnit\DataProviderReturnTypeIgnoreExtension:
phpstan.ignoreErrorExtension: %phpunit.checkDataProviderData%
2 changes: 1 addition & 1 deletion rules.neon
Original file line number Diff line number Diff line change
Expand Up @@ -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:
-
Expand Down
6 changes: 1 addition & 5 deletions src/Rules/PHPUnit/DataProviderDataRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 [];
}

Expand Down
8 changes: 4 additions & 4 deletions src/Rules/PHPUnit/DataProviderHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,23 +53,23 @@ public function __construct(
}

/**
* @param ReflectionMethod|ClassMethod $node
* @param ReflectionMethod|ClassMethod $testMethod
*
* @return iterable<array{ClassReflection|null, string, int}>
*/
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);
}

/**
Expand Down
5 changes: 5 additions & 0 deletions src/Rules/PHPUnit/TestMethodsHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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()) {
Expand Down
53 changes: 53 additions & 0 deletions src/Type/PHPUnit/DataProviderReturnTypeIgnoreExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\PHPUnit;

use PhpParser\Node;
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;

final class DataProviderReturnTypeIgnoreExtension implements IgnoreErrorExtension
{

private TestMethodsHelper $testMethodsHelper;

private DataProviderHelper $dataProviderHelper;

public function __construct(
TestMethodsHelper $testMethodsHelper,
DataProviderHelper $dataProviderHelper
)
{
$this->testMethodsHelper = $testMethodsHelper;
$this->dataProviderHelper = $dataProviderHelper;
}

public function shouldIgnore(Error $error, Node $node, Scope $scope): bool
{
if (! $node instanceof InClassMethodNode) { // @phpstan-ignore phpstanApi.instanceofAssumption
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assumes an implementation detail that missingType.iterableValue is reported by a rule that hooks onto InClassMethodNode.

You can remove this instanceof and just ask for $scope->isInClass(), $scope->getFunction() etc.

Copy link
Contributor Author

@staabm staabm Oct 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got this code from the docs :).

doc-fix in phpstan/phpstan#13726

thank you.

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;
}

}
38 changes: 38 additions & 0 deletions tests/Type/PHPUnit/DataProviderReturnTypeIgnoreExtensionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\PHPUnit;

use PHPStan\Rules\Methods\MissingMethodReturnTypehintRule;
use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;

/**
* @extends RuleTestCase<MissingMethodReturnTypehintRule>
*/
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'], [
[
'Method DataProviderIterableValueTest\Foo::notADataProvider() return type has no value type specified in iterable type iterable.',
32,
'See: https://phpstan.org/blog/solving-phpstan-no-value-type-specified-in-iterable-type'
],
]);
}

static public function getAdditionalConfigFiles(): array
{
return [
__DIR__ . '/data/data-provider-iterable-value.neon'
];
}
}
6 changes: 6 additions & 0 deletions tests/Type/PHPUnit/data/data-provider-iterable-value.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
parameters:
phpunit:
checkDataProviderData: true

includes:
- ../../../../extension.neon
39 changes: 39 additions & 0 deletions tests/Type/PHPUnit/data/data-provider-iterable-value.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

namespace DataProviderIterableValueTest;

use PHPUnit\Framework\TestCase;

class Foo extends TestCase {
/**
* @dataProvider dataProvider
* @dataProvider dataProvider2
*/
public function testFoo():void {

}

public function dataProvider(): iterable {
return [
[1, 2],
[3, 4],
[5, 6],
];
}

public function dataProvider2(): iterable {
$i = rand(0, 10);

return [
[$i, 2],
];
}

public function notADataProvider(): iterable {
return [
[1, 2],
[3, 4],
[5, 6],
];
}
}
Loading