Skip to content

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
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-wmsurx3
Open

Allow yield with nullable/union return types by checking each union member for an iterable, non-array part#5903
phpstan-bot wants to merge 1 commit into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-wmsurx3

Conversation

@phpstan-bot

Copy link
Copy Markdown
Collaborator

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|int or Iterator|float. These declarations are valid PHP — a function containing yield always returns a Generator at 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 via TypeUtils::flattenTypes() and accept it when any member is iterable and not an array (i.e. can hold a Generator), OR-combining the per-member result. Previously the whole-type isIterable() returned maybe for ?Generator, causing a false positive under reportMaybes.
  • src/Rules/Generators/GeneratorReturnTypeHelper.php (new): getGeneratorType() extracts the iterable, non-array part of a return type.
  • src/Rules/Generators/YieldTypeRule.php and src/Rules/Generators/YieldFromTypeRule.php: run the declared return type through GeneratorReturnTypeHelper::getGeneratorType() before computing getIterableKeyType()/getIterableValueType() and the delegated TSend template type.

Root cause

Two related problems, both stemming from treating a generator's declared return type as a single iterable type:

  1. False positive in YieldInGeneratorRule. The rule applied $returnType->isIterable() to the entire type. For a union like Generator|null this is maybe (because null is 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.

  2. 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, …) contribute ErrorType via NonIterableTypeTrait. TypeCombinator::union(Food, ErrorType) collapses to *ERROR*, which accepts() everything — so yield <wrong-type> inside a ?Generator<int, Food> was never reported. The new GeneratorReturnTypeHelper strips the non-iterable/array members first, so the real Generator type drives the check.

Test

  • tests/PHPStan/Rules/Generators/data/yield-in-generator.php + YieldInGeneratorRuleTest: \Generator|int and \Generator|array are no longer reported; added \Generator|null, \Iterator|float, \Traversable|null, iterable|null (all valid, no error) and string|null (still invalid, reported).
  • tests/PHPStan/Rules/Generators/data/bug-6190.php + YieldTypeRuleTest::testBug6190: a ?Generator<int, Food> and an Iterator<int, Food>|float still report wrong yielded value/key types.
  • tests/PHPStan/Rules/Generators/data/bug-6190-from.php + YieldFromTypeRuleTest::testBug6190: yield from into 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

… 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants