Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions src/Rules/Generators/GeneratorReturnTypeHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Generators;

use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypeUtils;

final class GeneratorReturnTypeHelper
{

/**
* Extracts the part of a generator function's declared return type that can
* actually hold the yielded Generator. Nullable or union return types like
* ?Generator or Iterator|float are valid in PHP, but their non-iterable parts
* (null, float, ...) would otherwise poison getIterableKeyType()/getIterableValueType()
* with ErrorType and silently disable yield key/value type checks.
*/
public static function getGeneratorType(Type $returnType): Type
{
$iterableTypes = [];
foreach (TypeUtils::flattenTypes($returnType) as $innerType) {
if ($innerType->isIterable()->no() || $innerType->isArray()->yes()) {

Check warning on line 23 in src/Rules/Generators/GeneratorReturnTypeHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ { $iterableTypes = []; foreach (TypeUtils::flattenTypes($returnType) as $innerType) { - if ($innerType->isIterable()->no() || $innerType->isArray()->yes()) { + if ($innerType->isIterable()->no() || !$innerType->isArray()->no()) { continue; }

Check warning on line 23 in src/Rules/Generators/GeneratorReturnTypeHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ { $iterableTypes = []; foreach (TypeUtils::flattenTypes($returnType) as $innerType) { - if ($innerType->isIterable()->no() || $innerType->isArray()->yes()) { + if ($innerType->isIterable()->no() || !$innerType->isArray()->no()) { continue; }

Check warning on line 23 in src/Rules/Generators/GeneratorReturnTypeHelper.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ { $iterableTypes = []; foreach (TypeUtils::flattenTypes($returnType) as $innerType) { - if ($innerType->isIterable()->no() || $innerType->isArray()->yes()) { + if (!$innerType->isIterable()->yes() || $innerType->isArray()->yes()) { continue; }
continue;
}

$iterableTypes[] = $innerType;
}

if ($iterableTypes === []) {
return $returnType;
}

return TypeCombinator::union(...$iterableTypes);
}

}
4 changes: 3 additions & 1 deletion src/Rules/Generators/YieldFromTypeRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
14 changes: 11 additions & 3 deletions src/Rules/Generators/YieldInGeneratorRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use PHPStan\TrinaryLogic;
use PHPStan\Type\MixedType;
use PHPStan\Type\NeverType;
use PHPStan\Type\TypeUtils;
use function sprintf;

/**
Expand Down Expand Up @@ -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 [];
Expand Down
2 changes: 2 additions & 0 deletions src/Rules/Generators/YieldTypeRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
10 changes: 10 additions & 0 deletions tests/PHPStan/Rules/Generators/YieldFromTypeRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
],
]);
}

}
16 changes: 8 additions & 8 deletions tests/PHPStan/Rules/Generators/YieldInGeneratorRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
],
]);
}

Expand Down
18 changes: 18 additions & 0 deletions tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
],
]);
}

}
24 changes: 24 additions & 0 deletions tests/PHPStan/Rules/Generators/data/bug-6190-from.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace Bug6190From;

class Food
{

public static function bone(): self
{
return new self();
}

}

/**
* @param \Generator<int, Food> $good
* @param \Generator<int, int> $bad
* @return \Generator<int, Food>|null
*/
function nullableGenerator($good, $bad)
{
yield from $good;
yield from $bad;
}
39 changes: 39 additions & 0 deletions tests/PHPStan/Rules/Generators/data/bug-6190.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

namespace Bug6190;

class Food
{

public static function bone(): self
{
return new self();
}

}

/**
* @return \Generator<int, Food>|null
*/
function nullableGenerator()
{
yield Food::bone();
yield 5;
}

/**
* @return \Iterator<int, Food>|float
*/
function unionGenerator()
{
yield Food::bone();
yield 'foo';
}

/**
* @return \Generator<int, Food>|null
*/
function nullableGeneratorWrongKey()
{
yield 'key' => Food::bone();
}
45 changes: 45 additions & 0 deletions tests/PHPStan/Rules/Generators/data/yield-in-generator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Loading