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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Yield cannot be used with nullable/union return types

2 participants