From c1f7aafc47d007cbb82114ce1fb306a971333f26 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Mon, 22 Feb 2021 15:21:30 +0100 Subject: [PATCH] Keep iterable key type and value type when subtracting from iterable --- src/Type/TypeCombinator.php | 11 ++-- .../Analyser/NodeScopeResolverTest.php | 6 +++ tests/PHPStan/Analyser/data/bug-1233.php | 2 +- tests/PHPStan/Analyser/data/bug-4498.php | 50 +++++++++++++++++++ .../Rules/Methods/CallMethodsRuleTest.php | 8 +++ tests/PHPStan/Type/TypeCombinatorTest.php | 2 +- 6 files changed, 74 insertions(+), 5 deletions(-) create mode 100644 tests/PHPStan/Analyser/data/bug-4498.php diff --git a/src/Type/TypeCombinator.php b/src/Type/TypeCombinator.php index 19291fdae2..475a1d9a5e 100644 --- a/src/Type/TypeCombinator.php +++ b/src/Type/TypeCombinator.php @@ -11,6 +11,7 @@ use PHPStan\Type\Constant\ConstantFloatType; use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\Generic\GenericObjectType; use PHPStan\Type\Generic\TemplateType; class TypeCombinator @@ -61,13 +62,17 @@ public static function remove(Type $fromType, Type $typeToRemove): Type return new ConstantBooleanType(!$typeToRemove->getValue()); } } elseif ($fromType instanceof IterableType) { - $traversableType = new ObjectType(\Traversable::class); $arrayType = new ArrayType(new MixedType(), new MixedType()); if ($typeToRemove->isSuperTypeOf($arrayType)->yes()) { - return $traversableType; + return new GenericObjectType(\Traversable::class, [ + $fromType->getIterableKeyType(), + $fromType->getIterableValueType(), + ]); } + + $traversableType = new ObjectType(\Traversable::class); if ($typeToRemove->isSuperTypeOf($traversableType)->yes()) { - return $arrayType; + return new ArrayType($fromType->getIterableKeyType(), $fromType->getIterableValueType()); } } elseif ($fromType instanceof IntegerRangeType) { $type = $fromType->tryRemove($typeToRemove); diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 6b217ae6c1..c30d956ac5 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -10918,6 +10918,11 @@ public function dataBugInstanceOfClassString(): array return $this->gatherAssertTypes(__DIR__ . '/data/instanceof-class-string.php'); } + public function dataBug4498(): array + { + return $this->gatherAssertTypes(__DIR__ . '/data/bug-4498.php'); + } + /** * @param string $file * @return array @@ -11163,6 +11168,7 @@ private function gatherAssertTypes(string $file): array * @dataProvider dataBug3321 * @dataProvider dataBug3769 * @dataProvider dataBugInstanceOfClassString + * @dataProvider dataBug4498 * @param string $assertType * @param string $file * @param mixed ...$args diff --git a/tests/PHPStan/Analyser/data/bug-1233.php b/tests/PHPStan/Analyser/data/bug-1233.php index 901f1d1f55..99fe7748e2 100644 --- a/tests/PHPStan/Analyser/data/bug-1233.php +++ b/tests/PHPStan/Analyser/data/bug-1233.php @@ -17,7 +17,7 @@ public function toArray($value): array assertType('mixed~array', $value); if (is_iterable($value)) { - assertType('Traversable', $value); + assertType('Traversable', $value); return iterator_to_array($value); } diff --git a/tests/PHPStan/Analyser/data/bug-4498.php b/tests/PHPStan/Analyser/data/bug-4498.php new file mode 100644 index 0000000000..eb30e6f45c --- /dev/null +++ b/tests/PHPStan/Analyser/data/bug-4498.php @@ -0,0 +1,50 @@ + $iterable + * + * @return iterable + * + * @template TKey + * @template TValue + */ + public function fcn(iterable $iterable): iterable + { + if ($iterable instanceof \Traversable) { + assertType('iterable&Traversable', $iterable); + return $iterable; + } + + assertType('array', $iterable); + + return $iterable; + } + + /** + * @param iterable $iterable + * + * @return iterable + * + * @template TKey + * @template TValue + */ + public function bar(iterable $iterable): iterable + { + if (is_array($iterable)) { + assertType('array', $iterable); + return $iterable; + } + + assertType('Traversable', $iterable); + + return $iterable; + } + +} diff --git a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php index 71f2187929..460e053627 100644 --- a/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php @@ -1862,4 +1862,12 @@ public function testBug3321(): void $this->analyse([__DIR__ . '/../../Analyser/data/bug-3321.php'], []); } + public function testBug4498(): void + { + $this->checkThisOnly = false; + $this->checkNullables = true; + $this->checkUnionTypes = true; + $this->analyse([__DIR__ . '/../../Analyser/data/bug-4498.php'], []); + } + } diff --git a/tests/PHPStan/Type/TypeCombinatorTest.php b/tests/PHPStan/Type/TypeCombinatorTest.php index a42c4e2600..265362a6b7 100644 --- a/tests/PHPStan/Type/TypeCombinatorTest.php +++ b/tests/PHPStan/Type/TypeCombinatorTest.php @@ -3176,7 +3176,7 @@ public function dataRemove(): array new IterableType(new MixedType(), new MixedType()), new ArrayType(new MixedType(), new MixedType()), ObjectType::class, - 'Traversable', + 'Traversable', ], [ new IterableType(new MixedType(), new MixedType()),