Allow yield with nullable/union return types by checking each union member for an iterable, non-array part#5903
Open
phpstan-bot wants to merge 1 commit into
Conversation
… member for an iterable, non-array part - `YieldInGeneratorRule` now flattens the declared return type and accepts it when at least one member is iterable and not an array (Generator, Iterator, Traversable, iterable). This mirrors PHP, which permits `?Generator`, `Iterator|float` and similar declarations for generator functions instead of requiring the whole return type to be iterable. - Added `GeneratorReturnTypeHelper::getGeneratorType()` which extracts only the iterable, non-array part of the return type. Without it, the non-iterable members (null, float, ...) collapse `getIterableKeyType()`/`getIterableValueType()` to `ErrorType`, which silently accepts every yielded key/value. - `YieldTypeRule` and `YieldFromTypeRule` use the helper so yielded key/value types (and the delegated `TSend` type) are still validated for nullable/union generator return types. - Updated `yield-in-generator.php` expectations: `\Generator|int` and `\Generator|array` are no longer reported, and added nullable/union regression cases plus a still-invalid `string|null` case.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
PHPStan reported
Yield can be used only with these return types: Generator, Iterator, Traversable, iterable.for generator functions declared with a nullable or union return type such as?Generator,\Generator|intorIterator|float. These declarations are valid PHP — a function containingyieldalways returns aGeneratorat runtime, and PHP only requires that one of the declared union members can hold it. This PR makes the generator rules treat the return type per-member instead of as a whole.Changes
src/Rules/Generators/YieldInGeneratorRule.php: flatten the declared return type viaTypeUtils::flattenTypes()and accept it when any member is iterable and not an array (i.e. can hold aGenerator), OR-combining the per-member result. Previously the whole-typeisIterable()returnedmaybefor?Generator, causing a false positive underreportMaybes.src/Rules/Generators/GeneratorReturnTypeHelper.php(new):getGeneratorType()extracts the iterable, non-array part of a return type.src/Rules/Generators/YieldTypeRule.phpandsrc/Rules/Generators/YieldFromTypeRule.php: run the declared return type throughGeneratorReturnTypeHelper::getGeneratorType()before computinggetIterableKeyType()/getIterableValueType()and the delegatedTSendtemplate type.Root cause
Two related problems, both stemming from treating a generator's declared return type as a single iterable type:
False positive in
YieldInGeneratorRule. The rule applied$returnType->isIterable()to the entire type. For a union likeGenerator|nullthis ismaybe(becausenullis not iterable), so the rule emitted an error. The correct rule — matching PHP's compiler — is that the type is valid if at least one member is iterable-and-not-array.Silent loss of yield key/value checking for nullable/union generators.
UnionType::getIterableValueType()unions the per-member iterable value types, and the non-iterable members (null,float, …) contributeErrorTypeviaNonIterableTypeTrait.TypeCombinator::union(Food, ErrorType)collapses to*ERROR*, whichaccepts()everything — soyield <wrong-type>inside a?Generator<int, Food>was never reported. The newGeneratorReturnTypeHelperstrips the non-iterable/array members first, so the realGeneratortype drives the check.Test
tests/PHPStan/Rules/Generators/data/yield-in-generator.php+YieldInGeneratorRuleTest:\Generator|intand\Generator|arrayare no longer reported; added\Generator|null,\Iterator|float,\Traversable|null,iterable|null(all valid, no error) andstring|null(still invalid, reported).tests/PHPStan/Rules/Generators/data/bug-6190.php+YieldTypeRuleTest::testBug6190: a?Generator<int, Food>and anIterator<int, Food>|floatstill report wrong yielded value/key types.tests/PHPStan/Rules/Generators/data/bug-6190-from.php+YieldFromTypeRuleTest::testBug6190:yield frominto a?Generator<int, Food>still reports a wrong delegated value type.All three regression tests were confirmed to fail before the corresponding fix and pass after.
Fixes phpstan/phpstan#6190