diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 6e73c78738..8042f03e03 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -618,10 +618,10 @@ public function getAnonymousFunctionReturnType(): ?Type public function getType(Expr $node): Type { if ($node instanceof GetIterableKeyTypeExpr) { - return $this->getType($node->getExpr())->getIterableKeyType(); + return $this->getIterableKeyType($this->getType($node->getExpr())); } if ($node instanceof GetIterableValueTypeExpr) { - return $this->getType($node->getExpr())->getIterableValueType(); + return $this->getIterableValueType($this->getType($node->getExpr())); } if ($node instanceof GetOffsetValueTypeExpr) { return $this->getType($node->getVar())->getOffsetValueType($this->getType($node->getDim())); @@ -1201,8 +1201,8 @@ private function resolveType(string $exprString, Expr $node): Type } } else { $yieldFromType = $arrowScope->getType($yieldNode->expr); - $keyType = $yieldFromType->getIterableKeyType(); - $valueType = $yieldFromType->getIterableValueType(); + $keyType = $arrowScope->getIterableKeyType($yieldFromType); + $valueType = $arrowScope->getIterableValueType($yieldFromType); } $returnType = new GenericObjectType(Generator::class, [ @@ -1305,8 +1305,8 @@ private function resolveType(string $exprString, Expr $node): Type } $yieldFromType = $yieldScope->getType($yieldNode->expr); - $keyTypes[] = $yieldFromType->getIterableKeyType(); - $valueTypes[] = $yieldFromType->getIterableValueType(); + $keyTypes[] = $yieldScope->getIterableKeyType($yieldFromType); + $valueTypes[] = $yieldScope->getIterableValueType($yieldFromType); } $returnType = new GenericObjectType(Generator::class, [ @@ -3115,7 +3115,11 @@ public function enterForeach(Expr $iteratee, string $valueName, ?string $keyName { $iterateeType = $this->getType($iteratee); $nativeIterateeType = $this->getNativeType($iteratee); - $scope = $this->assignVariable($valueName, $iterateeType->getIterableValueType(), $nativeIterateeType->getIterableValueType()); + $scope = $this->assignVariable( + $valueName, + $this->getIterableValueType($iterateeType), + $this->getIterableValueType($nativeIterateeType), + ); if ($keyName !== null) { $scope = $scope->enterForeachKey($iteratee, $keyName); } @@ -3127,13 +3131,17 @@ public function enterForeachKey(Expr $iteratee, string $keyName): self { $iterateeType = $this->getType($iteratee); $nativeIterateeType = $this->getNativeType($iteratee); - $scope = $this->assignVariable($keyName, $iterateeType->getIterableKeyType(), $nativeIterateeType->getIterableKeyType()); + $scope = $this->assignVariable( + $keyName, + $this->getIterableKeyType($iterateeType), + $this->getIterableKeyType($nativeIterateeType), + ); if ($iterateeType->isArray()->yes()) { $scope = $scope->assignExpression( new Expr\ArrayDimFetch($iteratee, new Variable($keyName)), - $iterateeType->getIterableValueType(), - $nativeIterateeType->getIterableValueType(), + $this->getIterableValueType($iterateeType), + $this->getIterableValueType($nativeIterateeType), ); } @@ -5013,4 +5021,44 @@ private function getNativeConstantTypes(): array return $constantTypes; } + public function getIterableKeyType(Type $iteratee): Type + { + if ($iteratee instanceof UnionType) { + $newTypes = []; + foreach ($iteratee->getTypes() as $innerType) { + if (!$innerType->isIterable()->yes()) { + continue; + } + + $newTypes[] = $innerType; + } + if (count($newTypes) === 0) { + return $iteratee->getIterableKeyType(); + } + $iteratee = TypeCombinator::union(...$newTypes); + } + + return $iteratee->getIterableKeyType(); + } + + public function getIterableValueType(Type $iteratee): Type + { + if ($iteratee instanceof UnionType) { + $newTypes = []; + foreach ($iteratee->getTypes() as $innerType) { + if (!$innerType->isIterable()->yes()) { + continue; + } + + $newTypes[] = $innerType; + } + if (count($newTypes) === 0) { + return $iteratee->getIterableValueType(); + } + $iteratee = TypeCombinator::union(...$newTypes); + } + + return $iteratee->getIterableValueType(); + } + } diff --git a/src/Analyser/Scope.php b/src/Analyser/Scope.php index bad37ff852..8db13ed44b 100644 --- a/src/Analyser/Scope.php +++ b/src/Analyser/Scope.php @@ -63,6 +63,10 @@ public function getMethodReflection(Type $typeWithMethod, string $methodName): ? public function getConstantReflection(Type $typeWithConstant, string $constantName): ?ConstantReflection; + public function getIterableKeyType(Type $iteratee): Type; + + public function getIterableValueType(Type $iteratee): Type; + public function isInAnonymousFunction(): bool; public function getAnonymousFunctionReflection(): ?ParametersAcceptor; diff --git a/src/Dependency/DependencyResolver.php b/src/Dependency/DependencyResolver.php index e0ed43d35c..9a8d0b95ac 100644 --- a/src/Dependency/DependencyResolver.php +++ b/src/Dependency/DependencyResolver.php @@ -404,12 +404,13 @@ public function resolveDependencies(Node $node, Scope $scope): NodeDependencies } elseif ($node instanceof Foreach_) { $exprType = $scope->getType($node->expr); if ($node->keyVar !== null) { - foreach ($exprType->getIterableKeyType()->getReferencedClasses() as $referencedClass) { + + foreach ($scope->getIterableKeyType($exprType)->getReferencedClasses() as $referencedClass) { $this->addClassToDependencies($referencedClass, $dependenciesReflections); } } - foreach ($exprType->getIterableValueType()->getReferencedClasses() as $referencedClass) { + foreach ($scope->getIterableValueType($exprType)->getReferencedClasses() as $referencedClass) { $this->addClassToDependencies($referencedClass, $dependenciesReflections); } } elseif ( diff --git a/src/Reflection/ParametersAcceptorSelector.php b/src/Reflection/ParametersAcceptorSelector.php index e665f02567..915e98e0e8 100644 --- a/src/Reflection/ParametersAcceptorSelector.php +++ b/src/Reflection/ParametersAcceptorSelector.php @@ -18,7 +18,6 @@ use PHPStan\Type\BooleanType; use PHPStan\Type\CallableType; use PHPStan\Type\Constant\ConstantIntegerType; -use PHPStan\Type\ErrorType; use PHPStan\Type\Generic\TemplateType; use PHPStan\Type\Generic\TemplateTypeMap; use PHPStan\Type\IntegerType; @@ -90,7 +89,7 @@ public static function selectFromArgs( $parameters = $acceptor->getParameters(); $callbackParameters = []; foreach ($arrayMapArgs as $arg) { - $callbackParameters[] = new DummyParameter('item', self::getIterableValueType($scope->getType($arg->value)), false, PassedByReference::createNo(), false, null); + $callbackParameters[] = new DummyParameter('item', $scope->getIterableValueType($scope->getType($arg->value)), false, PassedByReference::createNo(), false, null); } $parameters[0] = new NativeParameterReflection( $parameters[0]->getName(), @@ -151,12 +150,12 @@ public static function selectFromArgs( if ($mode instanceof ConstantIntegerType) { if ($mode->getValue() === ARRAY_FILTER_USE_KEY) { $arrayFilterParameters = [ - new DummyParameter('key', self::getIterableKeyType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null), + new DummyParameter('key', $scope->getIterableKeyType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null), ]; } elseif ($mode->getValue() === ARRAY_FILTER_USE_BOTH) { $arrayFilterParameters = [ - new DummyParameter('item', self::getIterableValueType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null), - new DummyParameter('key', self::getIterableKeyType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null), + new DummyParameter('item', $scope->getIterableValueType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null), + new DummyParameter('key', $scope->getIterableKeyType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null), ]; } } @@ -169,7 +168,7 @@ public static function selectFromArgs( $parameters[1]->isOptional(), new CallableType( $arrayFilterParameters ?? [ - new DummyParameter('item', self::getIterableValueType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null), + new DummyParameter('item', $scope->getIterableValueType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null), ], new MixedType(), false, @@ -191,8 +190,8 @@ public static function selectFromArgs( if (isset($args[0]) && (bool) $args[0]->getAttribute(ArrayWalkArgVisitor::ATTRIBUTE_NAME)) { $arrayWalkParameters = [ - new DummyParameter('item', self::getIterableValueType($scope->getType($args[0]->value)), false, PassedByReference::createReadsArgument(), false, null), - new DummyParameter('key', self::getIterableKeyType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null), + new DummyParameter('item', $scope->getIterableValueType($scope->getType($args[0]->value)), false, PassedByReference::createReadsArgument(), false, null), + new DummyParameter('key', $scope->getIterableKeyType($scope->getType($args[0]->value)), false, PassedByReference::createNo(), false, null), ]; if (isset($args[2])) { $arrayWalkParameters[] = new DummyParameter('arg', $scope->getType($args[2]->value), false, PassedByReference::createNo(), false, null); @@ -548,44 +547,6 @@ private static function wrapParameter(ParameterReflection $parameter): Parameter ); } - private static function getIterableValueType(Type $type): Type - { - if ($type instanceof UnionType) { - $types = []; - foreach ($type->getTypes() as $innerType) { - $iterableValueType = $innerType->getIterableValueType(); - if ($iterableValueType instanceof ErrorType) { - continue; - } - - $types[] = $iterableValueType; - } - - return TypeCombinator::union(...$types); - } - - return $type->getIterableValueType(); - } - - private static function getIterableKeyType(Type $type): Type - { - if ($type instanceof UnionType) { - $types = []; - foreach ($type->getTypes() as $innerType) { - $iterableKeyType = $innerType->getIterableKeyType(); - if ($iterableKeyType instanceof ErrorType) { - continue; - } - - $types[] = $iterableKeyType; - } - - return TypeCombinator::union(...$types); - } - - return $type->getIterableKeyType(); - } - private static function getCurlOptValueType(int $curlOpt): ?Type { if (defined('CURLOPT_SSL_VERIFYHOST') && $curlOpt === CURLOPT_SSL_VERIFYHOST) { diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 40fd65d589..6c2623e32f 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -1277,6 +1277,7 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/image-size.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/base64_decode.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9404.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/foreach-partially-non-iterable.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/globals.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9208.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/finite-types.php'); diff --git a/tests/PHPStan/Analyser/data/foreach-partially-non-iterable.php b/tests/PHPStan/Analyser/data/foreach-partially-non-iterable.php new file mode 100644 index 0000000000..f982f1d0ff --- /dev/null +++ b/tests/PHPStan/Analyser/data/foreach-partially-non-iterable.php @@ -0,0 +1,21 @@ +|false $a + */ + public function doFoo($a): void + { + foreach ($a as $k => $v) { + assertType('string', $k); + assertType('int', $v); + } + } + +}