diff --git a/src/Hooks/TestCaseHandler.php b/src/Hooks/TestCaseHandler.php index eb20931..273868a 100644 --- a/src/Hooks/TestCaseHandler.php +++ b/src/Hooks/TestCaseHandler.php @@ -20,6 +20,7 @@ use Psalm\Plugin\Hook\AfterCodebasePopulatedInterface; use Psalm\StatementsSource; use Psalm\Storage\ClassLikeStorage; use Psalm\Type; +use Psalm\Type\Atomic\TNull; use RuntimeException; class TestCaseHandler implements @@ -230,22 +231,42 @@ class TestCaseHandler implements Type::getArray(), ]); + $non_null_provider_return_types = []; foreach (self::getAtomics($provider_return_type) as $type) { + // PHPUnit allows returning null from providers and treats it as an empty set + // resulting in the test being skipped + if ($type instanceof TNull) { + continue; + } + if (!$type->isIterable($codebase)) { IssueBuffer::accepts(new Issue\InvalidReturnType( 'Providers must return ' . $expected_provider_return_type->getId() . ', ' . $provider_return_type_string . ' provided', $provider_return_type_location )); - continue; + continue 2; } + $non_null_provider_return_types[] = $type; + } + + if ([] === $non_null_provider_return_types) { + IssueBuffer::accepts(new Issue\InvalidReturnType( + 'Providers must return ' . $expected_provider_return_type->getId() + . ', ' . $provider_return_type_string . ' provided', + $provider_return_type_location + )); + continue; } // unionize iterable so that instead of array|Traversable // we get iterable // // TODO: this may get implemented in a future Psalm version, remove it then - $provider_return_type = self::unionizeIterables($codebase, $provider_return_type); + $provider_return_type = self::unionizeIterables( + $codebase, + new Type\Union($non_null_provider_return_types) + ); $provider_key_type_is_compatible = $codebase->isTypeContainedByType( $provider_return_type->type_params[0], diff --git a/tests/acceptance/TestCase.feature b/tests/acceptance/TestCase.feature index 50162bd..dd36870 100644 --- a/tests/acceptance/TestCase.feature +++ b/tests/acceptance/TestCase.feature @@ -1261,3 +1261,36 @@ Feature: TestCase Then I see these errors | Type | Message | | InvalidArgument | Argument 1 of NS\MyTestCase::testSomething expects int, string provided by NS\MyTestCase::provide():(iterable>) | + + Scenario: Providers returning nullable generator are ok + Given I have the following code + """ + class MyTestCase extends TestCase { + /** @return ?\Generator> */ + public function provide(): ?\Generator { + return null; + } + /** @dataProvider provide */ + public function testSomething(int $_p): void {} + } + """ + When I run Psalm + Then I see no errors + + Scenario: Providers returning null are flagged + Given I have the following code + """ + class MyTestCase extends TestCase { + /** @return null */ + public function provide() { + return null; + } + /** @dataProvider provide */ + public function testSomething(): void {} + } + """ + When I run Psalm + Then I see these errors + | Type | Message | + | InvalidReturnType | Providers must return iterable>, null provided | + And I see no other errors