diff --git a/src/Rules/Generators/GeneratorReturnTypeHelper.php b/src/Rules/Generators/GeneratorReturnTypeHelper.php new file mode 100644 index 00000000000..c185fa16789 --- /dev/null +++ b/src/Rules/Generators/GeneratorReturnTypeHelper.php @@ -0,0 +1,37 @@ +isIterable()->no() || $innerType->isArray()->yes()) { + continue; + } + + $iterableTypes[] = $innerType; + } + + if ($iterableTypes === []) { + return $returnType; + } + + return TypeCombinator::union(...$iterableTypes); + } + +} diff --git a/src/Rules/Generators/YieldFromTypeRule.php b/src/Rules/Generators/YieldFromTypeRule.php index b77f04b53a4..7c2ae146866 100644 --- a/src/Rules/Generators/YieldFromTypeRule.php +++ b/src/Rules/Generators/YieldFromTypeRule.php @@ -81,6 +81,8 @@ public function processNode(Node $node, Scope $scope): array return []; } + $returnType = GeneratorReturnTypeHelper::getGeneratorType($returnType); + $messages = []; $acceptsKey = $this->ruleLevelHelper->accepts($returnType->getIterableKeyType(), $exprType->getIterableKeyType(), $scope->isDeclareStrictTypes()); if (!$acceptsKey->result) { @@ -115,7 +117,7 @@ public function processNode(Node $node, Scope $scope): array return $messages; } - $currentReturnType = $scopeFunction->getReturnType(); + $currentReturnType = GeneratorReturnTypeHelper::getGeneratorType($scopeFunction->getReturnType()); $exprSendType = $exprType->getTemplateType(Generator::class, 'TSend'); $thisSendType = $currentReturnType->getTemplateType(Generator::class, 'TSend'); if ($exprSendType instanceof ErrorType || $thisSendType instanceof ErrorType) { diff --git a/src/Rules/Generators/YieldInGeneratorRule.php b/src/Rules/Generators/YieldInGeneratorRule.php index 601c8f43a4d..3d95bbb88f3 100644 --- a/src/Rules/Generators/YieldInGeneratorRule.php +++ b/src/Rules/Generators/YieldInGeneratorRule.php @@ -11,6 +11,7 @@ use PHPStan\TrinaryLogic; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; +use PHPStan\Type\TypeUtils; use function sprintf; /** @@ -60,9 +61,16 @@ public function processNode(Node $node, Scope $scope): array if ($returnType instanceof NeverType && $returnType->isExplicit()) { $isSuperType = TrinaryLogic::createNo(); } else { - $isSuperType = $returnType->isIterable()->and(TrinaryLogic::createFromBoolean( - !$returnType->isArray()->yes(), - )); + // A function containing yield always returns a Generator at runtime, so the + // declared return type is valid as long as at least one of its parts can hold + // a Generator (iterable but not array). This mirrors PHP's own rule, which + // permits nullable and union return types like ?Generator or Iterator|float. + $isSuperType = TrinaryLogic::createNo(); + foreach (TypeUtils::flattenTypes($returnType) as $innerType) { + $isSuperType = $isSuperType->or($innerType->isIterable()->and(TrinaryLogic::createFromBoolean( + !$innerType->isArray()->yes(), + ))); + } } if ($isSuperType->yes()) { return []; diff --git a/src/Rules/Generators/YieldTypeRule.php b/src/Rules/Generators/YieldTypeRule.php index e5f94c81932..adb165e4f2f 100644 --- a/src/Rules/Generators/YieldTypeRule.php +++ b/src/Rules/Generators/YieldTypeRule.php @@ -48,6 +48,8 @@ public function processNode(Node $node, Scope $scope): array return []; } + $returnType = GeneratorReturnTypeHelper::getGeneratorType($returnType); + if ($node->key === null) { $keyType = new IntegerType(); } else { diff --git a/tests/PHPStan/Rules/Generators/YieldFromTypeRuleTest.php b/tests/PHPStan/Rules/Generators/YieldFromTypeRuleTest.php index 8bf38677d9f..8e8da530871 100644 --- a/tests/PHPStan/Rules/Generators/YieldFromTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generators/YieldFromTypeRuleTest.php @@ -65,4 +65,14 @@ public function testBug11517(): void $this->analyse([__DIR__ . '/data/bug-11517.php'], []); } + public function testBug6190(): void + { + $this->analyse([__DIR__ . '/data/bug-6190-from.php'], [ + [ + 'Generator expects value type Bug6190From\Food, int given.', + 23, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Generators/YieldInGeneratorRuleTest.php b/tests/PHPStan/Rules/Generators/YieldInGeneratorRuleTest.php index 7e234250d84..18dd9cf673c 100644 --- a/tests/PHPStan/Rules/Generators/YieldInGeneratorRuleTest.php +++ b/tests/PHPStan/Rules/Generators/YieldInGeneratorRuleTest.php @@ -27,14 +27,6 @@ public function testRule(): void 'Yield can be used only with these return types: Generator, Iterator, Traversable, iterable.', 14, ], - [ - 'Yield can be used only with these return types: Generator, Iterator, Traversable, iterable.', - 31, - ], - [ - 'Yield can be used only with these return types: Generator, Iterator, Traversable, iterable.', - 32, - ], [ 'Yield can be used only with these return types: Generator, Iterator, Traversable, iterable.', 37, @@ -59,6 +51,14 @@ public function testRule(): void 'Yield can be used only with these return types: Generator, Iterator, Traversable, iterable.', 88, ], + [ + 'Yield can be used only with these return types: Generator, Iterator, Traversable, iterable.', + 132, + ], + [ + 'Yield can be used only with these return types: Generator, Iterator, Traversable, iterable.', + 133, + ], ]); } diff --git a/tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php b/tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php index 555ae5f5baf..fedf2fab260 100644 --- a/tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php +++ b/tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php @@ -87,4 +87,22 @@ public function testYieldOversizedSelfRejection(): void $this->analyse([__DIR__ . '/data/bug-yield-oversized-self-rejection.php'], []); } + public function testBug6190(): void + { + $this->analyse([__DIR__ . '/data/bug-6190.php'], [ + [ + 'Generator expects value type Bug6190\Food, int given.', + 21, + ], + [ + 'Generator expects value type Bug6190\Food, string given.', + 30, + ], + [ + 'Generator expects key type int, string given.', + 38, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Generators/data/bug-6190-from.php b/tests/PHPStan/Rules/Generators/data/bug-6190-from.php new file mode 100644 index 00000000000..92e56999344 --- /dev/null +++ b/tests/PHPStan/Rules/Generators/data/bug-6190-from.php @@ -0,0 +1,24 @@ + $good + * @param \Generator $bad + * @return \Generator|null + */ +function nullableGenerator($good, $bad) +{ + yield from $good; + yield from $bad; +} diff --git a/tests/PHPStan/Rules/Generators/data/bug-6190.php b/tests/PHPStan/Rules/Generators/data/bug-6190.php new file mode 100644 index 00000000000..300d87e836e --- /dev/null +++ b/tests/PHPStan/Rules/Generators/data/bug-6190.php @@ -0,0 +1,39 @@ +|null + */ +function nullableGenerator() +{ + yield Food::bone(); + yield 5; +} + +/** + * @return \Iterator|float + */ +function unionGenerator() +{ + yield Food::bone(); + yield 'foo'; +} + +/** + * @return \Generator|null + */ +function nullableGeneratorWrongKey() +{ + yield 'key' => Food::bone(); +} diff --git a/tests/PHPStan/Rules/Generators/data/yield-in-generator.php b/tests/PHPStan/Rules/Generators/data/yield-in-generator.php index d896ab49861..9ef2b530cdc 100644 --- a/tests/PHPStan/Rules/Generators/data/yield-in-generator.php +++ b/tests/PHPStan/Rules/Generators/data/yield-in-generator.php @@ -87,3 +87,48 @@ function doNever() yield 1; yield from doFoo(); } + +/** + * @return \Generator|null + */ +function doNullableGenerator() +{ + yield 1; + yield from doFoo(); +} + +/** + * @return \Iterator|float + */ +function doIteratorFloat() +{ + yield 1; + yield from doFoo(); +} + +/** + * @return \Traversable|null + */ +function doNullableTraversable() +{ + yield 1; + yield from doFoo(); +} + +/** + * @return iterable|null + */ +function doNullableIterable() +{ + yield 1; + yield from doFoo(); +} + +/** + * @return string|null + */ +function doNullableString() +{ + yield 1; + yield from doFoo(); +}