diff --git a/src/Type/Php/GeneratorSendReturnTypeExtension.php b/src/Type/Php/GeneratorSendReturnTypeExtension.php new file mode 100644 index 00000000000..8137db2b79c --- /dev/null +++ b/src/Type/Php/GeneratorSendReturnTypeExtension.php @@ -0,0 +1,58 @@ +getName() === 'send'; + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + { + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $methodReflection->getVariants(), + ); + + $returnType = $parametersAcceptor->getReturnType(); + + if ($parametersAcceptor instanceof ExtendedParametersAcceptor) { + $nativeReturnType = $parametersAcceptor->getNativeReturnType(); + if ($nativeReturnType->isSuperTypeOf(new NullType())->no()) { + return $returnType; + } + } + + $result = TypeCombinator::addNull($returnType); + if ($returnType instanceof BenevolentUnionType && !($result instanceof BenevolentUnionType)) { + $result = TypeUtils::toBenevolentUnion($result); + } + + return $result; + } + +} diff --git a/src/Type/Php/IteratorCurrentReturnTypeExtension.php b/src/Type/Php/IteratorCurrentReturnTypeExtension.php new file mode 100644 index 00000000000..5295107af02 --- /dev/null +++ b/src/Type/Php/IteratorCurrentReturnTypeExtension.php @@ -0,0 +1,59 @@ +getName(), ['current', 'key'], true); + } + + public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type + { + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $methodCall->getArgs(), + $methodReflection->getVariants(), + ); + + $returnType = $parametersAcceptor->getReturnType(); + + if ($parametersAcceptor instanceof ExtendedParametersAcceptor) { + $nativeReturnType = $parametersAcceptor->getNativeReturnType(); + if ($nativeReturnType->isSuperTypeOf(new NullType())->no()) { + return $returnType; + } + } + + $result = TypeCombinator::addNull($returnType); + if ($returnType instanceof BenevolentUnionType && !($result instanceof BenevolentUnionType)) { + $result = TypeUtils::toBenevolentUnion($result); + } + + return $result; + } + +} diff --git a/src/Type/Php/IteratorValidMethodTypeSpecifyingExtension.php b/src/Type/Php/IteratorValidMethodTypeSpecifyingExtension.php new file mode 100644 index 00000000000..a4da26faae9 --- /dev/null +++ b/src/Type/Php/IteratorValidMethodTypeSpecifyingExtension.php @@ -0,0 +1,72 @@ +typeSpecifier = $typeSpecifier; + } + + public function getClass(): string + { + return Iterator::class; + } + + public function isMethodSupported(MethodReflection $methodReflection, MethodCall $node, TypeSpecifierContext $context): bool + { + return $methodReflection->getName() === 'valid' + && $context->truthy(); + } + + public function specifyTypes(MethodReflection $methodReflection, MethodCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes + { + $calledOnType = $scope->getType($node->var); + $types = new SpecifiedTypes(); + + foreach (['current', 'key'] as $methodName) { + $methodCallExpr = new MethodCall($node->var, new Identifier($methodName)); + + $targetMethodReflection = $scope->getMethodReflection($calledOnType, $methodName); + if ($targetMethodReflection === null) { + continue; + } + + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + [], + $targetMethodReflection->getVariants(), + ); + + $baseReturnType = $parametersAcceptor->getReturnType(); + + $types = $types->unionWith($this->typeSpecifier->create( + $methodCallExpr, + $baseReturnType, + TypeSpecifierContext::createTrue(), + $scope, + )); + } + + return $types; + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-3674.php b/tests/PHPStan/Analyser/nsrt/bug-3674.php new file mode 100644 index 00000000000..69e8b45aa9d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-3674.php @@ -0,0 +1,237 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug3674; + +use ArrayIterator; +use Generator; +use Iterator; +use function PHPStan\Testing\assertType; + +/** @return Generator */ +function gen(): Generator { yield 'hello'; } + +function testGeneratorCurrent(): void +{ + $it = gen(); + assertType('string|null', $it->current()); + assertType('int|null', $it->key()); +} + +function testGeneratorAfterValid(): void +{ + $it = gen(); + if ($it->valid()) { + assertType('string', $it->current()); + assertType('int', $it->key()); + } +} + +function testGeneratorInForeach(): void +{ + foreach (gen() as $key => $value) { + assertType('string', $value); + assertType('int', $key); + } +} + +/** @param Iterator $it */ +function testIteratorCurrent(Iterator $it): void +{ + assertType('string|null', $it->current()); + assertType('int|null', $it->key()); +} + +/** @param Iterator $it */ +function testIteratorAfterValid(Iterator $it): void +{ + if ($it->valid()) { + assertType('string', $it->current()); + assertType('int', $it->key()); + } +} + +/** @param Iterator $it */ +function testIteratorInForeach(Iterator $it): void +{ + foreach ($it as $key => $value) { + assertType('string', $value); + assertType('int', $key); + } +} + +/** @param ArrayIterator $it */ +function testArrayIteratorCurrent(ArrayIterator $it): void +{ + assertType('string|null', $it->current()); + assertType('int|null', $it->key()); +} + +/** @param ArrayIterator $it */ +function testArrayIteratorAfterValid(ArrayIterator $it): void +{ + if ($it->valid()) { + assertType('string', $it->current()); + assertType('int', $it->key()); + } +} + +function testGeneratorWhileLoop(): void +{ + $it = gen(); + $it->rewind(); + while ($it->valid()) { + assertType('string', $it->current()); + assertType('int', $it->key()); + $it->next(); + } +} + +function testGeneratorSend(): void +{ + /** @var Generator $gen */ + $gen = gen(); + assertType('string|null', $gen->send(42)); +} + +/** @return Generator */ +function genInt(): Generator { yield 1; } + +function testOriginalIssue(): void +{ + $it = genInt(); + assertType('int|null', $it->current()); +} + +/** @param Iterator $it */ +function testNegatedValid(Iterator $it): void +{ + if (!$it->valid()) { + assertType('string|null', $it->current()); + assertType('int|null', $it->key()); + } +} + +function testWhileLoopWithValid(): void +{ + $it = gen(); + while ($it->valid()) { + $v = $it->current(); + assertType('string', $v); + $k = $it->key(); + assertType('int', $k); + $it->next(); + } + assertType('string|null', $it->current()); + assertType('int|null', $it->key()); +} + +/** + * @template T + * @implements Iterator + */ +class CustomIterator implements Iterator +{ + /** @return T|null */ + public function current(): mixed { return null; } + public function key(): int { return 0; } + public function next(): void {} + public function rewind(): void {} + public function valid(): bool { return false; } +} + +/** @param CustomIterator $it */ +function testCustomIterator(CustomIterator $it): void +{ + assertType('string|null', $it->current()); + assertType('int', $it->key()); + if ($it->valid()) { + assertType('string|null', $it->current()); + assertType('int', $it->key()); + } +} + +/** @param \IteratorIterator> $it */ +function testIteratorIterator(\IteratorIterator $it): void +{ + assertType('string|null', $it->current()); + assertType('int|null', $it->key()); + if ($it->valid()) { + assertType('string', $it->current()); + assertType('int', $it->key()); + } +} + +/** @param \NoRewindIterator> $it */ +function testNoRewindIterator(\NoRewindIterator $it): void +{ + assertType('string|null', $it->current()); + assertType('int|null', $it->key()); + if ($it->valid()) { + assertType('string', $it->current()); + assertType('int', $it->key()); + } +} + +/** @param Iterator $it */ +function testNextResetsNarrowing(Iterator $it): void +{ + if ($it->valid()) { + assertType('string', $it->current()); + assertType('int', $it->key()); + $it->next(); + assertType('string|null', $it->current()); + assertType('int|null', $it->key()); + } +} + +/** @param Iterator $it */ +function testRewindResetsNarrowing(Iterator $it): void +{ + if (!$it->valid()) { + assertType('string|null', $it->current()); + assertType('int|null', $it->key()); + $it->rewind(); + assertType('string|null', $it->current()); + assertType('int|null', $it->key()); + } +} + +/** + * @implements Iterator + */ +class NonNullIterator implements Iterator +{ + public function current(): string { return 'hello'; } + public function key(): int { return 0; } + public function next(): void {} + public function rewind(): void {} + public function valid(): bool { return false; } +} + +function testNonNullOverride(NonNullIterator $it): void +{ + assertType('string', $it->current()); + assertType('int', $it->key()); +} + +/** @param Iterator $it */ +function testNullInTemplateType(Iterator $it): void +{ + assertType('string|null', $it->current()); + assertType('int|null', $it->key()); + if ($it->valid()) { + assertType('string|null', $it->current()); + assertType('int|null', $it->key()); + } +} + +/** @param Iterator $it */ +function testNullInTemplateTypeForeach(Iterator $it): void +{ + foreach ($it as $key => $value) { + assertType('string|null', $value); + assertType('int|null', $key); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-7519.php b/tests/PHPStan/Analyser/nsrt/bug-7519.php index 1fd556f0e3a..b8201139bbf 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-7519.php +++ b/tests/PHPStan/Analyser/nsrt/bug-7519.php @@ -41,8 +41,8 @@ function doFoo() { $iterator = new FooFilterIterator($generator()); - assertType('array{}|bool|stdClass', $iterator->key()); - assertType('array{}|bool|stdClass', $iterator->current()); + assertType('array{}|bool|stdClass|null', $iterator->key()); + assertType('array{}|bool|stdClass|null', $iterator->current()); $generator = static function (): Generator { yield true => true; @@ -51,6 +51,6 @@ function doFoo() { $iterator = new FooFilterIterator($generator()); - assertType('bool', $iterator->key()); - assertType('bool', $iterator->current()); + assertType('bool|null', $iterator->key()); + assertType('bool|null', $iterator->current()); } diff --git a/tests/PHPStan/Rules/Methods/data/infer-array-key.php b/tests/PHPStan/Rules/Methods/data/infer-array-key.php index 86ab98a1b5c..2eeaeb49517 100644 --- a/tests/PHPStan/Rules/Methods/data/infer-array-key.php +++ b/tests/PHPStan/Rules/Methods/data/infer-array-key.php @@ -17,7 +17,7 @@ class Foo implements \IteratorAggregate public function getIterator() { $it = new \ArrayIterator($this->items); - assertType('(int|string)', $it->key()); + assertType('(int|string|null)', $it->key()); return $it; } @@ -37,7 +37,7 @@ class Bar implements \IteratorAggregate public function getIterator() { $it = new \ArrayIterator($this->items); - assertType('int', $it->key()); + assertType('int|null', $it->key()); return $it; } @@ -57,7 +57,7 @@ class Baz implements \IteratorAggregate public function getIterator() { $it = new \ArrayIterator($this->items); - assertType('string', $it->key()); + assertType('string|null', $it->key()); return $it; } @@ -77,7 +77,7 @@ class Lorem implements \IteratorAggregate public function getIterator() { $it = new \ArrayIterator($this->items); - assertType('(int|string)', $it->key()); + assertType('(int|string|null)', $it->key()); return $it; } @@ -97,7 +97,7 @@ class Ipsum implements \IteratorAggregate public function getIterator() { $it = new \ArrayIterator($this->items); - assertType('int|string', $it->key()); + assertType('int|string|null', $it->key()); return $it; }